Skip to content

Commit a2471fd

Browse files
authored
Add count methods to storage collection classes (#312)
* Add `countContacts` method to AndroidAddressBook with tests * Add `countEvents` method to AndroidCalendar * Add `countTasks` method to DmfsTaskList * Revert accidental copyright header changes * Optimize count methods by querying only ID columns - Update `countTasks` to query only `_ID` column - Update `countContacts` to query only `_ID` column - Update `countEvents` to query only `_ID` column - Fix test to use correct UID filter in `DmfsTaskListTest`
1 parent 72ff5b3 commit a2471fd

6 files changed

Lines changed: 263 additions & 12 deletions

File tree

lib/src/androidTest/kotlin/at/bitfire/synctools/storage/calendar/AndroidCalendarTest.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,67 @@ class AndroidCalendarTest {
113113
assertEntitiesEqual(entity, result, onlyFieldsInExpected = true)
114114
}
115115

116+
@Test
117+
fun testCountEvents_empty() {
118+
// Test counting when calendar is empty
119+
val count = calendar.countEvents(null, null)
120+
assertEquals(0, count)
121+
}
122+
123+
@Test
124+
fun testCountEvents_withEvents() {
125+
// Add multiple events and test counting
126+
calendar.addEvent(Entity(contentValuesOf(
127+
Events.CALENDAR_ID to calendar.id,
128+
Events.DTSTART to now,
129+
Events.DTEND to now + 3600000,
130+
Events.TITLE to "Event 1"
131+
)))
132+
calendar.addEvent(Entity(contentValuesOf(
133+
Events.CALENDAR_ID to calendar.id,
134+
Events.DTSTART to now + 3600000,
135+
Events.DTEND to now + 3600000*2,
136+
Events.TITLE to "Event 2"
137+
)))
138+
139+
val count = calendar.countEvents(null, null)
140+
assertEquals(2, count)
141+
}
142+
143+
@Test
144+
fun testCountEvents_filterMatch() {
145+
// Test counting with WHERE clause
146+
calendar.addEvent(Entity(contentValuesOf(
147+
Events.CALENDAR_ID to calendar.id,
148+
Events.DTSTART to now,
149+
Events.DTEND to now + 3600000,
150+
Events.TITLE to "Filter Test 1"
151+
)))
152+
calendar.addEvent(Entity(contentValuesOf(
153+
Events.CALENDAR_ID to calendar.id,
154+
Events.DTSTART to now + 3600000,
155+
Events.DTEND to now + 3600000*2,
156+
Events.TITLE to "Filter Test 2"
157+
)))
158+
159+
val filteredCount = calendar.countEvents("${Events.DTSTART}=?", arrayOf(now.toString()))
160+
assertEquals(1, filteredCount)
161+
}
162+
163+
@Test
164+
fun testCountEvents_filterNoMatch() {
165+
// Test counting with filter that matches nothing
166+
calendar.addEvent(Entity(contentValuesOf(
167+
Events.CALENDAR_ID to calendar.id,
168+
Events.DTSTART to now,
169+
Events.DTEND to now + 3600000,
170+
Events.TITLE to "Test Event"
171+
)))
172+
173+
val noMatchCount = calendar.countEvents("${Events.DTSTART}=?", arrayOf((now + 86400000).toString()))
174+
assertEquals(0, noMatchCount)
175+
}
176+
116177
@Test
117178
fun testFindEvent() {
118179
// no result

lib/src/androidTest/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskListTest.kt

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import at.bitfire.ical4android.DmfsStyleProvidersTaskTest
1414
import at.bitfire.ical4android.DmfsTask
1515
import at.bitfire.ical4android.Task
1616
import at.bitfire.ical4android.TaskProvider
17+
import junit.framework.Assert.assertTrue
18+
import junit.framework.TestCase.assertEquals
1719
import net.fortuna.ical4j.model.property.RelatedTo
1820
import org.dmfs.tasks.contract.TaskContract
19-
import org.junit.Assert
21+
import org.junit.Assert.assertNotNull
2022
import org.junit.Test
2123

2224
class DmfsTaskListTest(providerName: TaskProvider.ProviderName):
@@ -34,13 +36,73 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName):
3436

3537
val dmfsTaskListProvider = DmfsTaskListProvider(testAccount, provider.client, providerName)
3638
val id = dmfsTaskListProvider.createTaskList(info)
37-
Assert.assertNotNull(id)
39+
assertNotNull(id)
3840

3941
dmfsTaskListProvider.createTaskList(info)
4042

4143
return dmfsTaskListProvider.getTaskList(id)!!
4244
}
4345

