diff --git a/android/app/src/main/java/com/github/quarck/calnotify/app/ApplicationController.kt b/android/app/src/main/java/com/github/quarck/calnotify/app/ApplicationController.kt
index fcae9937..d1323e62 100644
--- a/android/app/src/main/java/com/github/quarck/calnotify/app/ApplicationController.kt
+++ b/android/app/src/main/java/com/github/quarck/calnotify/app/ApplicationController.kt
@@ -943,19 +943,8 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
): SnoozeResult? {
val now = clock.currentTimeMillis()
return snoozeEvents(context, { event ->
- // Pinned events are excluded from batch snooze
!event.isPinned &&
- // Search query filter (existing behavior)
- (searchQuery?.let { query ->
- event.title.contains(query, ignoreCase = true) ||
- event.desc.contains(query, ignoreCase = true)
- } ?: true) &&
- // FilterState filters
- (filterState?.let { filter ->
- filter.matchesCalendar(event) &&
- filter.matchesStatus(event) &&
- filter.matchesTime(event, now)
- } ?: true)
+ FilterState.matchesSearchAndFilters(event, searchQuery, filterState, now)
}, snoozeDelay, isChange, onlySnoozeVisible)
}
@@ -1122,12 +1111,17 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
}
}
- fun muteAllVisibleEvents(context: Context) {
-
+ fun muteAllVisibleEvents(
+ context: Context,
+ searchQuery: String? = null,
+ filterState: FilterState? = null
+ ) {
+ val now = clock.currentTimeMillis()
getEventsStorage(context).use {
db ->
val eventsToMute = db.events.filter {
- event -> (event.snoozedUntil == 0L) && event.isNotSpecial && !event.isTask
+ event -> (event.snoozedUntil == 0L) && event.isNotSpecial && !event.isTask &&
+ FilterState.matchesSearchAndFilters(event, searchQuery, filterState, now)
}
if (eventsToMute.isNotEmpty()) {
@@ -1146,10 +1140,16 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
}
}
- fun pinAllVisibleEvents(context: Context) {
+ fun pinAllVisibleEvents(
+ context: Context,
+ searchQuery: String? = null,
+ filterState: FilterState? = null
+ ) {
+ val now = clock.currentTimeMillis()
getEventsStorage(context).use { db ->
val eventsToPin = db.events.filter {
- event -> (event.snoozedUntil == 0L) && event.isNotSpecial && !event.isPinned
+ event -> (event.snoozedUntil == 0L) && event.isNotSpecial && !event.isPinned &&
+ FilterState.matchesSearchAndFilters(event, searchQuery, filterState, now)
}
if (eventsToPin.isNotEmpty()) {
val pinnedEvents = eventsToPin.map { it.isPinned = true; it }
@@ -1158,9 +1158,16 @@ object ApplicationController : ApplicationControllerInterface, EventMovedHandler
}
}
- fun unpinAllVisibleEvents(context: Context) {
+ fun unpinAllVisibleEvents(
+ context: Context,
+ searchQuery: String? = null,
+ filterState: FilterState? = null
+ ) {
+ val now = clock.currentTimeMillis()
getEventsStorage(context).use { db ->
- val eventsToUnpin = db.events.filter { event -> event.isPinned }
+ val eventsToUnpin = db.events.filter { event -> event.isPinned &&
+ FilterState.matchesSearchAndFilters(event, searchQuery, filterState, now)
+ }
if (eventsToUnpin.isNotEmpty()) {
val unpinnedEvents = eventsToUnpin.map { it.isPinned = false; it }
db.updateEvents(unpinnedEvents)
diff --git a/android/app/src/main/java/com/github/quarck/calnotify/ui/FilterState.kt b/android/app/src/main/java/com/github/quarck/calnotify/ui/FilterState.kt
index db767b83..3e153a58 100644
--- a/android/app/src/main/java/com/github/quarck/calnotify/ui/FilterState.kt
+++ b/android/app/src/main/java/com/github/quarck/calnotify/ui/FilterState.kt
@@ -90,6 +90,24 @@ data class FilterState(
) {
companion object {
+ /**
+ * Checks if an event matches the given search query and filter state.
+ * Handles nullable parameters: null searchQuery or filterState means "match all".
+ */
+ fun matchesSearchAndFilters(
+ event: EventAlertRecord,
+ searchQuery: String?,
+ filterState: FilterState?,
+ now: Long
+ ): Boolean {
+ if (searchQuery != null &&
+ !event.title.contains(searchQuery, ignoreCase = true) &&
+ !event.desc.contains(searchQuery, ignoreCase = true)) {
+ return false
+ }
+ return filterState?.matchesEvent(event, now) ?: true
+ }
+
private const val BUNDLE_CALENDAR_IDS = "filter_calendar_ids"
private const val BUNDLE_CALENDAR_NULL = "filter_calendar_null"
private const val BUNDLE_STATUS_FILTERS = "filter_status"
@@ -232,7 +250,12 @@ data class FilterState(
if (selectedCalendarIds == null) return true // No filter = show all
return event.calendarId in selectedCalendarIds // Empty set = show none
}
-
+
+ /** Check if an event matches all active filters */
+ fun matchesEvent(event: EventAlertRecord, now: Long): Boolean {
+ return filterEvents(listOf(event), now).isNotEmpty()
+ }
+
/**
* Filter events by specified filter types.
* @param events List of events to filter
diff --git a/android/app/src/main/java/com/github/quarck/calnotify/ui/MainActivityModern.kt b/android/app/src/main/java/com/github/quarck/calnotify/ui/MainActivityModern.kt
index 9d44d0b2..7fad27bd 100644
--- a/android/app/src/main/java/com/github/quarck/calnotify/ui/MainActivityModern.kt
+++ b/android/app/src/main/java/com/github/quarck/calnotify/ui/MainActivityModern.kt
@@ -216,11 +216,14 @@ class MainActivityModern : MainActivityBase() {
// Custom quiet hours is deprecated
menu.findItem(R.id.action_custom_quiet_interval)?.isVisible = false
+ val hasFilters = filterState.hasActiveFilters() || !currentFragment?.getSearchQuery().isNullOrEmpty()
+
// Show mute all only for fragments that support it (Active events)
val muteAllMenuItem = menu.findItem(R.id.action_mute_all)
val supportsMuteAll = currentFragment?.supportsMuteAll() == true
muteAllMenuItem?.isVisible = supportsMuteAll && settings.enableNotificationMute
muteAllMenuItem?.isEnabled = currentFragment?.anyForMuteAll() == true
+ muteAllMenuItem?.title = getString(if (hasFilters) R.string.mute_all_filtered else R.string.mute_all)
// Show dismiss all only for fragments that support it (Active events)
val dismissAllMenuItem = menu.findItem(R.id.action_dismiss_all)
@@ -233,9 +236,11 @@ class MainActivityModern : MainActivityBase() {
val pinAllMenuItem = menu.findItem(R.id.action_pin_all)
pinAllMenuItem?.isVisible = supportsPinAll
pinAllMenuItem?.isEnabled = currentFragment?.anyForPinAll() == true
+ pinAllMenuItem?.title = getString(if (hasFilters) R.string.pin_all_filtered else R.string.pin_all)
val unpinAllMenuItem = menu.findItem(R.id.action_unpin_all)
unpinAllMenuItem?.isVisible = supportsPinAll
unpinAllMenuItem?.isEnabled = currentFragment?.anyForUnpinAll() == true
+ unpinAllMenuItem?.title = getString(if (hasFilters) R.string.unpin_all_filtered else R.string.unpin_all)
// Show snooze all only for fragments that support it (Active events)
val snoozeAllMenuItem = menu.findItem(R.id.action_snooze_all)
@@ -390,8 +395,9 @@ class MainActivityModern : MainActivityBase() {
}
private fun onMuteAll() {
+ val hasFilters = filterState.hasActiveFilters() || !getCurrentSearchableFragment()?.getSearchQuery().isNullOrEmpty()
AlertDialog.Builder(this)
- .setMessage(R.string.mute_all_events_question)
+ .setMessage(if (hasFilters) R.string.mute_all_filtered_events_question else R.string.mute_all_events_question)
.setCancelable(false)
.setPositiveButton(android.R.string.yes) { _, _ ->
doMuteAll()
@@ -402,14 +408,17 @@ class MainActivityModern : MainActivityBase() {
}
private fun doMuteAll() {
- ApplicationController.muteAllVisibleEvents(this)
+ val searchQuery = getCurrentSearchableFragment()?.getSearchQuery()
+ val currentFilterState = getCurrentFilterState()
+ ApplicationController.muteAllVisibleEvents(this, searchQuery, currentFilterState)
getCurrentSearchableFragment()?.onMuteAllComplete()
invalidateOptionsMenu()
}
private fun onPinAll() {
+ val hasFilters = filterState.hasActiveFilters() || !getCurrentSearchableFragment()?.getSearchQuery().isNullOrEmpty()
AlertDialog.Builder(this)
- .setMessage(R.string.pin_all_events_question)
+ .setMessage(if (hasFilters) R.string.pin_all_filtered_events_question else R.string.pin_all_events_question)
.setCancelable(false)
.setPositiveButton(android.R.string.yes) { _, _ ->
doPinAll()
@@ -420,14 +429,17 @@ class MainActivityModern : MainActivityBase() {
}
private fun doPinAll() {
- ApplicationController.pinAllVisibleEvents(this)
+ val searchQuery = getCurrentSearchableFragment()?.getSearchQuery()
+ val currentFilterState = getCurrentFilterState()
+ ApplicationController.pinAllVisibleEvents(this, searchQuery, currentFilterState)
getCurrentSearchableFragment()?.onPinAllComplete()
invalidateOptionsMenu()
}
private fun onUnpinAll() {
+ val hasFilters = filterState.hasActiveFilters() || !getCurrentSearchableFragment()?.getSearchQuery().isNullOrEmpty()
AlertDialog.Builder(this)
- .setMessage(R.string.unpin_all_events_question)
+ .setMessage(if (hasFilters) R.string.unpin_all_filtered_events_question else R.string.unpin_all_events_question)
.setCancelable(false)
.setPositiveButton(android.R.string.yes) { _, _ ->
doUnpinAll()
@@ -438,7 +450,9 @@ class MainActivityModern : MainActivityBase() {
}
private fun doUnpinAll() {
- ApplicationController.unpinAllVisibleEvents(this)
+ val searchQuery = getCurrentSearchableFragment()?.getSearchQuery()
+ val currentFilterState = getCurrentFilterState()
+ ApplicationController.unpinAllVisibleEvents(this, searchQuery, currentFilterState)
getCurrentSearchableFragment()?.onPinAllComplete()
invalidateOptionsMenu()
}
diff --git a/android/app/src/main/java/com/github/quarck/calnotify/ui/ViewEventActivityNoRecents.kt b/android/app/src/main/java/com/github/quarck/calnotify/ui/ViewEventActivityNoRecents.kt
index d7a2e784..fcee2a08 100644
--- a/android/app/src/main/java/com/github/quarck/calnotify/ui/ViewEventActivityNoRecents.kt
+++ b/android/app/src/main/java/com/github/quarck/calnotify/ui/ViewEventActivityNoRecents.kt
@@ -527,14 +527,16 @@ open class ViewEventActivityNoRecents : AppCompatActivity() {
}
private fun togglePinForEvent(pinned: Boolean) {
- getEventsStorage(this).use { db ->
- val current = db.getEvent(event.eventId, event.instanceStartTime)
- if (current != null) {
- current.isPinned = pinned
- db.updateEvent(current)
+ event.isPinned = pinned
+ background {
+ getEventsStorage(this).use { db ->
+ val current = db.getEvent(event.eventId, event.instanceStartTime)
+ if (current != null) {
+ current.isPinned = pinned
+ db.updateEvent(current)
+ }
}
}
- event.isPinned = pinned
}
private fun formatPreset(preset: Long): String {
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 623bb2b3..de71eec4 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -394,16 +394,22 @@
Un-Mute
Mute
- Mute all visible
- Mute all visible events? (Tasks excluded)
+ Mute all
+ Mute all filtered
+ Mute all events? (Tasks excluded)
+ Mute all filtered events? (Tasks excluded)
Notification is pinned
Pin
Unpin
- Pin all visible
+ Pin all
+ Pin all filtered
Unpin all
- Pin all visible events?\nPinned events are excluded from batch snooze.
+ Unpin all filtered
+ Pin all events?\nPinned events are excluded from batch snooze.
+ Pin all filtered events?\nPinned events are excluded from batch snooze.
Unpin all pinned events?
+ Unpin all filtered pinned events?
Pinned
Unpinned
diff --git a/android/app/src/test/java/com/github/quarck/calnotify/app/ApplicationControllerCoreRobolectricTest.kt b/android/app/src/test/java/com/github/quarck/calnotify/app/ApplicationControllerCoreRobolectricTest.kt
index 2e9ff30f..7e6e6735 100644
--- a/android/app/src/test/java/com/github/quarck/calnotify/app/ApplicationControllerCoreRobolectricTest.kt
+++ b/android/app/src/test/java/com/github/quarck/calnotify/app/ApplicationControllerCoreRobolectricTest.kt
@@ -84,6 +84,7 @@ class ApplicationControllerCoreRobolectricTest {
private fun createTestEvent(
eventId: Long = 1L,
+ calendarId: Long = 1L,
instanceStartTime: Long = baseTime,
snoozedUntil: Long = 0L,
isMuted: Boolean = false,
@@ -91,7 +92,7 @@ class ApplicationControllerCoreRobolectricTest {
lastStatusChangeTime: Long = baseTime
): EventAlertRecord {
val event = EventAlertRecord(
- calendarId = 1L,
+ calendarId = calendarId,
eventId = eventId,
isAllDay = false,
isRepeating = false,
@@ -462,6 +463,87 @@ class ApplicationControllerCoreRobolectricTest {
assertFalse("No events should be pinned after unpin all", events.any { it.isPinned })
}
+ // === filter-aware muteAllVisibleEvents tests ===
+
+ @Test
+ fun testMuteAllVisibleEvents_respectsSearchQuery() {
+ mockEventsStorage.addEvent(createTestEvent(eventId = 1).apply { title = "Meeting with Alice" })
+ mockEventsStorage.addEvent(createTestEvent(eventId = 2).apply { title = "Lunch with Bob" })
+
+ ApplicationController.muteAllVisibleEvents(context, searchQuery = "Alice")
+
+ val events = mockEventsStorage.events
+ assertTrue("Matching event should be muted", events.find { it.eventId == 1L }?.isMuted == true)
+ assertFalse("Non-matching event should NOT be muted", events.find { it.eventId == 2L }?.isMuted == true)
+ }
+
+ @Test
+ fun testMuteAllVisibleEvents_respectsFilterState() {
+ mockEventsStorage.addEvent(createTestEvent(eventId = 1, calendarId = 10L))
+ mockEventsStorage.addEvent(createTestEvent(eventId = 2, calendarId = 20L))
+
+ val filter = FilterState(selectedCalendarIds = setOf(10L))
+ ApplicationController.muteAllVisibleEvents(context, filterState = filter)
+
+ val events = mockEventsStorage.events
+ assertTrue("Filtered-in event should be muted", events.find { it.eventId == 1L }?.isMuted == true)
+ assertFalse("Filtered-out event should NOT be muted", events.find { it.eventId == 2L }?.isMuted == true)
+ }
+
+ // === filter-aware pinAllVisibleEvents tests ===
+
+ @Test
+ fun testPinAllVisibleEvents_respectsSearchQuery() {
+ mockEventsStorage.addEvent(createTestEvent(eventId = 1).apply { title = "Meeting with Alice" })
+ mockEventsStorage.addEvent(createTestEvent(eventId = 2).apply { title = "Lunch with Bob" })
+
+ ApplicationController.pinAllVisibleEvents(context, searchQuery = "Alice")
+
+ val events = mockEventsStorage.events
+ assertTrue("Matching event should be pinned", events.find { it.eventId == 1L }?.isPinned == true)
+ assertFalse("Non-matching event should NOT be pinned", events.find { it.eventId == 2L }?.isPinned == true)
+ }
+
+ @Test
+ fun testPinAllVisibleEvents_respectsFilterState() {
+ mockEventsStorage.addEvent(createTestEvent(eventId = 1, calendarId = 10L))
+ mockEventsStorage.addEvent(createTestEvent(eventId = 2, calendarId = 20L))
+
+ val filter = FilterState(selectedCalendarIds = setOf(10L))
+ ApplicationController.pinAllVisibleEvents(context, filterState = filter)
+
+ val events = mockEventsStorage.events
+ assertTrue("Filtered-in event should be pinned", events.find { it.eventId == 1L }?.isPinned == true)
+ assertFalse("Filtered-out event should NOT be pinned", events.find { it.eventId == 2L }?.isPinned == true)
+ }
+
+ // === filter-aware unpinAllVisibleEvents tests ===
+
+ @Test
+ fun testUnpinAllVisibleEvents_respectsSearchQuery() {
+ mockEventsStorage.addEvent(createTestEvent(eventId = 1).apply { isPinned = true; title = "Meeting with Alice" })
+ mockEventsStorage.addEvent(createTestEvent(eventId = 2).apply { isPinned = true; title = "Lunch with Bob" })
+
+ ApplicationController.unpinAllVisibleEvents(context, searchQuery = "Alice")
+
+ val events = mockEventsStorage.events
+ assertFalse("Matching event should be unpinned", events.find { it.eventId == 1L }?.isPinned == true)
+ assertTrue("Non-matching event should stay pinned", events.find { it.eventId == 2L }?.isPinned == true)
+ }
+
+ @Test
+ fun testUnpinAllVisibleEvents_respectsFilterState() {
+ mockEventsStorage.addEvent(createTestEvent(eventId = 1, calendarId = 10L).apply { isPinned = true })
+ mockEventsStorage.addEvent(createTestEvent(eventId = 2, calendarId = 20L).apply { isPinned = true })
+
+ val filter = FilterState(selectedCalendarIds = setOf(10L))
+ ApplicationController.unpinAllVisibleEvents(context, filterState = filter)
+
+ val events = mockEventsStorage.events
+ assertFalse("Filtered-in event should be unpinned", events.find { it.eventId == 1L }?.isPinned == true)
+ assertTrue("Filtered-out event should stay pinned", events.find { it.eventId == 2L }?.isPinned == true)
+ }
+
// === onEventAlarm tests ===
@Test