diff --git a/opentasks-contract/src/main/java/org/dmfs/tasks/contract/TaskContract.java b/opentasks-contract/src/main/java/org/dmfs/tasks/contract/TaskContract.java index c773d0565..d8363ddbf 100644 --- a/opentasks-contract/src/main/java/org/dmfs/tasks/contract/TaskContract.java +++ b/opentasks-contract/src/main/java/org/dmfs/tasks/contract/TaskContract.java @@ -1036,8 +1036,7 @@ public interface InstanceColumns String INSTANCE_DURATION = "instance_duration"; /** - * The start of the original instance as specified in the master task. For non-recurring task instances this equals the value of {@link - * #INSTANCE_START}, except that `null` values are represented as `0`. + * The start of the original instance as specified in the master task. For non-recurring task instances this is {@code null}. *
* For recurring tasks, these are the timestamps which have been derived from the recurrence rule or dates, except those specified as exdates. */ @@ -1056,16 +1055,61 @@ public interface InstanceColumns /** - * Instances of a task. At present this table is read only. Currently it contains exactly one entry per task (and task exception), so it's merely a copy of - * {@link Tasks}. + * A table containing one entry per task instance. This table is writable in order to allow modification of single instances of a task. Write operations to + * this table will be converted into operations on overrides and forwarded to the task table. *
- * TODO: Insert all instances of recurring the tasks. - *
+ * Note: The {@link #DTSTART}, {@link #DUE} values of instances of recurring tasks represent the actual instance values, i.e. they are different for each + * instance ({@link #DURATION} is always {@code null}). *- * TODO: In later releases it's planned to provide a convenient interface to add, change or delete task instances via this URI. - *
+ * Also, none of the instances are recurring themselves, so {@link #RRULE}, {@link #RDATE} and {@link #EXDATE} are always {@code null}. + *+ * TODO: Insert all instances of recurring tasks. + *
+ * The following operations are supported: + *
+ *
+ * Note, the data of an insert must not contain the fields {@link #RRULE}, {@link #RDATE} or {@link #EXDATE}. If the new instance belongs to an existing + * task the data must contain the fields {@link #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME}. Also note, this table supports writing {@link + * #DURATION} (if the instance has a {@link #DTSTART}), but reading it back will always return a {@code null} {@link #DURATION} and a non-{@code null} + * {@link #DUE} date. Reading the task in the tasks table will, however, return the original {@link #DURATION}. + *
+ * If there already is an instance (with or without override) for the given {@link #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME} an exception + * is thrown. + *
+ *
ORIGINAL_INSTANCE_ID value | Result |
---|---|
absent or empty | A new non-recurring task is created with the given + * values. |
a valid {@link Tasks} row {@code _ID} | An {@link #RDATE} for the given {@link #ORIGINAL_INSTANCE_TIME} time is added to
+ * the given master task, any {@link #EXDATE} for this time is removed. The task is inserted as an override to the given master. No fields are inherited
+ * though. {@link #ORIGINAL_INSTANCE_ALLDAY} will be set to {@link #IS_ALLDAY} of the master.
+ * + * Note, if the given master is non-recurring, this operation will turn it into a recurring task. |
invalid {@link Tasks} row {@code + * _ID} | An exception is thrown. |
+ *
+ * Note, the data of an update must not contain any fields related to recurrence ({@link #RRULE}, {@link #RDATE}, {@link #EXDATE}, {@link + * #ORIGINAL_INSTANCE_ID}, {@link #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY}). Also note, this table supports writing {@link #DURATION} + * (if the instance has a {@link #DTSTART}), but reading it back will always return a {@code null} {@link #DURATION} and a non-{@code null} {@link #DUE} + * date. Reading the task in the tasks table will, however, return the original {@link #DURATION}. + *
+ *
Target task type | Result |
---|---|
Recurring master task | A new override is created with the given data. Note, + * any fields which are not provided are inherited from the master, except for {@link #DTSTART} and {@link #DUE} which will be inherited from the instance + * and {@link #DURATION}, {@link #RRULE}, {@link #RDATE} and {@link #EXDATE} which are set to {@code null}. {@link #ORIGINAL_INSTANCE_ID}, {@link + * #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY} will be set accordingly. |
Single instance task | The task is + * updated with the given values. |
Recurrence override with existing master | The task is updated with the given values. |
Recurrence override without existing master | The task is updated with the given values. |
+ *
+ *
Target task type | Result |
---|---|
Recurring master task | An {@link #EXDATE} for this instance is added, any {@link
+ * #RDATE} for this instance is removed. The instance row is removed. TODO: mark the task deleted if the remaining recurrence set is empty |
Single instance task | The {@link Tasks#_DELETED} flag of the task is set. |
Recurrence override with existing + * master | The {@link Tasks#_DELETED} flag of the override is set, an {@link #EXDATE} for this instance is added to the master, any {@link #RDATE} + * for this instance is removed from the master. TODO: mark the master deleted if the remaining recurrence set of the master is empty |
Recurrence override without existing master | The {@link Tasks#_DELETED} flag of the task is set. |
+ * This is the instances view as seen by the content provider clients.
+ */
+ private final static String SQL_CREATE_INSTANCE_CLIENT_VIEW = "CREATE VIEW " + Tables.INSTANCE_CLIENT_VIEW + " AS SELECT "
+ + Tables.INSTANCES + ".*, "
+ // override task due, start and original times with the instance values
+ + Tables.INSTANCES + "." + TaskContract.Instances.INSTANCE_START + " as " + Tasks.DTSTART + ", "
+ + Tables.INSTANCES + "." + TaskContract.Instances.INSTANCE_DUE + " as " + Tasks.DUE + ", "
+ + Tables.INSTANCES + "." + TaskContract.Instances.INSTANCE_ORIGINAL_TIME + " as " + Tasks.ORIGINAL_INSTANCE_TIME + ", "
+ // override task duration with null, we already have a due
+ + "null as " + Tasks.DURATION + ", "
+ // override recurrence values with null, instances themselves are not recurring
+ + "null as " + Tasks.RRULE + ", "
+ + "null as " + Tasks.RDATE + ", "
+ + "null as " + Tasks.EXDATE + ", "
+ + Tables.TASKS + ".*, "
+ + Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", "
+ + Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", "
+ + Tables.LISTS + "." + Tasks.LIST_OWNER + ", "
+ + Tables.LISTS + "." + Tasks.LIST_NAME + ", "
+ + Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", "
+ + Tables.LISTS + "." + Tasks.LIST_COLOR + ", "
+ + Tables.LISTS + "." + Tasks.VISIBLE
+ + " FROM " + Tables.TASKS
+ + " JOIN " + Tables.LISTS + " ON (" + Tables.TASKS + "." + TaskContract.Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskContract.TaskLists._ID + ")"
+ + " JOIN " + Tables.INSTANCES + " ON (" + Tables.TASKS + "." + TaskContract.Tasks._ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ");";
+
/**
* SQL command to create a view that combines task instances view with the belonging properties.
*/
@@ -586,6 +618,7 @@ public void onCreate(SQLiteDatabase db)
db.execSQL(SQL_CREATE_TASK_VIEW);
db.execSQL(SQL_CREATE_TASK_PROPERTY_VIEW);
db.execSQL(SQL_CREATE_INSTANCE_VIEW);
+ db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW);
db.execSQL(SQL_CREATE_INSTANCE_PROPERTY_VIEW);
db.execSQL(SQL_CREATE_INSTANCE_CATEGORY_VIEW);
@@ -773,6 +806,11 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
db.execSQL("alter table " + Tables.INSTANCES + " add column " + TaskContract.Instances.DISTANCE_FROM_CURRENT + " integer default 0;");
}
+ if (oldVersion < 19)
+ {
+ db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW);
+ }
+
// upgrade FTS
FTSDatabaseHelper.onUpgrade(db, oldVersion, newVersion);
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java
index 8c351d471..0e5fe6f47 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java
@@ -602,7 +602,7 @@ public Cursor query(Uri uri, String[] projection, String selection, String[] sel
}
else
{
- sqlBuilder.setTables(Tables.INSTANCE_VIEW);
+ sqlBuilder.setTables(Tables.INSTANCE_CLIENT_VIEW);
}
if (!isSyncAdapter)
{
@@ -625,7 +625,7 @@ public Cursor query(Uri uri, String[] projection, String selection, String[] sel
}
else
{
- sqlBuilder.setTables(Tables.INSTANCE_VIEW);
+ sqlBuilder.setTables(Tables.INSTANCE_CLIENT_VIEW);
}
selectId(sqlBuilder, Instances._ID, uri);
if (!isSyncAdapter)
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java
index d6bb135d0..52d252ea1 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java
@@ -24,6 +24,7 @@
import org.dmfs.jems.iterable.decorators.Mapped;
import org.dmfs.jems.pair.Pair;
import org.dmfs.jems.single.Single;
+import org.dmfs.optional.NullSafe;
import org.dmfs.optional.Optional;
import org.dmfs.provider.tasks.TaskDatabaseHelper;
import org.dmfs.provider.tasks.model.TaskAdapter;
@@ -185,7 +186,8 @@ private void updateInstances(SQLiteDatabase db, TaskAdapter taskAdapter, long id
(newInstanceValues, cursorRow) ->
{
existingInstances.moveToPosition(cursorRow);
- return (int) (existingInstances.getLong(startIdx) - newInstanceValues.getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME));
+ return (int) (existingInstances.getLong(startIdx) -
+ new NullSafe<>(newInstanceValues.getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME)).value(0L));
});
// sync the instances table with the new instances
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java
index f4b8cd407..13fe85a30 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java
@@ -52,11 +52,13 @@ public Overridden(Optional
* Note: this is meant for use with an assert operation during tests as the instances table is read only and doesn't allow inserts nor updates.
*
@@ -78,8 +79,16 @@ public ContentProviderOperation.Builder updatedBuilder(@NonNull TransactionConte
.withValue(TaskContract.Instances.INSTANCE_DUE_SORTING, new Mapped<>(DateTime::getInstance, mInstanceDue).value(null))
.withValue(TaskContract.Instances.INSTANCE_DURATION,
new Zipped<>(mInstanceStart, mInstanceDue, (start, due) -> (due.getTimestamp() - start.getTimestamp())).value(null))
- .withValue(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, mOriginalTime.value(new DateTime(0)).getTimestamp())
- .withValue(TaskContract.Instances.DISTANCE_FROM_CURRENT, mDistanceFromCurrent);
+ .withValue(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, new Mapped<>(DateTime::getTimestamp, mOriginalTime).value(null))
+ .withValue(TaskContract.Instances.DISTANCE_FROM_CURRENT, mDistanceFromCurrent)
+ // the instances view overrides some of the task values. Since they are closely tied to the instance data we test them here as well.
+ .withValue(TaskContract.Instances.DTSTART, new Mapped<>(DateTime::getTimestamp, mInstanceStart).value(null))
+ .withValue(TaskContract.Instances.DUE, new Mapped<>(DateTime::getTimestamp, mInstanceDue).value(null))
+ .withValue(TaskContract.Instances.ORIGINAL_INSTANCE_TIME, new Mapped<>(DateTime::getTimestamp, mOriginalTime).value(null))
+ .withValue(TaskContract.Instances.DURATION, null)
+ .withValue(TaskContract.Instances.RRULE, null)
+ .withValue(TaskContract.Instances.RDATE, null)
+ .withValue(TaskContract.Instances.EXDATE, null);
}
}
diff --git a/opentaskspal/src/test/java/org/dmfs/opentaskstestpal/InstanceTestDataTest.java b/opentaskspal/src/test/java/org/dmfs/opentaskstestpal/InstanceTestDataTest.java
index 4ef6de6c2..bbfb4b92c 100644
--- a/opentaskspal/src/test/java/org/dmfs/opentaskstestpal/InstanceTestDataTest.java
+++ b/opentaskspal/src/test/java/org/dmfs/opentaskstestpal/InstanceTestDataTest.java
@@ -53,8 +53,15 @@ public void testNoDate() throws Exception
withNullValue(TaskContract.Instances.INSTANCE_DUE),
withNullValue(TaskContract.Instances.INSTANCE_DUE_SORTING),
withNullValue(TaskContract.Instances.INSTANCE_DURATION),
- containing(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 0L),
- containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5)
+ withNullValue(TaskContract.Instances.INSTANCE_ORIGINAL_TIME),
+ containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5),
+ withNullValue(TaskContract.Instances.DTSTART),
+ withNullValue(TaskContract.Instances.DUE),
+ withNullValue(TaskContract.Instances.ORIGINAL_INSTANCE_TIME),
+ withNullValue(TaskContract.Instances.DURATION),
+ withNullValue(TaskContract.Instances.RRULE),
+ withNullValue(TaskContract.Instances.RDATE),
+ withNullValue(TaskContract.Instances.EXDATE)
)
));
}
@@ -73,8 +80,15 @@ public void testWithDate() throws Exception
containing(TaskContract.Instances.INSTANCE_DUE, due.getTimestamp()),
containing(TaskContract.Instances.INSTANCE_DUE_SORTING, due.swapTimeZone(TimeZone.getDefault()).getInstance()),
containing(TaskContract.Instances.INSTANCE_DURATION, due.getTimestamp() - start.getTimestamp()),
- containing(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 0L),
- containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5)
+ withNullValue(TaskContract.Instances.INSTANCE_ORIGINAL_TIME),
+ containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5),
+ containing(TaskContract.Instances.DTSTART, start.getTimestamp()),
+ containing(TaskContract.Instances.DUE, due.getTimestamp()),
+ withNullValue(TaskContract.Instances.ORIGINAL_INSTANCE_TIME),
+ withNullValue(TaskContract.Instances.DURATION),
+ withNullValue(TaskContract.Instances.RRULE),
+ withNullValue(TaskContract.Instances.RDATE),
+ withNullValue(TaskContract.Instances.EXDATE)
)
));
}
@@ -95,7 +109,14 @@ public void testWithDateAndOriginalTime() throws Exception
containing(TaskContract.Instances.INSTANCE_DUE_SORTING, due.swapTimeZone(TimeZone.getDefault()).getInstance()),
containing(TaskContract.Instances.INSTANCE_DURATION, due.getTimestamp() - start.getTimestamp()),
containing(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, original.getTimestamp()),
- containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5)
+ containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5),
+ containing(TaskContract.Instances.DTSTART, start.getTimestamp()),
+ containing(TaskContract.Instances.DUE, due.getTimestamp()),
+ containing(TaskContract.Instances.ORIGINAL_INSTANCE_TIME, original.getTimestamp()),
+ withNullValue(TaskContract.Instances.DURATION),
+ withNullValue(TaskContract.Instances.RRULE),
+ withNullValue(TaskContract.Instances.RDATE),
+ withNullValue(TaskContract.Instances.EXDATE)
)
));
}
@@ -115,7 +136,14 @@ public void testWithStartDateAndOriginalTime() throws Exception
withNullValue(TaskContract.Instances.INSTANCE_DUE_SORTING),
withNullValue(TaskContract.Instances.INSTANCE_DURATION),
containing(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, original.getTimestamp()),
- containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5)
+ containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5),
+ containing(TaskContract.Instances.DTSTART, start.getTimestamp()),
+ containing(TaskContract.Instances.ORIGINAL_INSTANCE_TIME, original.getTimestamp()),
+ withNullValue(TaskContract.Instances.DUE),
+ withNullValue(TaskContract.Instances.DURATION),
+ withNullValue(TaskContract.Instances.RRULE),
+ withNullValue(TaskContract.Instances.RDATE),
+ withNullValue(TaskContract.Instances.EXDATE)
)
));
}
@@ -135,7 +163,14 @@ public void testWithDueDateAndOriginalTime() throws Exception
containing(TaskContract.Instances.INSTANCE_DUE_SORTING, due.swapTimeZone(TimeZone.getDefault()).getInstance()),
withNullValue(TaskContract.Instances.INSTANCE_DURATION),
containing(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, original.getTimestamp()),
- containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5)
+ containing(TaskContract.Instances.DISTANCE_FROM_CURRENT, 5),
+ withNullValue(TaskContract.Instances.DTSTART),
+ containing(TaskContract.Instances.DUE, due.getTimestamp()),
+ containing(TaskContract.Instances.ORIGINAL_INSTANCE_TIME, original.getTimestamp()),
+ withNullValue(TaskContract.Instances.DURATION),
+ withNullValue(TaskContract.Instances.RRULE),
+ withNullValue(TaskContract.Instances.RDATE),
+ withNullValue(TaskContract.Instances.EXDATE)
)
));
}