46+
@Test
47+
fun testCountTasks_empty() {
48+
val taskList = createTaskList()
49+
try {
50+
val count = taskList.countTasks(null, null)
51+
assertEquals(0, count)
52+
} finally {
53+
taskList.delete()
54+
}
55+
}
56+
57+
@Test
58+
fun testCountTasks_withFilter() {
59+
val taskList = createTaskList()
60+
try {
61+
// Add tasks with different UIDs
62+
val task1 = Task().apply {
63+
uid = "filter-uid-1"
64+
summary = "Filter Test 1"
65+
}
66+
val task2 = Task().apply {
67+
uid = "filter-uid-2"
68+
summary = "Filter Test 2"
69+
}
70+
71+
DmfsTask(taskList, task1, "sync-id-1", null, 0).add()
72+
DmfsTask(taskList, task2, "sync-id-2", null, 0).add()
73+
74+
// Test counting with UID filter
75+
val filteredCount = taskList.countTasks("${TaskContract.Tasks._UID}=?", arrayOf("filter-uid-1"))
76+
assertEquals(1, filteredCount)
77+
} finally {
78+
taskList.delete()
79+
}
80+
}
81+
82+
@Test
83+
fun testCountTasks_withoutFilter() {
84+
val taskList = createTaskList()
85+
try {
86+
// Add multiple tasks
87+
val task1 = Task().apply {
88+
uid = "task-1"
89+
summary = "Test Task 1"
90+
}
91+
val task2 = Task().apply {
92+
uid = "task-2"
93+
summary = "Test Task 2"
94+
}
95+
96+
DmfsTask(taskList, task1, "sync-id-1", null, 0).add()
97+
DmfsTask(taskList, task2, "sync-id-2", null, 0).add()
98+
99+
val count = taskList.countTasks(null, null)
100+
assertEquals(2, count)
101+
} finally {
102+
taskList.delete()
103+
}
104+
}
105+
44106
@Test
45107
fun testTouchRelations() {
46108
val taskList = createTaskList()
@@ -76,25 +138,25 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName):
76138
taskList.provider.client.query(taskList.tasksPropertiesUri(), null,
77139
"${TaskContract.Properties.TASK_ID}=?", arrayOf(childId.toString()),
78140
null, null)!!.use { cursor ->
79-
Assert.assertEquals(1, cursor.count)
141+
assertEquals(1, cursor.count)
80142
cursor.moveToNext()
81143

82144
val row = ContentValues()
83145
DatabaseUtils.cursorRowToContentValues(cursor, row)
84146

85-
Assert.assertEquals(
147+
assertEquals(
86148
TaskContract.Property.Relation.CONTENT_ITEM_TYPE,
87149
row.getAsString(TaskContract.Properties.MIMETYPE)
88150
)
89-
Assert.assertEquals(
151+
assertEquals(
90152
parentId,
91153
row.getAsLong(TaskContract.Property.Relation.RELATED_ID)
92154
)
93-
Assert.assertEquals(
155+
assertEquals(
94156
parent.uid,
95157
row.getAsString(TaskContract.Property.Relation.RELATED_UID)
96158
)
97-
Assert.assertEquals(
159+
assertEquals(
98160
TaskContract.Property.Relation.RELTYPE_PARENT,
99161
row.getAsInteger(TaskContract.Property.Relation.RELATED_TYPE)
100162
)
@@ -106,8 +168,8 @@ class DmfsTaskListTest(providerName: TaskProvider.ProviderName):
106168
// now parent_id should bet set
107169
taskList.provider.client.query(childContentUri, arrayOf(TaskContract.Tasks.PARENT_ID),
108170
null, null, null)!!.use { cursor ->
109-
Assert.assertTrue(cursor.moveToNext())
110-
Assert.assertEquals(parentId, cursor.getLong(0))
171+
assertTrue(cursor.moveToNext())
172+
assertEquals(parentId, cursor.getLong(0))
111173
}
112174
} finally {
113175
taskList.delete()

lib/src/androidTest/kotlin/at/bitfire/vcard4android/AndroidAddressBookTest.kt

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ import android.provider.ContactsContract
1414
import androidx.test.platform.app.InstrumentationRegistry
1515
import androidx.test.rule.GrantPermissionRule
1616
import at.bitfire.vcard4android.impl.TestAddressBook
17-
import org.junit.*
18-
import org.junit.Assert.*
17+
import org.junit.Assert.assertArrayEquals
18+
import org.junit.Assert.assertEquals
19+
import org.junit.Assert.assertFalse
20+
import org.junit.Assert.assertNotNull
21+
import org.junit.Assert.assertTrue
22+
import org.junit.BeforeClass
23+
import org.junit.ClassRule
24+
import org.junit.Test
1925

2026
class AndroidAddressBookTest {
2127

@@ -45,8 +51,71 @@ class AndroidAddressBookTest {
4551

4652

4753
@Test
48-
fun testSettings() {
54+
fun testCountContacts_empty() {
4955
val addressBook = TestAddressBook(testAddressBookAccount, provider)
56+
val count = addressBook.countContacts(null, null)
57+
assertEquals(0, count)
58+
}
59+
60+
@Test
61+
fun testCountContacts_withContacts() {
62+
val addressBook = TestAddressBook(testAddressBookAccount, provider)
63+
64+
// Create some test contacts
65+
val contact1 = AndroidContact(addressBook, Contact().apply {
66+
displayName = "Test Contact 1"
67+
}, null, null)
68+
contact1.add()
69+
70+
val contact2 = AndroidContact(addressBook, Contact().apply {
71+
displayName = "Test Contact 2"
72+
}, null, null)
73+
contact2.add()
74+
75+
try {
76+
val count = addressBook.countContacts(null, null)
77+
assertEquals(2, count)
78+
} finally {
79+
contact1.delete()
80+
contact2.delete()
81+
}
82+
}
83+
84+
@Test
85+
fun testCountContacts_withFilter() {
86+
val addressBook = TestAddressBook(testAddressBookAccount, provider)
87+
88+
// Create test contacts with different UIDs
89+
val contact1 = AndroidContact(addressBook, Contact().apply {
90+
displayName = "Filter Test 1"
91+
uid = "test-uid-1"
92+
}, null, null)
93+
contact1.add()
94+
95+
val contact2 = AndroidContact(addressBook, Contact().apply {
96+
displayName = "Filter Test 2"
97+
uid = "test-uid-2"
98+
}, null, null)
99+
contact2.add()
100+
101+
try {
102+
// Test counting with filter
103+
val filteredCount = addressBook.countContacts("${AndroidContact.COLUMN_UID}=?", arrayOf("test-uid-1"))
104+
assertEquals(1, filteredCount)
105+
106+
// Test counting with non-matching filter
107+
val noMatchCount = addressBook.countContacts("${AndroidContact.COLUMN_UID}=?", arrayOf("non-existent"))
108+
assertEquals(0, noMatchCount)
109+
} finally {
110+
contact1.delete()
111+
contact2.delete()
112+
}
113+
}
114+
115+
116+
@Test
117+
fun testSettings() {
118+
val addressBook = TestAddressBook(testAddressBookAccount, provider)
50119

51120
var values = ContentValues()
52121
values.put(ContactsContract.Settings.SHOULD_SYNC, false)

lib/src/main/kotlin/at/bitfire/synctools/storage/calendar/AndroidCalendar.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,28 @@ class AndroidCalendar(
113113
.withValueBackReference(EventsContract.DATA_ROW_EVENT_ID, eventRowIdx)
114114
}
115115

116+
/**
117+
* Counts the number of events in this calendar that match the given selection criteria.
118+
*
119+
* @param where An optional filter declaring which rows to return.
120+
* @param whereArgs Optional arguments for [where].
121+
* @return The number of events matching the selection criteria.
122+
* @throws LocalStorageException when the content provider returns an error
123+
*/
124+
fun countEvents(where: String?, whereArgs: Array<String>?): Int {
125+
try {
126+
val (protectedWhere, protectedWhereArgs) = whereWithCalendarId(where, whereArgs)
127+
client.query(eventsUri, arrayOf(Events._ID),
128+
protectedWhere, protectedWhereArgs, null)?.use { cursor ->
129+
return cursor.count
130+
}
131+
} catch (e: RemoteException) {
132+
throw LocalStorageException("Couldn't count events", e)
133+
}
134+
// If the query was invalid, an exception should have been thrown. So this should never be reached:
135+
return 0
136+
}
137+
116138
/**
117139
* Gets the first event from this calendar that matches the given query.
118140
*

lib/src/main/kotlin/at/bitfire/synctools/storage/tasks/DmfsTaskList.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,28 @@ class DmfsTaskList(
5252

5353
// CRUD DmfsTask
5454

55+
/**
56+
* Counts the number of tasks in this task list that match the given selection criteria.
57+
*
58+
* @param where An optional filter declaring which rows to return.
59+
* @param whereArgs Optional arguments for [where].
60+
* @return The number of tasks matching the selection criteria.
61+
* @throws LocalStorageException when the content provider returns an error
62+
*/
63+
fun countTasks(where: String? = null, whereArgs: Array<String>? = null): Int {
64+
try {
65+
val (protectedWhere, protectedWhereArgs) = whereWithTaskListId(where, whereArgs)
66+
client.query(tasksUri(), arrayOf(TaskContract.Tasks._ID),
67+
protectedWhere, protectedWhereArgs, null)?.use { cursor ->
68+
return cursor.count
69+
}
70+
} catch (e: RemoteException) {
71+
throw LocalStorageException("Couldn't count ${providerName.authority} tasks", e)
72+
}
73+
// If the query was invalid, an exception should have been thrown. So this should never be reached:
74+
return 0
75+
}
76+
5577
/**
5678
* Queries tasks from this task list. Adds a WHERE clause that restricts the
5779
* query to [TaskContract.TaskColumns.LIST_ID] = [id].

lib/src/main/kotlin/at/bitfire/vcard4android/AndroidAddressBook.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ open class AndroidAddressBook<T1: AndroidContact, T2: AndroidGroup>(
5656
get() = ContactsContract.SyncState.get(provider, addressBookAccount)
5757
set(data) = ContactsContract.SyncState.set(provider, addressBookAccount, data)
5858

59+
/**
60+
* Counts the number of contacts in the address book that match the given selection criteria.
61+
*
62+
* @param where An optional filter declaring which rows to return.
63+
* @param whereArgs Optional arguments for [where].
64+
* @return The number of contacts matching the selection criteria.
65+
*/
66+
fun countContacts(where: String?, whereArgs: Array<String>?): Int {
67+
provider!!.query(rawContactsSyncUri(), arrayOf(RawContacts._ID),
68+
where, whereArgs, null)?.use { cursor ->
69+
return cursor.count
70+
}
71+
// If the query was invalid, an exception should have been thrown. So this should never be reached:
72+
return 0
73+
}
5974

6075
fun queryContacts(where: String?, whereArgs: Array<String>?): List<T1> {
6176
val contacts = LinkedList<T1>()

0 commit comments

Comments
 (0)