Skip to content

Commit 389977e

Browse files
committed
feat: do the plan
1 parent b6b9a0c commit 389977e

9 files changed

Lines changed: 145 additions & 15 deletions

File tree

android/app/src/main/java/com/github/quarck/calnotify/ui/ActiveEventsFragment.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class ActiveEventsFragment : Fragment(), EventListCallback, SearchableFragment,
6767
private lateinit var refreshLayout: SwipeRefreshLayout
6868
private lateinit var emptyView: TextView
6969
private lateinit var adapter: EventListAdapter
70+
private var totalEventCount: Int = 0
7071
private var newUIBanner: LinearLayout? = null
7172

7273
// Selection mode UI elements
@@ -253,11 +254,13 @@ class ActiveEventsFragment : Fragment(), EventListCallback, SearchableFragment,
253254
val now = clock.currentTimeMillis()
254255

255256
background {
256-
val events = getEventsStorage(ctx).use { db ->
257-
filterState.filterEvents(db.eventsForDisplay, now)
257+
val (total, events) = getEventsStorage(ctx).use { db ->
258+
val allDbEvents = db.eventsForDisplay
259+
Pair(allDbEvents.size, filterState.filterEvents(allDbEvents, now))
258260
}
259261

260262
activity?.runOnUiThread {
263+
totalEventCount = total
261264
adapter.setEventsToDisplay(events)
262265
updateEmptyState()
263266
refreshLayout.isRefreshing = false
@@ -362,6 +365,8 @@ class ActiveEventsFragment : Fragment(), EventListCallback, SearchableFragment,
362365

363366
override fun getEventCount(): Int = adapter.getAllItemCount()
364367

368+
override fun getTotalEventCount(): Int = totalEventCount
369+
365370
override fun getDisplayedEventCount(): Int = adapter.itemCount
366371

367372
override fun hasActiveEvents(): Boolean = adapter.hasActiveEvents

android/app/src/main/java/com/github/quarck/calnotify/ui/DismissedEventsFragment.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class DismissedEventsFragment : Fragment(), DismissedEventListCallback, Searchab
6363
private lateinit var refreshLayout: SwipeRefreshLayout
6464
private lateinit var emptyView: TextView
6565
private lateinit var adapter: DismissedEventListAdapter
66+
private var totalEventCount: Int = 0
6667

6768
private val dataUpdatedReceiver = object : BroadcastReceiver() {
6869
override fun onReceive(context: Context?, intent: Intent?) {
@@ -166,11 +167,13 @@ class DismissedEventsFragment : Fragment(), DismissedEventListCallback, Searchab
166167
val now = clock.currentTimeMillis()
167168

168169
background {
169-
val events = getDismissedEventsStorage(ctx).use { db ->
170-
filterState.filterDismissedEvents(db.eventsForDisplay, now)
170+
val (total, events) = getDismissedEventsStorage(ctx).use { db ->
171+
val allDbEvents = db.eventsForDisplay
172+
Pair(allDbEvents.size, filterState.filterDismissedEvents(allDbEvents, now))
171173
}
172174

173175
activity?.runOnUiThread {
176+
totalEventCount = total
174177
adapter.setEventsToDisplay(events)
175178
updateEmptyState()
176179
refreshLayout.isRefreshing = false
@@ -259,6 +262,8 @@ class DismissedEventsFragment : Fragment(), DismissedEventListCallback, Searchab
259262

260263
override fun getEventCount(): Int = adapter.getAllItemCount()
261264

265+
override fun getTotalEventCount(): Int = totalEventCount
266+
262267
override fun onFilterChanged() {
263268
loadEvents()
264269
}

android/app/src/main/java/com/github/quarck/calnotify/ui/MainActivityModern.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package com.github.quarck.calnotify.ui
2222

2323
import android.app.AlertDialog
2424
import android.app.SearchManager
25+
import androidx.annotation.VisibleForTesting
2526
import android.content.Context
2627
import android.content.Intent
2728
import android.os.Bundle
@@ -111,6 +112,11 @@ class MainActivityModern : MainActivityBase() {
111112
/** Get current filter state for fragments to use */
112113
fun getCurrentFilterState(): FilterState = filterState
113114

115+
@VisibleForTesting
116+
fun setFilterStateForTesting(state: FilterState) {
117+
filterState = state
118+
}
119+
114120
private fun setupUI() {
115121
setContentView(R.layout.activity_main)
116122
val toolbar = find<Toolbar?>(R.id.toolbar)
@@ -254,13 +260,18 @@ class MainActivityModern : MainActivityBase() {
254260
val manager = getSystemService(Context.SEARCH_SERVICE) as SearchManager
255261

256262
val count = currentFragment?.getEventCount() ?: 0
263+
val totalCount = currentFragment?.getTotalEventCount() ?: count
257264
val tabName = when (navController?.currentDestination?.id) {
258265
R.id.activeEventsFragment -> getString(R.string.nav_active)
259266
R.id.upcomingEventsFragment -> getString(R.string.nav_upcoming)
260267
R.id.dismissedEventsFragment -> getString(R.string.nav_dismissed)
261268
else -> ""
262269
}
263-
searchView?.queryHint = resources.getQuantityString(R.plurals.search_placeholder, count, count, tabName)
270+
searchView?.queryHint = if (filterState.hasActiveFilters() && count != totalCount) {
271+
resources.getQuantityString(R.plurals.search_placeholder_filtered, count, count, totalCount, tabName)
272+
} else {
273+
resources.getQuantityString(R.plurals.search_placeholder, count, count, tabName)
274+
}
264275
searchView?.setSearchableInfo(manager.getSearchableInfo(componentName))
265276

266277
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {

android/app/src/main/java/com/github/quarck/calnotify/ui/SearchableFragment.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ interface SearchableFragment {
3232
/** Get the total number of events (for search placeholder) */
3333
fun getEventCount(): Int
3434

35+
/** Get total event count before filter pills (for "X of Y" hint) */
36+
fun getTotalEventCount(): Int = getEventCount()
37+
3538
/** Get the count of currently displayed events (after filtering) */
3639
fun getDisplayedEventCount(): Int = getEventCount()
3740

android/app/src/main/java/com/github/quarck/calnotify/ui/UpcomingEventsFragment.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class UpcomingEventsFragment : Fragment(), EventListCallback, SearchableFragment
6868
private lateinit var refreshLayout: SwipeRefreshLayout
6969
private lateinit var emptyView: TextView
7070
private lateinit var adapter: EventListAdapter
71+
private var totalEventCount: Int = 0
7172

7273
private val dataUpdatedReceiver = object : BroadcastReceiver() {
7374
override fun onReceive(context: Context?, intent: Intent?) {
@@ -131,22 +132,24 @@ class UpcomingEventsFragment : Fragment(), EventListCallback, SearchableFragment
131132
val filterState = getFilterState()
132133

133134
background {
134-
val events = getMonitorStorage(ctx).use { storage ->
135+
val (total, events) = getMonitorStorage(ctx).use { storage ->
135136
val provider = UpcomingEventsProvider(
136137
context = ctx,
137138
settings = settings,
138139
clock = clock,
139140
monitorStorage = storage,
140141
calendarProvider = getCalendarProvider()
141142
)
142-
filterState.filterEvents(
143-
provider.getUpcomingEvents(),
143+
val allUpcoming = provider.getUpcomingEvents()
144+
Pair(allUpcoming.size, filterState.filterEvents(
145+
allUpcoming,
144146
clock.currentTimeMillis(),
145147
apply = setOf(FilterType.CALENDAR, FilterType.STATUS)
146-
)
148+
))
147149
}
148150

149151
activity?.runOnUiThread {
152+
totalEventCount = total
150153
adapter.setEventsToDisplay(events)
151154
updateEmptyState()
152155
refreshLayout.isRefreshing = false
@@ -273,6 +276,8 @@ class UpcomingEventsFragment : Fragment(), EventListCallback, SearchableFragment
273276

274277
override fun getEventCount(): Int = adapter.getAllItemCount()
275278

279+
override fun getTotalEventCount(): Int = totalEventCount
280+
276281
override fun onFilterChanged() {
277282
loadEvents()
278283
}

android/app/src/main/res/values-fr/strings.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@
223223
<string name="snooze_on_swipe">"Glisser pour répéter"</string>
224224
<string name="snooze_on_swipe_summary">"Le glissement de la notificaton 'répètera' l'évenement au lieu de le rejeter (le 1er paramètre sera utilisé)"</string>
225225

226+
<plurals name="search_placeholder_filtered">
227+
<item quantity="one">Rechercher %1$d sur %2$d événement %3$s…</item>
228+
<item quantity="other">Rechercher %1$d sur %2$d événements %3$s…</item>
229+
</plurals>
230+
226231
</resources>
227232

228233

android/app/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@
9090
<item quantity="one">Search %1$d %2$s event...</item>
9191
<item quantity="other">Search %1$d %2$s events...</item>
9292
</plurals>
93+
<plurals name="search_placeholder_filtered">
94+
<item quantity="one">Search %1$d of %2$d %3$s event…</item>
95+
<item quantity="other">Search %1$d of %2$d %3$s events…</item>
96+
</plurals>
9397

9498
<string name="snooze_all_confirmation">Snooze ALL events?\nAlready snoozed would also change snooze time unless snoozed to longer period</string>
9599
<string name="change_all_notification">This will change snooze time for all events\nContinue?</string>

android/app/src/test/java/com/github/quarck/calnotify/ui/MainActivityModernRobolectricTest.kt

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,4 +845,90 @@ class MainActivityModernRobolectricTest {
845845

846846
scenario.close()
847847
}
848+
849+
// === Search Hint "X of Y" Tests ===
850+
851+
@Test
852+
fun search_hint_shows_x_of_y_when_filters_active() {
853+
fixture.createEvent(title = "Active Event 1")
854+
fixture.createEvent(title = "Active Event 2")
855+
fixture.createEvent(title = "Snoozed Event", snoozedUntil = Long.MAX_VALUE)
856+
857+
val scenario = fixture.launchMainActivityModern()
858+
shadowOf(Looper.getMainLooper()).idle()
859+
860+
scenario.onActivity { activity ->
861+
activity.setFilterStateForTesting(FilterState(statusFilters = setOf(StatusOption.ACTIVE)))
862+
863+
val navHostFragment = activity.supportFragmentManager
864+
.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
865+
val fragment = navHostFragment?.childFragmentManager?.primaryNavigationFragment as? SearchableFragment
866+
fragment?.onFilterChanged()
867+
shadowOf(Looper.getMainLooper()).idle()
868+
869+
activity.invalidateOptionsMenu()
870+
shadowOf(Looper.getMainLooper()).idle()
871+
872+
val hint = activity.searchView!!.queryHint?.toString() ?: ""
873+
assertTrue("Hint should contain filtered count '2'", hint.contains("2"))
874+
assertTrue("Hint should contain total count '3'", hint.contains("3"))
875+
assertTrue("Hint should contain 'of'", hint.contains("of"))
876+
}
877+
878+
scenario.close()
879+
}
880+
881+
@Test
882+
fun search_hint_shows_standard_format_when_filters_match_all() {
883+
fixture.createEvent(title = "Snoozed 1", snoozedUntil = Long.MAX_VALUE)
884+
fixture.createEvent(title = "Snoozed 2", snoozedUntil = Long.MAX_VALUE)
885+
886+
val scenario = fixture.launchMainActivityModern()
887+
shadowOf(Looper.getMainLooper()).idle()
888+
889+
scenario.onActivity { activity ->
890+
// Filter for SNOOZED, but all events are snoozed → count == totalCount
891+
activity.setFilterStateForTesting(FilterState(statusFilters = setOf(StatusOption.SNOOZED)))
892+
893+
val navHostFragment = activity.supportFragmentManager
894+
.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
895+
val fragment = navHostFragment?.childFragmentManager?.primaryNavigationFragment as? SearchableFragment
896+
fragment?.onFilterChanged()
897+
shadowOf(Looper.getMainLooper()).idle()
898+
899+
activity.invalidateOptionsMenu()
900+
shadowOf(Looper.getMainLooper()).idle()
901+
902+
val hint = activity.searchView!!.queryHint?.toString() ?: ""
903+
assertTrue("Hint should contain count '2'", hint.contains("2"))
904+
assertFalse("Hint should NOT contain 'of' when all match", hint.contains("of"))
905+
}
906+
907+
scenario.close()
908+
}
909+
910+
@Test
911+
fun getTotalEventCount_returns_unfiltered_count() {
912+
fixture.createEvent(title = "Active Event")
913+
fixture.createEvent(title = "Snoozed Event", snoozedUntil = Long.MAX_VALUE)
914+
fixture.createEvent(title = "Muted Event", isMuted = true)
915+
916+
val scenario = fixture.launchMainActivityModern()
917+
shadowOf(Looper.getMainLooper()).idle()
918+
919+
scenario.onActivity { activity ->
920+
activity.setFilterStateForTesting(FilterState(statusFilters = setOf(StatusOption.SNOOZED)))
921+
922+
val navHostFragment = activity.supportFragmentManager
923+
.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
924+
val fragment = navHostFragment?.childFragmentManager?.primaryNavigationFragment as? SearchableFragment
925+
fragment?.onFilterChanged()
926+
shadowOf(Looper.getMainLooper()).idle()
927+
928+
assertEquals("Filtered count should be 1 (only snoozed)", 1, fragment!!.getEventCount())
929+
assertEquals("Total count should be 3 (all events)", 3, fragment.getTotalEventCount())
930+
}
931+
932+
scenario.close()
933+
}
848934
}

docs/dev_todo/search_bar_x_of_y_count.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,18 @@ Note: `%2$d` is skipped intentionally — `getQuantityString(id, quantity, arg1,
135135

136136
## Testing
137137

138-
### Unit/Instrumentation Tests
138+
### Robolectric Tests (`MainActivityModernRobolectricTest.kt`)
139139

140-
- Search hint shows standard format when no filters active
141-
- Search hint shows "X of Y" format when filter pills reduce count
142-
- Search hint shows standard format when filters are active but match all events (count == totalCount)
143-
- `getTotalEventCount()` returns correct unfiltered count
144-
- `getEventCount()` returns correct filtered count (existing behavior, regression check)
140+
All search hint tests are Robolectric-only. No instrumentation tests assert on search hint text.
141+
142+
**Existing tests (should still pass unmodified — no filter pills active):**
143+
- `search_hint_shows_correct_event_count()` — 3 events, no filters, checks hint contains "3" and "Active"
144+
- `displayed_event_count_updates_during_search()` — checks `getEventCount()` / `getDisplayedEventCount()` during text search
145+
146+
**New tests to add:**
147+
- `search_hint_shows_x_of_y_when_filters_active()` — Set up filter pills that reduce count, verify hint contains "X of Y" format
148+
- `search_hint_shows_standard_format_when_filters_match_all()` — Filter pills active but match everything (count == totalCount), verify standard format
149+
- `getTotalEventCount_returns_unfiltered_count()` — Verify `getTotalEventCount()` returns DB total while `getEventCount()` returns filtered count
145150

146151
## Files Changed Summary
147152

@@ -154,3 +159,4 @@ Note: `%2$d` is skipped intentionally — `getQuantityString(id, quantity, arg1,
154159
| `MainActivityModern.kt` | Conditional hint text in `onCreateOptionsMenu()` |
155160
| `values/strings.xml` | Add `search_placeholder_filtered` plural |
156161
| `values-fr/strings.xml` | Add French translation |
162+
| `MainActivityModernRobolectricTest.kt` | New Robolectric tests for "X of Y" hint |

0 commit comments

Comments
 (0)