Commit 18585c9d authored by Ricki Hirner's avatar Ricki Hirner 🐑

Tasks: support CATEGORIES

parent a5984857
Pipeline #81811485 failed with stages
in 4 minutes and 7 seconds
......@@ -9,10 +9,14 @@
package at.bitfire.ical4android
import android.content.ContentValues
import android.database.MatrixCursor
import androidx.test.filters.SmallTest
import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues
import at.bitfire.ical4android.MiscUtils.TextListHelper.toList
import net.fortuna.ical4j.data.CalendarBuilder
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.TextList
import net.fortuna.ical4j.model.TimeZone
import net.fortuna.ical4j.model.component.VTimeZone
import net.fortuna.ical4j.model.property.DtStart
......@@ -98,6 +102,26 @@ class MiscUtilsTest {
}
@Test
@SmallTest
fun testCursorToValues() {
val columns = arrayOf("col1", "col2")
val c = MatrixCursor(columns)
c.addRow(arrayOf("row1_val1", "row1_val2"))
c.moveToFirst()
val values = c.toValues()
assertEquals("row1_val1", values.getAsString("col1"))
assertEquals("row1_val2", values.getAsString("col2"))
}
@Test
@SmallTest
fun testTextListToList() {
assertEquals(listOf("str1", "str2"), TextList(arrayOf("str1", "str2")).toList())
assertEquals(emptyList<String>(), TextList(arrayOf()).toList())
}
@Suppress("unused")
private class TestClass {
private val s = "test"
......
......@@ -12,10 +12,10 @@ import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.database.DatabaseUtils
import android.net.Uri
import android.provider.CalendarContract
import android.provider.CalendarContract.*
import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues
import java.io.FileNotFoundException
import java.util.*
import java.util.logging.Level
......@@ -177,11 +177,8 @@ abstract class AndroidCalendar<out T: AndroidEvent>(
val events = LinkedList<T>()
provider.query(eventsSyncURI(), null, where, whereArgs, null)?.use { cursor ->
while (cursor.moveToNext()) {
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
events += eventFactory.fromProvider(this, values)
}
while (cursor.moveToNext())
events += eventFactory.fromProvider(this, cursor.toValues())
}
return events
}
......
......@@ -13,12 +13,12 @@ import android.content.ContentProviderOperation.Builder
import android.content.ContentUris
import android.content.ContentValues
import android.content.EntityIterator
import android.database.DatabaseUtils
import android.net.Uri
import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.CalendarContract.*
import android.util.Base64
import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues
import net.fortuna.ical4j.model.*
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.TimeZone
......@@ -361,8 +361,7 @@ abstract class AndroidEvent(
null,
Events.ORIGINAL_ID + "=?", arrayOf(id.toString()), null)?.use { c ->
while (c.moveToNext()) {
val values = ContentValues(c.columnCount)
DatabaseUtils.cursorRowToContentValues(c, values)
val values = c.toValues()
try {
val exception = calendar.eventFactory.fromProvider(calendar, values)
......
......@@ -12,14 +12,16 @@ import android.content.ContentProviderOperation
import android.content.ContentProviderOperation.Builder
import android.content.ContentUris
import android.content.ContentValues
import android.database.DatabaseUtils
import android.net.Uri
import android.os.RemoteException
import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.Dur
import net.fortuna.ical4j.model.property.*
import org.dmfs.tasks.contract.TaskContract.Tasks
import org.dmfs.tasks.contract.TaskContract.*
import org.dmfs.tasks.contract.TaskContract.Properties
import org.dmfs.tasks.contract.TaskContract.Property.Category
import java.io.FileNotFoundException
import java.net.URI
import java.net.URISyntaxException
......@@ -66,11 +68,25 @@ abstract class AndroidTask(
val id = requireNotNull(id)
task = Task()
taskList.provider.client.query(taskSyncURI(), null, null, null, null)?.use { cursor ->
val client = taskList.provider.client
client.query(taskSyncURI(), null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
val values = cursor.toValues()
Constants.log.log(Level.FINER, "Found task", values)
populateTask(values)
if (values.getAsInteger(Tasks.HAS_PROPERTIES) != 0)
// fetch properties
client.query(taskList.tasksPropertiesSyncUri(), null,
"${Properties.TASK_ID}=?", arrayOf(id.toString()),
null)?.use { propCursor ->
while (propCursor.moveToNext()) {
val propValues = propCursor.toValues()
Constants.log.log(Level.FINER, "Found property", propValues)
populateProperty(propValues)
}
}
return task
}
}
......@@ -161,6 +177,17 @@ abstract class AndroidTask(
values.getAsString(Tasks.RRULE)?.let { task.rRule = RRule(it) }
}
protected open fun populateProperty(values: ContentValues) {
val task = requireNotNull(task)
val type = values.getAsString(Properties.MIMETYPE)
when (type) {
Category.CONTENT_ITEM_TYPE ->
task.categories += values.getAsString(Category.CATEGORY_NAME)
else ->
Constants.log.warning("Found unknown property of type $type")
}
}
fun add(): Uri {
val batch = BatchOperation(taskList.provider.client)
......@@ -169,8 +196,13 @@ abstract class AndroidTask(
batch.enqueue(BatchOperation.Operation(builder))
batch.commit()
// TODO use backref mechanism so that only one commit is required for the whole task
val result = batch.getResult(0) ?: throw CalendarStorageException("Empty result from provider when adding a task")
id = ContentUris.parseId(result.uri)
insertProperties(batch)
batch.commit()
return result.uri
}
......@@ -182,10 +214,30 @@ abstract class AndroidTask(
val builder = ContentProviderOperation.newUpdate(uri)
buildTask(builder, true)
batch.enqueue(BatchOperation.Operation(builder))
val deleteProperties = ContentProviderOperation.newDelete(taskList.tasksPropertiesSyncUri())
.withSelection("${Properties.TASK_ID}=?", arrayOf(id.toString()))
batch.enqueue(BatchOperation.Operation(deleteProperties))
insertProperties(batch)
batch.commit()
return uri
}
private fun insertProperties(batch: BatchOperation) {
val task = requireNotNull(task)
// insert categories
for (category in task.categories) {
val builder = ContentProviderOperation.newInsert(taskList.tasksPropertiesSyncUri())
builder .withValue(Category.TASK_ID, id)
.withValue(Category.MIMETYPE, Category.CONTENT_ITEM_TYPE)
.withValue(Category.CATEGORY_NAME, category)
Constants.log.log(Level.FINE, "Inserting category", builder.build())
batch.enqueue(BatchOperation.Operation(builder))
}
}
fun delete(): Int {
try {
return taskList.provider.client.delete(taskSyncURI(), null, null)
......@@ -268,7 +320,6 @@ abstract class AndroidTask(
.withValue(Tasks.RRULE, task.rRule?.value)
builder .withValue(Tasks.EXDATE, if (task.exDates.isEmpty()) null else DateUtils.recurrenceSetsToAndroidString(task.exDates, allDay))
Constants.log.log(Level.FINE, "Built task object", builder.build())
}
......@@ -286,7 +337,10 @@ abstract class AndroidTask(
protected fun taskSyncURI(): Uri {
val id = requireNotNull(id)
return ContentUris.withAppendedId(taskList.tasksSyncUri(), id)
val builder = taskList.tasksSyncUri().buildUpon()
return ContentUris.appendId(builder, id)
.appendQueryParameter(LOAD_PROPERTIES, "1")
.build()
}
......
......@@ -12,14 +12,15 @@ import android.accounts.Account
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.database.DatabaseUtils
import android.net.Uri
import at.bitfire.ical4android.MiscUtils.CursorHelper.toValues
import org.dmfs.tasks.contract.TaskContract
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
import java.util.*
/**
* Represents a locally stored task list, containing AndroidTasks (whose data objects are Tasks).
* Communicates with third-party content providers to store the tasks.
......@@ -67,9 +68,7 @@ abstract class AndroidTaskList<out T: AndroidTask>(
provider.client.query(TaskProvider.syncAdapterUri(ContentUris.withAppendedId(provider.taskListsUri(), id), account), null, null, null, null)?.use { cursor ->
if (cursor.moveToNext()) {
val taskList = factory.newInstance(account, provider, id)
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
taskList.populate(values)
taskList.populate(cursor.toValues())
return taskList
}
}
......@@ -80,8 +79,7 @@ abstract class AndroidTaskList<out T: AndroidTask>(
val taskLists = LinkedList<T>()
provider.client.query(TaskProvider.syncAdapterUri(provider.taskListsUri(), account), null, where, whereArgs, null)?.use { cursor ->
while (cursor.moveToNext()) {
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
val values = cursor.toValues()
val taskList = factory.newInstance(account, provider, values.getAsLong(TaskLists._ID))
taskList.populate(values)
taskLists += taskList
......@@ -127,11 +125,8 @@ abstract class AndroidTaskList<out T: AndroidTask>(
tasksSyncUri(),
null,
where, whereArgs, null)?.use { cursor ->
while (cursor.moveToNext()) {
val values = ContentValues(cursor.columnCount)
DatabaseUtils.cursorRowToContentValues(cursor, values)
tasks += taskFactory.fromProvider(this, values)
}
while (cursor.moveToNext())
tasks += taskFactory.fromProvider(this, cursor.toValues())
}
return tasks
}
......@@ -142,5 +137,6 @@ abstract class AndroidTaskList<out T: AndroidTask>(
fun taskListSyncUri() = TaskProvider.syncAdapterUri(ContentUris.withAppendedId(provider.taskListsUri(), id), account)
fun tasksSyncUri() = TaskProvider.syncAdapterUri(provider.tasksUri(), account)
fun tasksPropertiesSyncUri() = TaskProvider.syncAdapterUri(provider.propertiesUri(), account)
}
......@@ -9,10 +9,9 @@
package at.bitfire.ical4android
import net.fortuna.ical4j.data.CalendarBuilder
import net.fortuna.ical4j.data.CalendarParserFactory
import net.fortuna.ical4j.data.ParserException
import net.fortuna.ical4j.model.*
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.component.*
import net.fortuna.ical4j.model.property.DateProperty
import net.fortuna.ical4j.model.property.ProdId
......
......@@ -9,6 +9,9 @@
package at.bitfire.ical4android
import android.content.ContentValues
import android.database.Cursor
import android.database.DatabaseUtils
import net.fortuna.ical4j.model.TextList
import net.fortuna.ical4j.model.property.DateProperty
import net.fortuna.ical4j.util.TimeZones
import java.lang.reflect.Modifier
......@@ -93,4 +96,32 @@ object MiscUtils {
}
}
object CursorHelper {
/**
* Returns the entire contents of the current row as a [ContentValues] object.
* @return entire contents of the current row
*/
fun Cursor.toValues(): ContentValues {
val values = ContentValues(columnCount)
DatabaseUtils.cursorRowToContentValues(this, values)
return values
}
}
object TextListHelper {
fun TextList.toList(): List<String> {
val list = LinkedList<String>()
val it = iterator()
while (it.hasNext())
list += it.next()
return list
}
}
}
\ No newline at end of file
......@@ -8,6 +8,7 @@
package at.bitfire.ical4android
import at.bitfire.ical4android.MiscUtils.TextListHelper.toList
import net.fortuna.ical4j.data.CalendarBuilder
import net.fortuna.ical4j.data.CalendarOutputter
import net.fortuna.ical4j.data.ParserException
......@@ -50,6 +51,7 @@ class Task: ICalendar() {
val rDates = LinkedList<RDate>()
val exDates = LinkedList<ExDate>()
val categories = LinkedList<String>()
val unknownProperties = LinkedList<Property>()
companion object {
......@@ -112,6 +114,7 @@ class Task: ICalendar() {
is RRule -> t.rRule = prop
is RDate -> t.rDates += prop
is ExDate -> t.exDates += prop
is Categories -> t.categories.addAll(prop.categories.toList())
is ProdId, is DtStamp -> { /* don't save these as unknown properties */ }
else -> t.unknownProperties += prop
}
......@@ -186,6 +189,9 @@ class Task: ICalendar() {
}
percentComplete?.let { props += PercentComplete(it) }
if (categories.isNotEmpty())
props += Categories(TextList(categories.toTypedArray()))
props.addAll(unknownProperties)
// add VTIMEZONE components
......
......@@ -99,10 +99,13 @@ class TaskProvider private constructor(
fun taskListsUri() = TaskContract.TaskLists.getContentUri(name.authority)!!
fun tasksUri() = TaskContract.Tasks.getContentUri(name.authority)!!
//fun alarmsUri() = TaskContract.Alarms.getContentUri(name.authority)!!
fun syncStateUri() = TaskContract.SyncState.getContentUri(name.authority)!!
fun tasksUri() = TaskContract.Tasks.getContentUri(name.authority)!!
fun propertiesUri() = TaskContract.Properties.getContentUri(name.authority)!!
fun alarmsUri() = TaskContract.Alarms.getContentUri(name.authority)!!
fun categoriesUri() = TaskContract.Categories.getContentUri(name.authority)!!
override fun close() {
if (Build.VERSION.SDK_INT >= 24)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment