diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f8b02f6243677a23a688b8ef9bfc9963e742cb24
--- /dev/null
+++ b/.idea/assetWizardSettings.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index e3f27d1ac92b0eb3ed1aae161e224c85398a2de9..439e5b9e300fe20a04b2a680518c04aa486bcddd 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -45,7 +45,6 @@
-
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index eb2873e7ed57bab6887aec5b9785a87c916facee..bd4a8dc1bf734df52b2f4acee9632ef4a1284b84 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -26,5 +26,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index d31766a9b06afc0daae3eb8c7a1255558c74500e..c395c3ecccbce4b934cd83e83698c4c7f6785e7c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,6 +15,7 @@ android:
- tools
- build-tools-28.0.3
- build-tools-29.0.2
+ - build-tools-29.0.3
- android-24
- android-28
- android-29
diff --git a/build.gradle b/build.gradle
index 4230247bf2544e937c1459adcfdeb03ec4115edf..6701d0896011ba7028edd93998c3ee0a75805553 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,8 +6,8 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.2'
- classpath("com.github.triplet.gradle:play-publisher:2.0.0-rc1")
+ classpath 'com.android.tools.build:gradle:4.1.2'
+ classpath("com.github.triplet.gradle:play-publisher:2.8.1")
}
}
diff --git a/dependencies.gradle b/dependencies.gradle
index defd06e329fcd8f357d88b2ae52dcd910f06850b..003aa290432e2d2819a837738b1e426f6f6f38c8 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,18 +1,18 @@
-def jems_version = '1.24'
+def jems_version = '1.43'
def contentpal_version = '0.6'
def androidx_test_runner_version = '1.1.1'
ext.deps = [
// Support & Google libraries
- support_appcompat : 'androidx.appcompat:appcompat:1.0.2',
- support_annotations: 'androidx.annotation:annotation:1.0.0',
- support_design : 'com.google.android.material:material:1.0.0',
+ support_appcompat : 'androidx.appcompat:appcompat:1.2.0',
+ support_annotations: 'androidx.annotation:annotation:1.1.0',
+ support_design : 'com.google.android.material:material:1.2.1',
android_dashclock : 'com.google.android.apps.dashclock:dashclock-api:2.0.0',
// dmfs
jems : "org.dmfs:jems:$jems_version",
datetime : 'org.dmfs:rfc5545-datetime:0.2.4',
- lib_recur : 'org.dmfs:lib-recur:0.11.2',
+ lib_recur : 'org.dmfs:lib-recur:0.12.2',
xml_magic : 'org.dmfs:android-xml-magic:0.1.1',
color_picker : 'com.github.dmfs:color-picker:1.3',
android_carrot : 'com.github.dmfs.androidcarrot:androidcarrot:13edc04',
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0aabb04648a79d4547953d0ee3f543ed4862b158..ce73a4441b9aadd179b127052781f47455914c17 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Thu Nov 14 17:12:16 CET 2019
+#Wed Feb 10 23:10:33 CET 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
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 977b75598c8f9552878364813b60931a1cb2b261..3ce329daaa1e0c36d02ac9cc6a5ae0b9dad3bb1c 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
@@ -795,6 +795,13 @@ public final class TaskContract
/**
* The row id of the parent task. null if the task has no parent task.
*
+ * Note, when writing this value the task {@link Property.Relation} properties are updated accordingly. Any parent or child relations which
+ * make this a child of another task are deleted and a new {@link Property.Relation#RELTYPE_PARENT} relation pointing to the new parent is created.
+ * Be aware that Siblings will be split, i.e. they are not moved to the new parent. Currently this might cause siblings to become orphans if they
+ * don't have a parent-child relationship. This behavior may change in future version.
+ *
+ *
+ *
* Value: Long
*
*/
@@ -1204,6 +1211,15 @@ public final class TaskContract
*/
public static final String VISIBLE = "visible";
+ /**
+ * Flag indicating that ths is an instance of a recurring task.
+ *
+ * Value: Integer
+ *
+ * read-only
+ */
+ public static final String IS_RECURRING = "is_recurring";
+
public static final String CONTENT_URI_PATH = "instances";
public static final String DEFAULT_SORT_ORDER = INSTANCE_DUE_SORTING;
@@ -1566,6 +1582,8 @@ public final class TaskContract
*
* When writing a relation, exactly one of {@link #RELATED_ID} or {@link #RELATED_UID} must be present. The missing value and {@link
* #RELATED_CONTENT_URI} will be populated automatically if possible.
+ *
+ * {@link Tasks#PARENT_ID} is updated automatically if possible.
*/
interface Relation extends PropertyColumns
{
diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderDetachInstancesTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderDetachInstancesTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd824a9a44f5bb0c2ee1841ff5d83c296d25d1a8
--- /dev/null
+++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderDetachInstancesTest.java
@@ -0,0 +1,752 @@
+/*
+ * Copyright 2019 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks;
+
+import android.accounts.Account;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.Build;
+import android.os.RemoteException;
+
+import org.dmfs.android.contentpal.Operation;
+import org.dmfs.android.contentpal.OperationsQueue;
+import org.dmfs.android.contentpal.RowSnapshot;
+import org.dmfs.android.contentpal.Table;
+import org.dmfs.android.contentpal.operations.Assert;
+import org.dmfs.android.contentpal.operations.BulkAssert;
+import org.dmfs.android.contentpal.operations.BulkDelete;
+import org.dmfs.android.contentpal.operations.BulkUpdate;
+import org.dmfs.android.contentpal.operations.Counted;
+import org.dmfs.android.contentpal.operations.Put;
+import org.dmfs.android.contentpal.predicates.AllOf;
+import org.dmfs.android.contentpal.predicates.AnyOf;
+import org.dmfs.android.contentpal.predicates.EqArg;
+import org.dmfs.android.contentpal.predicates.Not;
+import org.dmfs.android.contentpal.predicates.ReferringTo;
+import org.dmfs.android.contentpal.queues.BasicOperationsQueue;
+import org.dmfs.android.contentpal.rowdata.CharSequenceRowData;
+import org.dmfs.android.contentpal.rowdata.Composite;
+import org.dmfs.android.contentpal.rowdata.EmptyRowData;
+import org.dmfs.android.contentpal.rowsnapshots.VirtualRowSnapshot;
+import org.dmfs.android.contentpal.tables.Synced;
+import org.dmfs.android.contenttestpal.operations.AssertEmptyTable;
+import org.dmfs.android.contenttestpal.operations.AssertRelated;
+import org.dmfs.iterables.SingletonIterable;
+import org.dmfs.jems.iterable.elementary.Seq;
+import org.dmfs.jems.optional.elementary.Present;
+import org.dmfs.opentaskspal.tables.InstanceTable;
+import org.dmfs.opentaskspal.tables.LocalTaskListsTable;
+import org.dmfs.opentaskspal.tables.TaskListScoped;
+import org.dmfs.opentaskspal.tables.TaskListsTable;
+import org.dmfs.opentaskspal.tables.TasksTable;
+import org.dmfs.opentaskspal.tasks.ExDatesTaskData;
+import org.dmfs.opentaskspal.tasks.RDatesTaskData;
+import org.dmfs.opentaskspal.tasks.RRuleTaskData;
+import org.dmfs.opentaskspal.tasks.StatusData;
+import org.dmfs.opentaskspal.tasks.TimeData;
+import org.dmfs.opentaskspal.tasks.TitleData;
+import org.dmfs.opentaskstestpal.InstanceTestData;
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.rfc5545.Duration;
+import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException;
+import org.dmfs.rfc5545.recur.RecurrenceRule;
+import org.dmfs.tasks.contract.TaskContract.Instances;
+import org.dmfs.tasks.contract.TaskContract.TaskLists;
+import org.dmfs.tasks.contract.TaskContract.Tasks;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.TimeZone;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn;
+import static org.dmfs.jems.optional.elementary.Absent.absent;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * Test {@link TaskProvider} for correctly detaching completed instances.
+ *
+ * @author Marten Gajda
+ */
+@RunWith(AndroidJUnit4.class)
+public class TaskProviderDetachInstancesTest
+{
+ private String mAuthority;
+ private ContentProviderClient mClient;
+ private Account mTestAccount = new Account("testname", "testtype");
+
+
+ @Before
+ public void setUp() throws Exception
+ {
+ Context context = InstrumentationRegistry.getTargetContext();
+ mAuthority = AuthorityUtil.taskAuthority(context);
+ mClient = context.getContentResolver().acquireContentProviderClient(mAuthority);
+
+ // Assert that tables are empty:
+ OperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.flush();
+ queue.enqueue(new Seq>(
+ new AssertEmptyTable<>(new TasksTable(mAuthority)),
+ new AssertEmptyTable<>(new TaskListsTable(mAuthority)),
+ new AssertEmptyTable<>(new InstanceTable(mAuthority))));
+ queue.flush();
+ }
+
+
+ @After
+ public void tearDown() throws Exception
+ {
+ /*
+ TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation
+ https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html
+ https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator
+ */
+
+ // Clear the DB:
+ BasicOperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new SingletonIterable>(new BulkDelete<>(new LocalTaskListsTable(mAuthority))));
+ queue.flush();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ {
+ mClient.close();
+ }
+ else
+ {
+ mClient.release();
+ }
+ }
+
+
+ /**
+ * Test if the first instance of a task with a DTSTART, DUE and an RRULE is correctly detached when completed.
+ */
+ @Test
+ public void testRRule() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority)));
+ Table instancesTable = new InstanceTable(mAuthority);
+ RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ Duration hour = new Duration(1, 0, 3600 /* 1 hour */);
+ DateTime start = DateTime.parse("20180104T123456Z");
+ DateTime due = start.addDuration(hour);
+ DateTime localStart = start.shiftTimeZone(TimeZone.getDefault());
+
+ Duration day = new Duration(1, 1, 0);
+
+ DateTime second = localStart.addDuration(day);
+ DateTime third = second.addDuration(day);
+ DateTime fourth = third.addDuration(day);
+ DateTime fifth = fourth.addDuration(day);
+
+ DateTime localDue = due.shiftTimeZone(TimeZone.getDefault());
+
+ OperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq<>(
+ new Put<>(taskList, new EmptyRowData<>()),
+ new Put<>(task,
+ new Composite<>(
+ new TimeData<>(start, due),
+ new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX))))
+ ));
+ queue.flush();
+
+ assertThat(new Seq<>(
+ // update the first non-closed instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ),
+ resultsIn(queue,
+ /*
+ * We expect three tasks:
+ * - the original master with updated RRULE, DTSTART and DUE
+ * - a deleted instance
+ * - a detached task
+ */
+
+ // the original master
+ new Assert<>(task,
+ new Composite<>(
+ new TimeData<>(start.addDuration(day), due.addDuration(day)),
+ new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=4"))),
+ // there is one instance referring to the master (the old second instance, now first)
+ new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)),
+ // the detached task instance:
+ new Counted<>(1, new BulkAssert<>(new Synced<>(mTestAccount, instancesTable),
+ new Composite<>(
+ new InstanceTestData(localStart, localDue, absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task)))),
+ // the deleted task (doesn't have an instance)
+ new Counted<>(1, new BulkAssert<>(new Synced<>(mTestAccount, new TasksTable(mAuthority)),
+ new Composite<>(new TimeData<>(start, due)),
+ new AllOf<>(
+ new ReferringTo<>(Tasks.ORIGINAL_INSTANCE_ID, task),
+ new EqArg<>(Tasks._DELETED, 1)))),
+ // the former 2nd instance (now first)
+ new AssertRelated<>(new Synced<>(mTestAccount, instancesTable), Instances.TASK_ID, task,
+ new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp()))));
+ }
+
+
+ /**
+ * Test if two instances of a task with a DTSTART, DUE and an RRULE are detached correctly.
+ */
+ @Test
+ public void testRRuleCompleteAll() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority)));
+ Table instancesTable = new InstanceTable(mAuthority);
+ RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ Duration hour = new Duration(1, 0, 3600 /* 1 hour */);
+ DateTime start = DateTime.parse("20180104T123456Z");
+ DateTime due = start.addDuration(hour);
+ DateTime localStart = start.shiftTimeZone(TimeZone.getDefault());
+
+ Duration day = new Duration(1, 1, 0);
+
+ DateTime second = localStart.addDuration(day);
+ DateTime third = second.addDuration(day);
+ DateTime fourth = third.addDuration(day);
+ DateTime fifth = fourth.addDuration(day);
+
+ DateTime localDue = due.shiftTimeZone(TimeZone.getDefault());
+
+ OperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq<>(
+ new Put<>(taskList, new EmptyRowData<>()),
+ new Put<>(task,
+ new Composite<>(
+ new TitleData("Test-Task"),
+ new TimeData<>(start, due),
+ new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)))),
+ // complete the first non-closed instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ));
+ queue.flush();
+
+ Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority));
+ Synced syncedInstances = new Synced<>(mTestAccount, instancesTable);
+ assertThat(new Seq<>(
+ // update the second instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ),
+ resultsIn(queue,
+ /*
+ * We expect five tasks:
+ * - the original master with updated RRULE, DTSTART and DUE, deleted
+ * - a completed and deleted overrides for the first and second instance
+ * - a detached first and second instance
+ */
+
+ // the original master
+ new Assert<>(task,
+ new Composite<>(
+ // points to former second instance before being deleted
+ new TimeData<>(start.addDuration(day), due.addDuration(day)),
+ new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=1"),
+ new CharSequenceRowData<>(Tasks._DELETED, "1"))),
+ // there is no instance referring to the master because it has been fully completed (and deleted)
+ new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)),
+ // the first detached task instance:
+ new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(localStart, localDue, absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, start.getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // the second detached task instance:
+ new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(second, second.addDuration(new Duration(1, 0, 3600)), absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, second.getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // two instances total, both completed
+ new Counted<>(2,
+ new BulkAssert<>(
+ syncedInstances,
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)),
+ new AnyOf<>())),
+ // five tasks in total
+ new Counted<>(5,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new AnyOf<>())),
+ // three deleted tasks in total
+ new Counted<>(3,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new EqArg<>(Tasks._DELETED, 1)))));
+ }
+
+
+ /**
+ * Test if two instances of a task with a DTSTART, DUE, RRULE and RDATE are detached correctly.
+ */
+ @Test
+ public void testRRuleRDateCompleteFirstTwo() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority)));
+ Table instancesTable = new InstanceTable(mAuthority);
+ RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ Duration hour = new Duration(1, 0, 3600 /* 1 hour */);
+ DateTime start = DateTime.parse("20180104T123456Z");
+ DateTime due = start.addDuration(hour);
+ DateTime localStart = start.shiftTimeZone(TimeZone.getDefault());
+
+ Duration day = new Duration(1, 1, 0);
+
+ DateTime second = localStart.addDuration(day);
+ DateTime third = second.addDuration(day);
+ DateTime fourth = third.addDuration(day);
+ DateTime fifth = fourth.addDuration(day);
+
+ DateTime localDue = due.shiftTimeZone(TimeZone.getDefault());
+
+ OperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq<>(
+ new Put<>(taskList, new EmptyRowData<>()),
+ new Put<>(task,
+ new Composite<>(
+ new TitleData("Test-Task"),
+ new TimeData<>(start, due),
+ new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)),
+ new RDatesTaskData(
+ new Seq<>(
+ DateTime.parse("20180103T123456Z"),
+ DateTime.parse("20180105T123456Z"),
+ DateTime.parse("20180107T123456Z"))))),
+ // update the first non-closed instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ));
+ queue.flush();
+
+ Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority));
+ Synced syncedInstances = new Synced<>(mTestAccount, instancesTable);
+ assertThat(new Seq<>(
+ // update the second instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ),
+ resultsIn(queue,
+ /*
+ * We expect five tasks:
+ * - the original master with updated RRULE, RDATES, DTSTART and DUE, deleted
+ * - completed and deleted overrides for the first and second instance
+ * - a detached first and second instance
+ */
+
+ // the first detached task instance:
+ new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(DateTime.parse("20180103T123456Z"), DateTime.parse("20180103T133456Z"), absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180103T123456Z").getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // the original master has been updated
+ new Assert<>(task,
+ new Composite<>(
+ // points to former third instance before being deleted
+ new TimeData<>(start.addDuration(day).addDuration(day), due.addDuration(day).addDuration(day)),
+ new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;INTERVAL=2;COUNT=1"),
+ new CharSequenceRowData<>(Tasks._DELETED, "0"),
+ new RDatesTaskData(
+ new Seq<>(
+ DateTime.parse("20180105T123456Z"),
+ DateTime.parse("20180107T123456Z"))))),
+ // there is one instance referring to the master
+ new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
+ new CharSequenceRowData<>(Instances.INSTANCE_ORIGINAL_TIME,
+ String.valueOf(DateTime.parse("20180105T123456Z").getTimestamp())))),
+ // the second detached task instance:
+ new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(start, due, absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, start.getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // two completed instances, neither of them referring to the master
+ new Counted<>(2,
+ new BulkAssert<>(
+ syncedInstances,
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)),
+ new AllOf<>(
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // one incomplete instance , the first instance of the new master
+ new Counted<>(1,
+ new BulkAssert<>(
+ syncedInstances,
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_NEEDS_ACTION)),
+ new AllOf<>(
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0),
+ new ReferringTo<>(Instances.TASK_ID, task)))),
+ // five tasks in total (two deleted overrides, two detached ones and the new master)
+ new Counted<>(5,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new AnyOf<>())),
+ // two deleted tasks in total (the old overrides)
+ new Counted<>(2,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new EqArg<>(Tasks._DELETED, 1)))));
+ }
+
+
+ /**
+ * Test if two instances of a task with a DTSTART, DUE, RRULE, RDATE and EXDATE are detached correctly.
+ */
+ @Test
+ public void testRRuleRDateCompleteWithExdates() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority)));
+ Table instancesTable = new InstanceTable(mAuthority);
+ RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ Duration hour = new Duration(1, 0, 3600 /* 1 hour */);
+ DateTime start = DateTime.parse("20180104T123456Z");
+ DateTime due = start.addDuration(hour);
+ DateTime localStart = start.shiftTimeZone(TimeZone.getDefault());
+
+ Duration day = new Duration(1, 1, 0);
+
+ DateTime second = localStart.addDuration(day);
+ DateTime third = second.addDuration(day);
+ DateTime fourth = third.addDuration(day);
+ DateTime fifth = fourth.addDuration(day);
+
+ DateTime localDue = due.shiftTimeZone(TimeZone.getDefault());
+
+ OperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq<>(
+ new Put<>(taskList, new EmptyRowData<>()),
+ new Put<>(task,
+ new Composite<>(
+ new TitleData("Test-Task"),
+ new TimeData<>(start, due),
+ new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)),
+ new RDatesTaskData(
+ new Seq<>(
+ DateTime.parse("20180105T123456Z"),
+ DateTime.parse("20180107T123456Z"))),
+ new ExDatesTaskData(
+ new Seq<>(
+ DateTime.parse("20180104T123456Z"),
+ DateTime.parse("20180105T123456Z"))))),
+ // update the first non-closed instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ));
+ queue.flush();
+
+ Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority));
+ Synced syncedInstances = new Synced<>(mTestAccount, instancesTable);
+ assertThat(new Seq<>(
+ // update the second instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ),
+ resultsIn(queue,
+ /*
+ * We expect five tasks:
+ * - the original master deleted
+ * - completed and deleted overrides for the first and second instance
+ * - detached first and second instances
+ */
+
+ // the first detached task instance:
+ new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(DateTime.parse("20180106T123456Z"), DateTime.parse("20180106T133456Z"), absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180106T123456Z").getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // the original master has been deleted
+ new Counted<>(0, new Assert<>(task, new Composite<>(new EmptyRowData<>()))),
+ // there is no instance referring to the master
+ new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)),
+ // the second detached task instance:
+ new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(DateTime.parse("20180107T123456Z"), DateTime.parse("20180107T133456Z"), absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180107T123456Z").getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // two completed instances, neither of them referring to the master
+ new Counted<>(2,
+ new BulkAssert<>(
+ syncedInstances,
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)),
+ new AllOf<>(
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // five tasks in total (two deleted overrides, two detached ones and the old master)
+ new Counted<>(5,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new AnyOf<>())),
+ // three deleted tasks in total (the old overrides and the old master)
+ new Counted<>(3,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new EqArg<>(Tasks._DELETED, 1)))));
+ }
+
+
+ /**
+ * Test if two instances of a task with a DTSTART, DUE, RRULE, RDATE and EXDATE are detached correctly.
+ */
+ @Test
+ public void testRRuleRDateCompleteOnlyRRuleInstances() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority)));
+ Table instancesTable = new InstanceTable(mAuthority);
+ RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ Duration hour = new Duration(1, 0, 3600 /* 1 hour */);
+ DateTime start = DateTime.parse("20180104T123456Z");
+ DateTime due = start.addDuration(hour);
+ DateTime localStart = start.shiftTimeZone(TimeZone.getDefault());
+
+ Duration day = new Duration(1, 1, 0);
+
+ DateTime second = localStart.addDuration(day);
+ DateTime third = second.addDuration(day);
+ DateTime fourth = third.addDuration(day);
+ DateTime fifth = fourth.addDuration(day);
+
+ DateTime localDue = due.shiftTimeZone(TimeZone.getDefault());
+
+ OperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq<>(
+ new Put<>(taskList, new EmptyRowData<>()),
+ new Put<>(task,
+ new Composite<>(
+ new TitleData("Test-Task"),
+ new TimeData<>(start, due),
+ new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)),
+ new RDatesTaskData(
+ new Seq<>(
+ DateTime.parse("20180105T123456Z"),
+ DateTime.parse("20180107T123456Z"))),
+ new ExDatesTaskData(
+ new Seq<>(
+ DateTime.parse("20180104T123456Z")))))
+/* // update the first non-closed instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))*/
+ ));
+ queue.flush();
+
+ Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority));
+ Synced syncedInstances = new Synced<>(mTestAccount, instancesTable);
+ assertThat(new Seq<>(
+ // update the second instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ),
+ resultsIn(queue,
+ /*
+ * We expect five tasks:
+ * - the original master deleted
+ * - completed and deleted overrides for the first and second instance
+ * - detached first and second instances
+ */
+
+ // the first detached task instance:
+ new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(DateTime.parse("20180105T123456Z"), DateTime.parse("20180105T133456Z"), absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180105T123456Z").getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // the original master has been updated
+ new Assert<>(task,
+ new Composite<>(
+ new TimeData<>(DateTime.parse("20180106T123456Z"), DateTime.parse("20180106T133456Z")),
+ new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;INTERVAL=2;COUNT=1"),
+ new CharSequenceRowData<>(Tasks._DELETED, "0"),
+ new RDatesTaskData(
+ new Seq<>(
+ DateTime.parse("20180107T123456Z"))))),
+ // the second detached task instance:
+ /* new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(DateTime.parse("20180106T123456Z"), DateTime.parse("20180106T133456Z"), absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180106T123456Z").getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),*/
+ // one completed instance, not referring to the master
+ new Counted<>(1,
+ new BulkAssert<>(
+ syncedInstances,
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)),
+ new AllOf<>(
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // three tasks in total (one deleted override, one detached one and the master)
+ new Counted<>(3,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new AnyOf<>())),
+ // three deleted tasks in total (the old overrides and the old master)
+ new Counted<>(1,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new EqArg<>(Tasks._DELETED, 1)))));
+ }
+
+
+ /**
+ * Test if two all-day instances of a task with a DTSTART, DUE, RRULE, RDATE and EXDATE are detached correctly.
+ */
+ @Test
+ public void testRRuleRDateCompleteWithExdatesAllDay() throws InvalidRecurrenceRuleException, RemoteException, OperationApplicationException
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(mTestAccount, new TaskListsTable(mAuthority)));
+ Table instancesTable = new InstanceTable(mAuthority);
+ RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ Duration hour = new Duration(1, 1, 0);
+ DateTime start = DateTime.parse("20180104");
+ DateTime due = start.addDuration(hour);
+
+ Duration day = new Duration(1, 1, 0);
+
+ OperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq<>(
+ new Put<>(taskList, new EmptyRowData<>()),
+ new Put<>(task,
+ new Composite<>(
+ new TitleData("Test-Task"),
+ new TimeData<>(start, due),
+ new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=2", RecurrenceRule.RfcMode.RFC2445_LAX)),
+ new RDatesTaskData(
+ new Seq<>(
+ DateTime.parse("20180105"),
+ DateTime.parse("20180107"))),
+ new ExDatesTaskData(
+ new Seq<>(
+ DateTime.parse("20180104"),
+ DateTime.parse("20180105"))))),
+ // update the first non-closed instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ));
+ queue.flush();
+
+ Synced tasksTable = new Synced<>(mTestAccount, new TasksTable(mAuthority));
+ Synced syncedInstances = new Synced<>(mTestAccount, instancesTable);
+ assertThat(new Seq<>(
+ // update the second instance
+ new BulkUpdate<>(instancesTable, new StatusData<>(Tasks.STATUS_COMPLETED),
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, 0)))
+ ),
+ resultsIn(queue,
+ /*
+ * We expect five tasks:
+ * - the original master deleted
+ * - completed and deleted overrides for the first and second instance
+ * - detached first and second instances
+ */
+
+ // the first detached task instance:
+ new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(DateTime.parse("20180106"), DateTime.parse("20180107"), absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180106").getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // the original master has been deleted
+ new Counted<>(0, new Assert<>(task, new Composite<>(new EmptyRowData<>()))),
+ // there is no instance referring to the master
+ new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)),
+ // the second detached task instance:
+ new Counted<>(1, new BulkAssert<>(syncedInstances,
+ new Composite<>(
+ new InstanceTestData(DateTime.parse("20180107"), DateTime.parse("20180108"), absent(), -1),
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED))),
+ new AllOf<>(
+ new EqArg<>(Instances.INSTANCE_START, DateTime.parse("20180107").getTimestamp()),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // two completed instances, neither of them referring to the master
+ new Counted<>(2,
+ new BulkAssert<>(
+ syncedInstances,
+ new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)),
+ new AllOf<>(
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, -1),
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // five tasks in total (two deleted overrides, two detached ones and the old master)
+ new Counted<>(5,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new AnyOf<>())),
+ // three deleted tasks in total (the old overrides and the old master)
+ new Counted<>(3,
+ new BulkAssert<>(
+ tasksTable,
+ new TitleData("Test-Task"),
+ new EqArg<>(Tasks._DELETED, 1)))));
+ }
+
+}
diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRecurrenceTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRecurrenceTest.java
index 69123f62787644529b108e9c27f76722d120ed62..727bf5328180576ab03eda58c82fb1a0e2c233e1 100644
--- a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRecurrenceTest.java
+++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRecurrenceTest.java
@@ -32,6 +32,7 @@ import org.dmfs.android.contentpal.operations.Counted;
import org.dmfs.android.contentpal.operations.Put;
import org.dmfs.android.contentpal.predicates.AllOf;
import org.dmfs.android.contentpal.predicates.EqArg;
+import org.dmfs.android.contentpal.predicates.IsNull;
import org.dmfs.android.contentpal.predicates.Not;
import org.dmfs.android.contentpal.predicates.ReferringTo;
import org.dmfs.android.contentpal.queues.BasicOperationsQueue;
@@ -42,7 +43,8 @@ import org.dmfs.android.contentpal.rowsnapshots.VirtualRowSnapshot;
import org.dmfs.android.contenttestpal.operations.AssertEmptyTable;
import org.dmfs.android.contenttestpal.operations.AssertRelated;
import org.dmfs.iterables.SingletonIterable;
-import org.dmfs.iterables.elementary.Seq;
+import org.dmfs.jems.iterable.elementary.Seq;
+import org.dmfs.jems.optional.elementary.Present;
import org.dmfs.opentaskspal.tables.InstanceTable;
import org.dmfs.opentaskspal.tables.LocalTaskListsTable;
import org.dmfs.opentaskspal.tables.TaskListScoped;
@@ -59,7 +61,6 @@ import org.dmfs.opentaskspal.tasks.SyncIdData;
import org.dmfs.opentaskspal.tasks.TimeData;
import org.dmfs.opentaskspal.tasks.TitleData;
import org.dmfs.opentaskstestpal.InstanceTestData;
-import org.dmfs.optional.Present;
import org.dmfs.rfc5545.DateTime;
import org.dmfs.rfc5545.Duration;
import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException;
@@ -179,23 +180,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)
);
}
@@ -243,23 +244,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)
);
}
@@ -305,23 +306,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)
);
}
@@ -369,23 +370,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())) */)
);
}
@@ -428,23 +429,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(absent(), new Present<>(localDue), new Present<>(due), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, due.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, due.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(absent(), new Present<>(second), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(absent(), new Present<>(third), new Present<>(third), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(absent(), new Present<>(fourth), new Present<>(fourth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(absent(), new Present<>(fifth), new Present<>(fifth), 4),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
);
}
@@ -487,23 +488,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(new Present<>(localStart), absent(), new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(new Present<>(second), absent(), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(new Present<>(third), absent(), new Present<>(third), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(new Present<>(fourth), absent(), new Present<>(fourth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(new Present<>(fifth), absent(), new Present<>(fifth), 4),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
);
}
@@ -541,7 +542,7 @@ public class TaskProviderRecurrenceTest
new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))),
// remove the third instance
new BulkDelete<>(instancesTable,
- new AllOf(new ReferringTo<>(Instances.TASK_ID, task), new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())))
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())))
), resultsIn(mClient,
new Assert<>(task,
@@ -553,19 +554,19 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())),
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 4th instance (now 3rd):
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance (now 4th):
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())))
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())))
);
}
@@ -625,28 +626,156 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3th instance (the overridden one):
new AssertRelated<>(instancesTable, Instances.TASK_ID, taskOverride,
new InstanceTestData(third.addDuration(hour), third.addDuration(hour).addDuration(hour), new Present<>(third),
2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
);
}
+ /**
+ * Test RRULE with overridden instance (inserted into the tasks table) and a completed 1st instance.
+ */
+ @Test
+ public void testRRuleWith2ndOverrideAndCompleted1st() throws InvalidRecurrenceRuleException
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority));
+ Table instancesTable = new InstanceTable(mAuthority);
+ RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskOverride = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ Duration hour = new Duration(1, 0, 3600 /* 1 hour */);
+ DateTime start = DateTime.parse("20180104T123456Z");
+ DateTime due = start.addDuration(hour);
+
+ Duration day = new Duration(1, 1, 0);
+
+ DateTime localStart = start.shiftTimeZone(TimeZone.getDefault());
+ DateTime localDue = due.shiftTimeZone(TimeZone.getDefault());
+
+ DateTime second = localStart.addDuration(day);
+ DateTime third = second.addDuration(day);
+ DateTime fourth = third.addDuration(day);
+ DateTime fifth = fourth.addDuration(day);
+
+ assertThat(new Seq<>(
+ new Put<>(taskList, new EmptyRowData<>()),
+ new Put<>(task,
+ new Composite<>(
+ new TimeData<>(start, due),
+ new TitleData("original"),
+ new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))),
+ // the override moves the instance by an hour
+ new Put<>(taskOverride, new Composite<>(
+ new TimeData<>(second.addDuration(hour), second.addDuration(hour).addDuration(hour)),
+ new TitleData("override"),
+ new OriginalInstanceData(task, second))),
+ new Put<>(task, new StatusData<>(Tasks.STATUS_COMPLETED))),
+ resultsIn(mClient,
+ new Assert<>(task,
+ new Composite<>(
+ new TimeData<>(start, due),
+ new CharSequenceRowData<>(Tasks.TITLE, "original"),
+ new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"),
+ new StatusData<>(Tasks.STATUS_COMPLETED))),
+ new Assert<>(taskOverride,
+ new Composite<>(
+ new TimeData<>(second.addDuration(hour), second.addDuration(hour).addDuration(hour)),
+ new CharSequenceRowData<>(Tasks.TITLE, "override"),
+ new OriginalInstanceData(task, second))),
+ // 1st (completed) instance:
+ new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
+ new InstanceTestData(localStart, localDue, new Present<>(start), -1),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))),
+ // 2nd instance (now the current one):
+ new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, taskOverride,
+ new InstanceTestData(
+ second.addDuration(hour),
+ second.addDuration(hour).addDuration(hour),
+ new Present<>(second), 0)))));
+ }
+
+
+ /**
+ * Test RRULE with overridden instance (inserted into the tasks table) and a deleted 1st instance.
+ */
+ @Test
+ public void testRRuleWith2ndOverrideAndDeleted1st() throws InvalidRecurrenceRuleException
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority));
+ Table instancesTable = new InstanceTable(mAuthority);
+ RowSnapshot task = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskOverride = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ Duration hour = new Duration(1, 0, 3600 /* 1 hour */);
+ DateTime start = DateTime.parse("20180104T123456Z");
+ DateTime due = start.addDuration(hour);
+
+ Duration day = new Duration(1, 1, 0);
+
+ DateTime localStart = start.shiftTimeZone(TimeZone.getDefault());
+ DateTime localDue = due.shiftTimeZone(TimeZone.getDefault());
+
+ DateTime second = localStart.addDuration(day);
+ DateTime third = second.addDuration(day);
+ DateTime fourth = third.addDuration(day);
+ DateTime fifth = fourth.addDuration(day);
+
+ assertThat(new Seq<>(
+ new Put<>(taskList, new EmptyRowData<>()),
+ new Put<>(task,
+ new Composite<>(
+ new TimeData<>(start, due),
+ new TitleData("original"),
+ new RRuleTaskData(new RecurrenceRule("FREQ=DAILY;COUNT=5", RecurrenceRule.RfcMode.RFC2445_LAX)))),
+ // the override moves the instance by an hour
+ new Put<>(taskOverride, new Composite<>(
+ new TimeData<>(second.addDuration(hour), second.addDuration(hour).addDuration(hour)),
+ new TitleData("override"),
+ new OriginalInstanceData(task, second))),
+ // delete 1st instance
+ new BulkDelete<>(instancesTable, new AllOf<>(
+ new ReferringTo<>(Instances.TASK_ID, task),
+ new EqArg<>(Instances.DISTANCE_FROM_CURRENT, "0")))),
+ resultsIn(mClient,
+ new Assert<>(task,
+ new Composite<>(
+ new TimeData<>(start, due),
+ new CharSequenceRowData<>(Tasks.TITLE, "original"),
+ new CharSequenceRowData<>(Tasks.RRULE, "FREQ=DAILY;COUNT=5"),
+ new CharSequenceRowData<>(Tasks.EXDATE, start.toString()),
+ new StatusData<>(Tasks.STATUS_DEFAULT))),
+ new Assert<>(taskOverride,
+ new Composite<>(
+ new TimeData<>(second.addDuration(hour), second.addDuration(hour).addDuration(hour)),
+ new CharSequenceRowData<>(Tasks.TITLE, "override"),
+ new OriginalInstanceData(task, second))),
+ // no instances point to the original task
+ new Counted<>(0, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)),
+ // 2nd instance (now the current one):
+ new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, taskOverride,
+ new InstanceTestData(
+ second.addDuration(hour),
+ second.addDuration(hour).addDuration(hour),
+ new Present<>(second), 0)))));
+ }
+
+
/**
* Test RRULE with overridden instance (via update on the instances table). This time we don't override the date time fields and expect the instance to
* inherit the original instance start and due (instead of the master start and due)
@@ -685,7 +814,7 @@ public class TaskProviderRecurrenceTest
new BulkUpdate<>(instancesTable,
new Composite<>(
new CharSequenceRowData(Tasks.TITLE, "override")),
- new AllOf(new ReferringTo<>(Instances.TASK_ID, task), new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())))
+ new AllOf<>(new ReferringTo<>(Instances.TASK_ID, task), new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())))
), resultsIn(mClient,
new Assert<>(task,
new Composite<>(
@@ -703,23 +832,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())),
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3th instance (the overridden one). We don't have a row reference to this row, so we select it by the ORIGINAL_INSTANCE-ID
new AssertRelated<>(instancesTable, Tasks.ORIGINAL_INSTANCE_ID, task,
new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())))
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp())))
);
}
@@ -767,15 +896,15 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 4th instance (now 3rd):
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp()))*/)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp()))*/)
);
}
@@ -827,23 +956,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 4),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
);
}
@@ -900,23 +1029,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(localStart, localDue, new Present<>(start), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()))/*,
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())),
// 3rd instance:
// new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
// new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 2),
-// new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+// new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
);
}
@@ -977,23 +1106,23 @@ public class TaskProviderRecurrenceTest
// 1st instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, override,
new InstanceTestData(localStart, localDue, new Present<>(start), -1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())),
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp()))/*,
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
);
}
@@ -1061,23 +1190,23 @@ public class TaskProviderRecurrenceTest
// 1st instance, overridden and completed
new AssertRelated<>(instancesTable, Instances.TASK_ID, override,
new InstanceTestData(localStart, localDue, new Present<>(start), -1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())),
// 2nd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp()))/*,
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp()))/*,
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
);
}
@@ -1113,58 +1242,63 @@ public class TaskProviderRecurrenceTest
new Put<>(task,
new Composite<>(
new TimeData<>(start, due),
- new RDatesTaskData(new Seq<>(start, second, third, fourth, fifth)))),
+ new RDatesTaskData(start, second, third, fourth, fifth))),
// then complete the first instance
new BulkUpdate<>(instancesTable, new CharSequenceRowData<>(Tasks.STATUS, String.valueOf(Tasks.STATUS_COMPLETED)),
- new AllOf(
+ new AllOf<>(
new ReferringTo<>(Instances.TASK_ID, task),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())))
- ), resultsIn(mClient,
- new Assert<>(task,
- new Composite<>(
- new TimeData<>(start, due),
- new CharSequenceRowData<>(Tasks.RDATE,
- "20180104T123456Z," +
- "20180105T123456Z," +
- "20180106T123456Z," +
- "20180107T123456Z," +
- "20180108T123456Z"
- ))),
- // there must be one task which is not equal to the original task
- new Counted<>(1,
- new BulkAssert<>(tasksTable,
- new Composite<>(
- new TimeData<>(start, due),
- new StatusData<>(Tasks.STATUS_COMPLETED)),
- new Not(new ReferringTo<>(Tasks._ID, task)))),
- // and one instance which doesn't refer to the original task
- new Counted<>(1, new BulkAssert<>(instancesTable, new Not(new ReferringTo<>(Instances.TASK_ID, task)))),
- // but 4 instances of that original task
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp())))),
+ resultsIn(mClient,
+ // we've already closed the first instance which has been detached, the master now points to the second instance
+ new Counted<>(1,
+ new Assert<>(task,
+ new Composite<>(
+ new TimeData<>(DateTime.parse("20180105T123456Z"), DateTime.parse("20180105T133456Z")),
+ new RDatesTaskData(
+ // "20180104T123456Z" // the detached instance
+ DateTime.parse("20180105T123456Z"),
+ DateTime.parse("20180106T123456Z"),
+ DateTime.parse("20180107T123456Z"),
+ DateTime.parse("20180108T123456Z"))))),
+ // there must be one task which is not equal to the original task, it's the detached instance
+ new Counted<>(1,
+ new BulkAssert<>(tasksTable,
+ new Composite<>(
+ new TimeData<>(start, due),
+ new StatusData<>(Tasks.STATUS_COMPLETED),
+ new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_ID, null),
+ new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_SYNC_ID, null),
+ new CharSequenceRowData<>(Tasks.ORIGINAL_INSTANCE_TIME, null)),
+ new Not<>(new ReferringTo<>(Tasks._ID, task)))),
+ // and one instance which doesn't refer to the original task
+ new Counted<>(1, new BulkAssert<>(instancesTable, new Not<>(new ReferringTo<>(Instances.TASK_ID, task)))),
+ // but 4 instances of that original task
// new Counted<>(4, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)),
- new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)),
- // 1st instance, overridden and completed
- new Counted<>(1, new BulkAssert<>(instancesTable,
- new Composite<>(
- new InstanceTestData(localStart, localDue, new Present<>(start), -1)),
- new AllOf(
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, start.getTimestamp()),
- new Not(new ReferringTo<>(Instances.TASK_ID, task))))),
- // 2nd instance:
- new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
- new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp()))/*,
+ new Counted<>(1, new AssertRelated<>(instancesTable, Instances.TASK_ID, task)),
+ // 1st instance, detached and completed
+ new Counted<>(1, new BulkAssert<>(instancesTable,
+ new Composite<>(
+ new InstanceTestData(localStart, localDue, absent(), -1)),
+ new AllOf<>(
+ new IsNull<>(Instances.INSTANCE_ORIGINAL_TIME), // the detached instance has no INSTANCE_ORIGINAL_TIME
+ new Not<>(new ReferringTo<>(Instances.TASK_ID, task))))),
+ // 2nd instance:
+ new Counted<>(1,
+ new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
+ new InstanceTestData(second, second.addDuration(hour), new Present<>(second), 0),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, second.getTimestamp())))/*,
// 3rd instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(third, third.addDuration(hour), new Present<>(third), 1),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, third.getTimestamp())),
// 4th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fourth, fourth.addDuration(hour), new Present<>(fourth), 2),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fourth.getTimestamp())),
// 5th instance:
new AssertRelated<>(instancesTable, Instances.TASK_ID, task,
new InstanceTestData(fifth, fifth.addDuration(hour), new Present<>(fifth), 3),
- new EqArg(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
+ new EqArg<>(Instances.INSTANCE_ORIGINAL_TIME, fifth.getTimestamp()))*/)
);
}
diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRelatingTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRelatingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7595805fb478535e9e98848768913de87032f42
--- /dev/null
+++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderRelatingTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2020 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks;
+
+import android.accounts.Account;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Build;
+
+import org.dmfs.android.contentpal.Operation;
+import org.dmfs.android.contentpal.OperationsQueue;
+import org.dmfs.android.contentpal.RowSnapshot;
+import org.dmfs.android.contentpal.operations.Assert;
+import org.dmfs.android.contentpal.operations.BulkAssert;
+import org.dmfs.android.contentpal.operations.BulkDelete;
+import org.dmfs.android.contentpal.operations.Counted;
+import org.dmfs.android.contentpal.operations.Insert;
+import org.dmfs.android.contentpal.operations.Put;
+import org.dmfs.android.contentpal.predicates.AllOf;
+import org.dmfs.android.contentpal.predicates.EqArg;
+import org.dmfs.android.contentpal.predicates.ReferringTo;
+import org.dmfs.android.contentpal.queues.BasicOperationsQueue;
+import org.dmfs.android.contentpal.rowdata.CharSequenceRowData;
+import org.dmfs.android.contentpal.rowdata.Composite;
+import org.dmfs.android.contentpal.rowdata.Referring;
+import org.dmfs.android.contentpal.rowsnapshots.VirtualRowSnapshot;
+import org.dmfs.android.contentpal.tables.Synced;
+import org.dmfs.android.contenttestpal.operations.AssertEmptyTable;
+import org.dmfs.jems.iterable.elementary.Seq;
+import org.dmfs.opentaskspal.tables.InstanceTable;
+import org.dmfs.opentaskspal.tables.LocalTaskListsTable;
+import org.dmfs.opentaskspal.tables.PropertiesTable;
+import org.dmfs.opentaskspal.tables.TaskListScoped;
+import org.dmfs.opentaskspal.tables.TaskListsTable;
+import org.dmfs.opentaskspal.tables.TasksTable;
+import org.dmfs.opentaskspal.tasklists.NameData;
+import org.dmfs.opentaskspal.tasks.TitleData;
+import org.dmfs.tasks.contract.TaskContract;
+import org.dmfs.tasks.contract.TaskContract.TaskLists;
+import org.dmfs.tasks.contract.TaskContract.Tasks;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * Tests for {@link TaskProvider} reparenting feature.
+ *
+ * @author Marten Gajda
+ */
+@RunWith(AndroidJUnit4.class)
+public class TaskProviderRelatingTest
+{
+ private ContentResolver mResolver;
+ private String mAuthority;
+ private Context mContext;
+ private ContentProviderClient mClient;
+ private final Account testAccount = new Account("foo", "bar");
+
+
+ @Before
+ public void setUp() throws Exception
+ {
+ mContext = InstrumentationRegistry.getTargetContext();
+ mResolver = mContext.getContentResolver();
+ mAuthority = AuthorityUtil.taskAuthority(mContext);
+ mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority);
+
+ // Assert that tables are empty:
+ OperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq>(
+ new AssertEmptyTable<>(new TasksTable(mAuthority)),
+ new AssertEmptyTable<>(new TaskListsTable(mAuthority)),
+ new AssertEmptyTable<>(new PropertiesTable(mAuthority)),
+ new AssertEmptyTable<>(new InstanceTable(mAuthority))));
+ queue.flush();
+ }
+
+
+ @After
+ public void tearDown() throws Exception
+ {
+ /*
+ TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation
+ https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html
+ https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator
+ */
+
+ // Clear the DB:
+ BasicOperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq>(
+ new BulkDelete<>(new LocalTaskListsTable(mAuthority)),
+ new BulkDelete<>(new PropertiesTable(mAuthority)),
+ new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority)))));
+ queue.flush();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ {
+ mClient.close();
+ }
+ else
+ {
+ mClient.release();
+ }
+ }
+
+
+ /**
+ * Create 1 local task list, then create a child Task, related to a parent UID and finally the parent.
+ */
+ @Test
+ public void testRelateTask()
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new Synced<>(testAccount, new TaskListsTable(mAuthority)));
+ RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new Synced<>(testAccount, new TasksTable(mAuthority))));
+ RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new Synced<>(testAccount, new TasksTable(mAuthority))));
+
+ assertThat(new Seq<>(
+ new Put<>(taskList, new NameData("list1")),
+ new Put<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new CharSequenceRowData<>(Tasks._UID, "childUID"))),
+ new Insert<>(new PropertiesTable(mAuthority), new Composite<>(
+ new CharSequenceRowData<>(TaskContract.Property.Relation.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new Referring<>(TaskContract.Property.Relation.TASK_ID, taskChild),
+ new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_UID, "parentUID"),
+ new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT))
+ )),
+ new Put<>(taskParent, new Composite<>(
+ new TitleData("parent"),
+ new CharSequenceRowData<>(Tasks._UID, "parentUID")))
+ ),
+ resultsIn(mClient,
+ new Assert<>(taskList, new NameData("list1")),
+ new Assert<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new CharSequenceRowData<>(Tasks._UID, "childUID"),
+ new Referring<>(Tasks.PARENT_ID, taskParent))),
+ new Assert<>(taskParent, new Composite<>(
+ new CharSequenceRowData<>(Tasks._UID, "parentUID"),
+ new TitleData("parent"))),
+ new Counted<>(1, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new Composite<>(
+ new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE,
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)),
+ new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_UID, "parentUID"),
+ new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent)
+ ),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)
+ ))),
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)
+ )))
+ ));
+ }
+}
diff --git a/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderReparentingTest.java b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderReparentingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..57eedc1fda205bf5bc9dfe8d6d22362726f29d40
--- /dev/null
+++ b/opentasks-provider/src/androidTest/java/org/dmfs/provider/tasks/TaskProviderReparentingTest.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2020 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks;
+
+import android.accounts.Account;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Build;
+
+import org.dmfs.android.contentpal.Operation;
+import org.dmfs.android.contentpal.OperationsQueue;
+import org.dmfs.android.contentpal.RowSnapshot;
+import org.dmfs.android.contentpal.operations.Assert;
+import org.dmfs.android.contentpal.operations.BulkAssert;
+import org.dmfs.android.contentpal.operations.BulkDelete;
+import org.dmfs.android.contentpal.operations.Counted;
+import org.dmfs.android.contentpal.operations.Insert;
+import org.dmfs.android.contentpal.operations.Put;
+import org.dmfs.android.contentpal.predicates.AllOf;
+import org.dmfs.android.contentpal.predicates.EqArg;
+import org.dmfs.android.contentpal.predicates.ReferringTo;
+import org.dmfs.android.contentpal.queues.BasicOperationsQueue;
+import org.dmfs.android.contentpal.rowdata.CharSequenceRowData;
+import org.dmfs.android.contentpal.rowdata.Composite;
+import org.dmfs.android.contentpal.rowdata.Referring;
+import org.dmfs.android.contentpal.rowsnapshots.VirtualRowSnapshot;
+import org.dmfs.android.contentpal.tables.Synced;
+import org.dmfs.android.contenttestpal.operations.AssertEmptyTable;
+import org.dmfs.jems.iterable.elementary.Seq;
+import org.dmfs.opentaskspal.tables.InstanceTable;
+import org.dmfs.opentaskspal.tables.LocalTaskListsTable;
+import org.dmfs.opentaskspal.tables.PropertiesTable;
+import org.dmfs.opentaskspal.tables.TaskListScoped;
+import org.dmfs.opentaskspal.tables.TaskListsTable;
+import org.dmfs.opentaskspal.tables.TasksTable;
+import org.dmfs.opentaskspal.tasklists.NameData;
+import org.dmfs.opentaskspal.tasks.TitleData;
+import org.dmfs.tasks.contract.TaskContract;
+import org.dmfs.tasks.contract.TaskContract.TaskLists;
+import org.dmfs.tasks.contract.TaskContract.Tasks;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * Tests for {@link TaskProvider} reparenting feature.
+ *
+ * @author Marten Gajda
+ */
+@RunWith(AndroidJUnit4.class)
+public class TaskProviderReparentingTest
+{
+ private ContentResolver mResolver;
+ private String mAuthority;
+ private Context mContext;
+ private ContentProviderClient mClient;
+ private final Account testAccount = new Account("foo", "bar");
+
+
+ @Before
+ public void setUp() throws Exception
+ {
+ mContext = InstrumentationRegistry.getTargetContext();
+ mResolver = mContext.getContentResolver();
+ mAuthority = AuthorityUtil.taskAuthority(mContext);
+ mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority);
+
+ // Assert that tables are empty:
+ OperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq>(
+ new AssertEmptyTable<>(new TasksTable(mAuthority)),
+ new AssertEmptyTable<>(new TaskListsTable(mAuthority)),
+ new AssertEmptyTable<>(new PropertiesTable(mAuthority)),
+ new AssertEmptyTable<>(new InstanceTable(mAuthority))));
+ queue.flush();
+ }
+
+
+ @After
+ public void tearDown() throws Exception
+ {
+ /*
+ TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation
+ https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html
+ https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator
+ */
+
+ // Clear the DB:
+ BasicOperationsQueue queue = new BasicOperationsQueue(mClient);
+ queue.enqueue(new Seq>(
+ new BulkDelete<>(new LocalTaskListsTable(mAuthority)),
+ new BulkDelete<>(new PropertiesTable(mAuthority)),
+ new BulkDelete<>(new Synced<>(testAccount, new TaskListsTable(mAuthority)))));
+ queue.flush();
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ {
+ mClient.close();
+ }
+ else
+ {
+ mClient.release();
+ }
+ }
+
+
+ /**
+ * Create 1 local task list and a parent and a child task.
+ */
+ @Test
+ public void testRelateTask()
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority));
+ RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ assertThat(new Seq<>(
+ new Put<>(taskList, new NameData("list1")),
+ new Put<>(taskParent, new TitleData("parent")),
+ new Put<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new Referring<>(Tasks.PARENT_ID, taskParent)))
+ ),
+ resultsIn(mClient,
+ new Assert<>(taskList, new NameData("list1")),
+ new Assert<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new Referring<>(Tasks.PARENT_ID, taskParent))),
+ new Assert<>(taskParent, new Composite<>(
+ new TitleData("parent"))),
+ new Counted<>(1, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new Composite<>(
+ new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE,
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)),
+ new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent)
+ ),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)
+ ))),
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)
+ )))
+ ));
+ }
+
+
+ /**
+ * Create 1 local task list and 2 tasks, in a second operation make the second one parent of the first one.
+ */
+ @Test
+ public void testAdoptTask()
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority));
+ RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ assertThat(new Seq<>(
+ new Put<>(taskList, new NameData("list1")),
+ new Put<>(taskChild, new TitleData("child")),
+ new Put<>(taskParent, new TitleData("parent")),
+ new Put<>(taskChild, new Referring<>(Tasks.PARENT_ID, taskParent))
+ ),
+ resultsIn(mClient,
+ new Assert<>(taskList, new NameData("list1")),
+ new Assert<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new Referring<>(Tasks.PARENT_ID, taskParent))),
+ new Assert<>(taskParent, new Composite<>(
+ new TitleData("parent"))),
+ new Counted<>(1, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new Composite<>(
+ new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE,
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)),
+ new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskParent)
+ ),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)
+ ))),
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)
+ )))
+ ));
+ }
+
+
+ /**
+ * Create 1 local task list and 3 tasks, create parent child relationship and change it afterwards
+ */
+ @Test
+ public void testReparentTask()
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority));
+ RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskNewParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ assertThat(new Seq<>(
+ new Put<>(taskList, new NameData("list1")),
+ new Put<>(taskParent, new TitleData("parent")),
+ new Put<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new Referring<>(Tasks.PARENT_ID, taskParent))),
+ new Put<>(taskNewParent, new TitleData("newParent")),
+ new Put<>(taskChild, new Referring<>(Tasks.PARENT_ID, taskNewParent))
+ ),
+ resultsIn(mClient,
+ new Assert<>(taskList, new NameData("list1")),
+ new Assert<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new Referring<>(Tasks.PARENT_ID, taskNewParent))),
+ new Assert<>(taskParent, new Composite<>(
+ new TitleData("parent"))),
+ new Assert<>(taskNewParent, new Composite<>(
+ new TitleData("newParent"))),
+
+ new Counted<>(1, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new Composite<>(
+ new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE,
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)),
+ new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskNewParent)
+ ),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)
+ ))),
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)
+ ))),
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskNewParent)
+ )))
+ ));
+ }
+
+
+ /**
+ * Create 1 local task list and 4 tasks, create parent child relationship with a sibling and change it afterwards
+ */
+ @Test
+ public void testReparentTaskWithSibling()
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority));
+ RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskNewParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskSibling = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ assertThat(new Seq<>(
+ new Put<>(taskList, new NameData("list1")),
+ new Put<>(taskParent, new TitleData("parent")),
+ new Put<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new Referring<>(Tasks.PARENT_ID, taskParent))),
+ new Put<>(taskNewParent, new TitleData("newParent")),
+ new Put<>(taskSibling, new TitleData("sibling")),
+ new Insert<>(new PropertiesTable(mAuthority), new Composite<>(
+ new CharSequenceRowData<>(TaskContract.Property.Relation.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new Referring<>(TaskContract.Property.Relation.TASK_ID, taskSibling),
+ new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskChild),
+ new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE, String.valueOf(
+ TaskContract.Property.Relation.RELTYPE_SIBLING))
+ )),
+ new Put<>(taskChild, new Referring<>(Tasks.PARENT_ID, taskNewParent))
+ ),
+ resultsIn(mClient,
+ new Assert<>(taskList, new NameData("list1")),
+ new Assert<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new Referring<>(Tasks.PARENT_ID, taskNewParent))),
+ new Assert<>(taskParent, new Composite<>(
+ new TitleData("parent"))),
+ new Assert<>(taskNewParent, new Composite<>(
+ new TitleData("newParent"))),
+ new Assert<>(taskSibling, new Composite<>(
+ new TitleData("sibling"))),
+
+ new Counted<>(1, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new Composite<>(
+ new CharSequenceRowData<>(TaskContract.Property.Relation.RELATED_TYPE,
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT)),
+ new Referring<>(TaskContract.Property.Relation.RELATED_ID, taskNewParent)
+ ),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)
+ ))),
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)
+ ))),
+ // yikes the sibling became an orphan because it has no relation to its parent anymore.
+ // this should be fixed, see https://github.com/dmfs/opentasks/issues/932
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskSibling)
+ ))),
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskNewParent)
+ )))
+ ));
+ }
+
+
+ /**
+ * Create 1 local task list and 2 tasks, create parent child relationship and remove it
+ */
+ @Test
+ public void testOrphanTask()
+ {
+ RowSnapshot taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority));
+ RowSnapshot taskChild = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+ RowSnapshot taskParent = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
+
+ assertThat(new Seq<>(
+ new Put<>(taskList, new NameData("list1")),
+ new Put<>(taskParent, new TitleData("parent")),
+ new Put<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new Referring<>(Tasks.PARENT_ID, taskParent))),
+ new Put<>(taskChild, new CharSequenceRowData<>(Tasks.PARENT_ID, null))
+ ),
+ resultsIn(mClient,
+ new Assert<>(taskList, new NameData("list1")),
+ new Assert<>(taskChild, new Composite<>(
+ new TitleData("child"),
+ new CharSequenceRowData<>(Tasks.PARENT_ID, null))),
+ new Assert<>(taskParent, new Composite<>(
+ new TitleData("parent"))),
+
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskChild)
+ ))),
+ new Counted<>(0, new BulkAssert<>(
+ new PropertiesTable(mAuthority),
+ new AllOf<>(
+ new EqArg<>(TaskContract.Properties.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE),
+ new ReferringTo<>(TaskContract.Properties.TASK_ID, taskParent)
+ )))
+ ));
+ }
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java
index fcdeebc10cbf7ae1a6e61c232717f756fe949c34..a78d8c76d7a556d29c5de49114587eeaa053aac2 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java
@@ -71,7 +71,7 @@ public class TaskDatabaseHelper extends SQLiteOpenHelper
/**
* The database version.
*/
- private static final int DATABASE_VERSION = 22;
+ private static final int DATABASE_VERSION = 23;
/**
@@ -197,6 +197,8 @@ public class TaskDatabaseHelper extends SQLiteOpenHelper
+ "null as " + Tasks.RRULE + ", "
+ "null as " + Tasks.RDATE + ", "
+ "null as " + Tasks.EXDATE + ", "
+ // this instance is part of a recurring task if either it has recurrence values or overrides an instance
+ + "not (" + Tasks.RRULE + " is null and " + Tasks.RDATE + " is null and " + Tasks.ORIGINAL_INSTANCE_ID + " is null and " + Tasks.ORIGINAL_INSTANCE_SYNC_ID + " is null) as " + TaskContract.Instances.IS_RECURRING + ", "
+ Tables.TASKS + ".*, "
+ Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", "
+ Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", "
@@ -875,6 +877,12 @@ public class TaskDatabaseHelper extends SQLiteOpenHelper
}
}
+ if (oldVersion < 23)
+ {
+ db.execSQL("drop view " + Tables.INSTANCE_CLIENT_VIEW + ";");
+ 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 6b0e2ff29892fd059e9e533e904d2879dc742404..d78d9a3ad7cfefac1da77574ec637720e82c49ed 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
@@ -52,6 +52,7 @@ import org.dmfs.provider.tasks.model.InstanceAdapter;
import org.dmfs.provider.tasks.model.ListAdapter;
import org.dmfs.provider.tasks.model.TaskAdapter;
import org.dmfs.provider.tasks.processors.EntityProcessor;
+import org.dmfs.provider.tasks.processors.instances.Detaching;
import org.dmfs.provider.tasks.processors.instances.TaskValueDelegate;
import org.dmfs.provider.tasks.processors.lists.ListCommitProcessor;
import org.dmfs.provider.tasks.processors.tasks.AutoCompleting;
@@ -59,6 +60,7 @@ import org.dmfs.provider.tasks.processors.tasks.Instantiating;
import org.dmfs.provider.tasks.processors.tasks.Moving;
import org.dmfs.provider.tasks.processors.tasks.Originating;
import org.dmfs.provider.tasks.processors.tasks.Relating;
+import org.dmfs.provider.tasks.processors.tasks.Reparenting;
import org.dmfs.provider.tasks.processors.tasks.Searchable;
import org.dmfs.provider.tasks.processors.tasks.TaskCommitProcessor;
import org.dmfs.provider.tasks.processors.tasks.Validating;
@@ -185,11 +187,12 @@ public final class TaskProvider extends SQLiteContentProvider implements OnAccou
mAuthority = AuthorityUtil.taskAuthority(getContext());
mTaskProcessorChain = new Validating(
- new AutoCompleting(new Relating(new Instantiating(new Searchable(new Moving(new Originating(new TaskCommitProcessor())))))));
+ new AutoCompleting(new Relating(new Reparenting(new Instantiating(new Searchable(new Moving(new Originating(new TaskCommitProcessor()))))))));
mListProcessorChain = new org.dmfs.provider.tasks.processors.lists.Validating(new ListCommitProcessor());
- mInstanceProcessorChain = new org.dmfs.provider.tasks.processors.instances.Validating(new TaskValueDelegate(mTaskProcessorChain));
+ mInstanceProcessorChain = new org.dmfs.provider.tasks.processors.instances.Validating(
+ new Detaching(new TaskValueDelegate(mTaskProcessorChain), mTaskProcessorChain));
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(mAuthority, TaskContract.TaskLists.CONTENT_URI_PATH, LISTS);
@@ -513,7 +516,7 @@ public final class TaskProvider extends SQLiteContentProvider implements OnAccou
{
if (sb.length() > 0)
{
- sb.append("AND ( ").append(selection).append(" ) ");
+ sb.append(" AND ( ").append(selection).append(" ) ");
}
else
{
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java
index 1821965995ce34786d0ebc67652a879520a9ef5d..6213cb378f3816b85b9ec1d68f4e5a06f7cdde9e 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java
@@ -98,7 +98,14 @@ public class CursorContentValuesInstanceAdapter extends AbstractInstanceAdapter
@Override
public boolean isUpdated(FieldAdapter, InstanceAdapter> fieldAdapter)
{
- return mValues != null && fieldAdapter.isSetIn(mValues);
+ if (mValues == null || !fieldAdapter.isSetIn(mValues))
+ {
+ return false;
+ }
+ Object oldValue = fieldAdapter.getFrom(mCursor);
+ Object newValue = fieldAdapter.getFrom(mValues);
+
+ return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue);
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java
index 76fa73152ecfdb815773fb5a661dbcafc4a803c4..4bdffb58d0d0701a66185a40253969da7cb182ff 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java
@@ -68,7 +68,14 @@ public class CursorContentValuesListAdapter extends AbstractListAdapter
@Override
public boolean isUpdated(FieldAdapter, ListAdapter> fieldAdapter)
{
- return mValues != null && fieldAdapter.isSetIn(mValues);
+ if (mValues == null || !fieldAdapter.isSetIn(mValues))
+ {
+ return false;
+ }
+ Object oldValue = fieldAdapter.getFrom(mCursor);
+ Object newValue = fieldAdapter.getFrom(mValues);
+
+ return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue);
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java
index d6c49331ab998d82d48197c2fa5d02c35f43bff6..0ed20dfcdaa0858476ad14baeb0e15afb2c44cad 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java
@@ -90,7 +90,22 @@ public class CursorContentValuesTaskAdapter extends AbstractTaskAdapter
@Override
public boolean isUpdated(FieldAdapter, TaskAdapter> fieldAdapter)
{
- return mValues != null && fieldAdapter.isSetIn(mValues);
+ if (mValues == null || !fieldAdapter.isSetIn(mValues))
+ {
+ return false;
+ }
+ Object oldValue = fieldAdapter.existsIn(mCursor) ? fieldAdapter.getFrom(mCursor) : null;
+ Object newValue = fieldAdapter.getFrom(mValues);
+ // we need to special case RRULE, because RecurrenceRule doesn't support `equals`
+ if (fieldAdapter != TaskAdapter.RRULE)
+ {
+ return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue);
+ }
+ else
+ {
+ // in case of RRULE we compare the String values.
+ return oldValue == null && newValue != null || oldValue != null && (newValue == null || !oldValue.toString().equals(newValue.toString()));
+ }
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java
index c4a1eba2d60a781817eee3271f6cdf4b167c3db0..4668cce310995396909be1fff987fde1f512e030 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java
@@ -81,6 +81,11 @@ public interface TaskAdapter extends EntityAdapter
*/
BooleanFieldAdapter ORIGINAL_INSTANCE_ALLDAY = new BooleanFieldAdapter(Tasks.ORIGINAL_INSTANCE_ALLDAY);
+ /**
+ * Adapter for the parent_id of a task.
+ */
+ LongFieldAdapter PARENT_ID = new LongFieldAdapter(Tasks.PARENT_ID);
+
/**
* Adapter for the all day flag of a task.
*/
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java
index 878b6c3a6bc3a900626a99cbbc3628bb07f73371..5c9159bef875713fe98c7aa7d3150ac844ee3adb 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java
@@ -89,9 +89,8 @@ public final class DateTimeFieldAdapter extends SimpleFieldAdapter extends SimpleFieldAdapter extends SimpleFieldAdapter implements Field
public boolean existsIn(Cursor cursor)
{
int columnIdx = cursor.getColumnIndex(fieldName());
- if (columnIdx < 0)
- {
- throw new IllegalArgumentException("The column '" + fieldName() + "' is missing in cursor.");
- }
-
- return !cursor.isNull(columnIdx);
+ return columnIdx >= 0 && !cursor.isNull(columnIdx);
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c98fd048a17b08f74cbdd86187eb9011a73ec43
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright 2019 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.instances;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+
+import org.dmfs.iterables.SingletonIterable;
+import org.dmfs.iterables.decorators.Sieved;
+import org.dmfs.jems.iterable.composite.Joined;
+import org.dmfs.jems.optional.adapters.FirstPresent;
+import org.dmfs.jems.optional.elementary.NullSafe;
+import org.dmfs.jems.predicate.composite.AnyOf;
+import org.dmfs.jems.predicate.composite.Not;
+import org.dmfs.provider.tasks.TaskDatabaseHelper;
+import org.dmfs.provider.tasks.model.CursorContentValuesInstanceAdapter;
+import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter;
+import org.dmfs.provider.tasks.model.InstanceAdapter;
+import org.dmfs.provider.tasks.model.TaskAdapter;
+import org.dmfs.provider.tasks.model.adapters.IntegerFieldAdapter;
+import org.dmfs.provider.tasks.model.adapters.LongFieldAdapter;
+import org.dmfs.provider.tasks.processors.EntityProcessor;
+import org.dmfs.provider.tasks.utils.Timestamps;
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.rfc5545.Duration;
+import org.dmfs.rfc5545.recur.RecurrenceRule;
+import org.dmfs.rfc5545.recurrenceset.RecurrenceList;
+import org.dmfs.rfc5545.recurrenceset.RecurrenceRuleAdapter;
+import org.dmfs.rfc5545.recurrenceset.RecurrenceSet;
+import org.dmfs.rfc5545.recurrenceset.RecurrenceSetIterator;
+import org.dmfs.tasks.contract.TaskContract;
+
+import java.util.HashSet;
+import java.util.TimeZone;
+
+import static java.util.Arrays.asList;
+
+
+/**
+ * An instance {@link EntityProcessor} detaches completed instances at the start of a recurring task.
+ *
+ * @author Marten Gajda
+ */
+public final class Detaching implements EntityProcessor
+{
+
+ private final EntityProcessor mDelegate;
+ private final EntityProcessor mTaskDelegate;
+
+
+ public Detaching(EntityProcessor delegate, EntityProcessor taskDelegate)
+ {
+ mDelegate = delegate;
+ mTaskDelegate = taskDelegate;
+ }
+
+
+ @Override
+ public InstanceAdapter insert(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
+ {
+ // just delegate for now
+ // if we ever support inserting instances, we'll have to make sure that inserting a completed instance results in a detached task
+ return mDelegate.insert(db, entityAdapter, isSyncAdapter);
+ }
+
+
+ /**
+ * Detach the given instance if all of the following conditions are met
+ *
+ * - The instance is a recurrence instance (INSTANCE_ORIGINAL_TIME != null)
+ * - and the task has been closed (IS_CLOSED != 0)
+ * - and the instance is the first non-closed instance (DISTANCE_FROM_CURRENT==0).
+ *
+ */
+ @Override
+ public InstanceAdapter update(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
+ {
+ if (entityAdapter.valueOf(InstanceAdapter.DISTANCE_FROM_CURRENT) != 0 // not the first open task
+
+ // not closed, note we can't use IS_CLOSED at this point because its not updated yet
+ || (!new HashSet<>(asList(TaskContract.Tasks.STATUS_COMPLETED, TaskContract.Tasks.STATUS_CANCELLED)).contains(
+ entityAdapter.valueOf(new IntegerFieldAdapter<>(TaskContract.Tasks.STATUS))))
+
+ // not recurring
+ || entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME) == null)
+ {
+ // not a detachable instance
+ return mDelegate.update(db, entityAdapter, isSyncAdapter);
+ }
+ // update instance accordingly and detach it
+ return detachAll(db, mDelegate.update(db, entityAdapter, isSyncAdapter));
+ }
+
+
+ @Override
+ public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
+ {
+ // just delegate
+ mDelegate.delete(db, entityAdapter, isSyncAdapter);
+ }
+
+
+ /**
+ * Detach all closed instances preceding the given one.
+ *
+ * TODO: this method needs some refactoring
+ */
+ private InstanceAdapter detachAll(SQLiteDatabase db, InstanceAdapter entityAdapter)
+ {
+ // keep some values for later
+ long masterId = new FirstPresent<>(
+ new NullSafe<>(entityAdapter.valueOf(new LongFieldAdapter<>(TaskContract.Instances.ORIGINAL_INSTANCE_ID))),
+ new NullSafe<>(entityAdapter.valueOf(new LongFieldAdapter<>(TaskContract.Instances.TASK_ID)))).value();
+ DateTime instanceOriginalTime = entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME);
+
+ // detach instances which are completed
+ try (Cursor instances = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW,
+ null,
+ String.format("%s < 0 and %s == ?", TaskContract.Instances.DISTANCE_FROM_CURRENT, TaskContract.Instances.ORIGINAL_INSTANCE_ID),
+ new String[] { String.valueOf(masterId) },
+ null,
+ null,
+ null))
+ {
+ while (instances.moveToNext())
+ {
+ detachSingle(db, new CursorContentValuesInstanceAdapter(instances, new ContentValues()));
+ }
+ }
+
+ // move the master to the first incomplete task
+ try (Cursor task = db.query(TaskDatabaseHelper.Tables.TASKS_VIEW,
+ null,
+ String.format("%s == ?", TaskContract.Tasks._ID),
+ new String[] { String.valueOf(masterId) },
+ null,
+ null,
+ null))
+ {
+ if (task.moveToFirst())
+ {
+ TaskAdapter masterTask = new CursorContentValuesTaskAdapter(task, new ContentValues());
+ DateTime oldStart = new FirstPresent<>(
+ new NullSafe<>(masterTask.valueOf(TaskAdapter.DTSTART)),
+ new NullSafe<>(masterTask.valueOf(TaskAdapter.DUE))).value();
+
+ // assume we have no instances left
+ boolean noInstances = true;
+
+ // update RRULE, if existent
+ RecurrenceRule rule = masterTask.valueOf(TaskAdapter.RRULE);
+ int count = 0;
+ if (rule != null)
+ {
+ RecurrenceSet ruleSet = new RecurrenceSet();
+ ruleSet.addInstances(new RecurrenceRuleAdapter(rule));
+ if (rule.getCount() == null)
+ {
+ // rule has no count limit, allowing us to exclude exdates
+ ruleSet.addExceptions(new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.EXDATE)).value()));
+ }
+ RecurrenceSetIterator ruleIterator = ruleSet.iterator(
+ oldStart.getTimeZone(),
+ oldStart.getTimestamp());
+
+ // move DTSTART to next RRULE instance which is > instanceOriginalTime
+ // reduce COUNT by the number of skipped instances, if present
+ while (count < 1000 && ruleIterator.hasNext())
+ {
+ DateTime inst = new DateTime(oldStart.getTimeZone(), ruleIterator.next());
+ if (instanceOriginalTime.before(inst))
+ {
+ updateStart(masterTask, inst);
+ noInstances = false; // just found another instance
+ break;
+ }
+ count += 1;
+ }
+
+ if (noInstances)
+ {
+ // remove the RRULE but keep a mask for the old start
+ masterTask.set(TaskAdapter.EXDATE,
+ new Joined<>(new SingletonIterable<>(oldStart), new Sieved<>(new Not<>(oldStart::equals), masterTask.valueOf(TaskAdapter.EXDATE))));
+ masterTask.set(TaskAdapter.RRULE, null);
+ }
+ else
+ {
+ // adjust COUNT if present
+ if (rule.getCount() != null)
+ {
+ rule.setCount(rule.getCount() - count);
+ masterTask.set(TaskAdapter.RRULE, rule);
+ }
+ }
+ }
+
+ DateTime newStart = new FirstPresent<>(
+ new NullSafe<>(masterTask.valueOf(TaskAdapter.DTSTART)),
+ new NullSafe<>(masterTask.valueOf(TaskAdapter.DUE))).value();
+
+ // update RDATE and EXDATE
+ masterTask.set(TaskAdapter.RDATE, new Sieved<>(instanceOriginalTime::before, masterTask.valueOf(TaskAdapter.RDATE)));
+ masterTask.set(TaskAdapter.EXDATE,
+ new Sieved<>(new AnyOf<>(instanceOriginalTime::before, newStart::equals), masterTask.valueOf(TaskAdapter.EXDATE)));
+
+ // First check if we still have any RDATE instances left
+ // TODO: 6 lines for something we should be able to express in one simple expression, we need to straighten lib-recur!!
+ RecurrenceSet rdateSet = new RecurrenceSet();
+ rdateSet.addInstances(new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.RDATE)).value()));
+ rdateSet.addExceptions(new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.EXDATE)).value()));
+ RecurrenceSetIterator iterator = rdateSet.iterator(DateTime.UTC, Long.MIN_VALUE);
+ iterator.fastForward(Long.MIN_VALUE + 1); // skip bogus start
+ noInstances &= !iterator.hasNext();
+
+ if (noInstances)
+ {
+ // no more instances left, remove the master
+ mTaskDelegate.delete(db, masterTask, false);
+ }
+ else
+ {
+ if (masterTask.valueOf(TaskAdapter.RRULE) == null)
+ {
+ // we don't have any RRULE, allowing us to adjust DTSTART/DUE to the first RDATE
+ DateTime start = new DateTime(iterator.next());
+ if (masterTask.valueOf(TaskAdapter.IS_ALLDAY))
+ {
+ start = start.toAllDay();
+ }
+ else if (masterTask.valueOf(TaskAdapter.TIMEZONE_RAW) != null)
+ {
+ start = start.shiftTimeZone(TimeZone.getTimeZone(masterTask.valueOf(TaskAdapter.TIMEZONE_RAW)));
+ }
+ updateStart(masterTask, start);
+ }
+
+ // we still have instances, update the database
+ mTaskDelegate.update(db, masterTask, false);
+ }
+ }
+ }
+
+ return entityAdapter;
+ }
+
+
+ private void updateStart(TaskAdapter task, DateTime newStart)
+ {
+ // this new instance becomes the new start (or due if we don't have a start)
+ if (task.valueOf(TaskAdapter.DTSTART) != null)
+ {
+ DateTime oldStart = task.valueOf(TaskAdapter.DTSTART);
+ task.set(TaskAdapter.DTSTART, newStart);
+ if (task.valueOf(TaskAdapter.DUE) != null)
+ {
+ long duration = task.valueOf(TaskAdapter.DUE).getTimestamp() - oldStart.getTimestamp();
+ task.set(TaskAdapter.DUE,
+ newStart.addDuration(
+ new Duration(1, (int) (duration / (3600 * 24 * 1000)), (int) (duration % (3600 * 24 * 1000)) / 1000)));
+ }
+ }
+ else
+ {
+ task.set(TaskAdapter.DUE, newStart);
+ }
+
+ }
+
+
+ /**
+ * Detach the given instance.
+ *
+ * - clone the override into a new deleted task (set _DELETED == 1)
+ * - detach the original override by removing the ORIGINAL_INSTANCE_ID, ORIGINAL_INSTANCE_SYNC_ID, ORIGINAL_INSTANCE_START and ORIGINAL_INSTANCE_ALLDAY
+ * (i.e. all columns which relate this to the original)
+ * - wipe _SYNC_ID, _UID and all sync columns (make this an unsynced task)
+ */
+ private void detachSingle(SQLiteDatabase db, InstanceAdapter entityAdapter)
+ {
+ TaskAdapter original = entityAdapter.taskAdapter();
+ TaskAdapter cloneAdapter = original.duplicate();
+
+ // first prepare the original to resemble the same instance but as a new, detached task
+ original.set(TaskAdapter.SYNC_ID, null);
+ original.set(TaskAdapter.SYNC_VERSION, null);
+ original.set(TaskAdapter.SYNC1, null);
+ original.set(TaskAdapter.SYNC2, null);
+ original.set(TaskAdapter.SYNC3, null);
+ original.set(TaskAdapter.SYNC4, null);
+ original.set(TaskAdapter.SYNC5, null);
+ original.set(TaskAdapter.SYNC6, null);
+ original.set(TaskAdapter.SYNC7, null);
+ original.set(TaskAdapter.SYNC8, null);
+ original.set(TaskAdapter._UID, null);
+ original.set(TaskAdapter._DIRTY, true);
+ original.set(TaskAdapter.ORIGINAL_INSTANCE_ID, null);
+ original.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, null);
+ original.set(TaskAdapter.ORIGINAL_INSTANCE_TIME, null);
+ original.unset(TaskAdapter.COMPLETED);
+ original.commit(db);
+
+ // wipe INSTANCE_ORIGINAL_TIME from instances entry
+ ContentValues noOriginalTime = new ContentValues();
+ noOriginalTime.putNull(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
+ db.update(TaskDatabaseHelper.Tables.INSTANCES, noOriginalTime, "_ID = ?", new String[] { String.valueOf(entityAdapter.id()) });
+
+ // reset the clone to be a deleted instance
+ cloneAdapter.set(TaskAdapter._DELETED, true);
+ // remove joined field values
+ cloneAdapter.unset(TaskAdapter.LIST_ACCESS_LEVEL);
+ cloneAdapter.unset(TaskAdapter.LIST_COLOR);
+ cloneAdapter.unset(TaskAdapter.LIST_NAME);
+ cloneAdapter.unset(TaskAdapter.LIST_OWNER);
+ cloneAdapter.unset(TaskAdapter.LIST_VISIBLE);
+ cloneAdapter.unset(TaskAdapter.ACCOUNT_NAME);
+ cloneAdapter.unset(TaskAdapter.ACCOUNT_TYPE);
+ cloneAdapter.commit(db);
+
+ // note, we don't have to create an instance for the clone because it's deleted
+ }
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java
index 02c502e2d065ca608bdb53b27fe75dcf9d184be2..f02ed1da305bfb024c40d267cc02b0e2b3d9f4d0 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java
@@ -80,7 +80,9 @@ public final class TaskValueDelegate implements EntityProcessor
// also unset any recurrence fields
TaskAdapter.RRULE,
TaskAdapter.RDATE,
- TaskAdapter.EXDATE
+ TaskAdapter.EXDATE,
+ TaskAdapter.CREATED,
+ TaskAdapter.LAST_MODIFIED
);
private final EntityProcessor mDelegate;
@@ -184,8 +186,7 @@ public final class TaskValueDelegate implements EntityProcessor
// copy original instance allday flag
override.set(TaskAdapter.ORIGINAL_INSTANCE_ALLDAY, taskAdapter.valueOf(TaskAdapter.IS_ALLDAY));
- // TODO: if this is the first instance (and maybe no other overrides exist), don't create an override but split the series into two tasks
- TaskAdapter newTask = mDelegate.insert(db, override, true /* for now insert as a sync adapter to retain the UID */);
+ TaskAdapter newTask = mDelegate.insert(db, override, false);
copyProperties(db, taskAdapter.id(), newTask.id());
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java
index 9b8619e39c6f52ed848889122d815b82f3bce151..65bbe9ade34cb49d5856cee05509156932c5c7e7 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java
@@ -128,8 +128,7 @@ public final class AutoCompleting implements EntityProcessor
if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID))
{
String[] syncId = { task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) };
- Cursor cursor = db.query(TaskDatabaseHelper.Tables.TASKS, TASK_ID_PROJECTION, SYNC_ID_SELECTION, syncId, null, null, null);
- try
+ try (Cursor cursor = db.query(TaskDatabaseHelper.Tables.TASKS, TASK_ID_PROJECTION, SYNC_ID_SELECTION, syncId, null, null, null))
{
if (cursor.moveToNext())
{
@@ -137,19 +136,11 @@ public final class AutoCompleting implements EntityProcessor
task.set(TaskAdapter.ORIGINAL_INSTANCE_ID, originalId);
}
}
- finally
- {
- if (cursor != null)
- {
- cursor.close();
- }
- }
}
else if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID)) // Find corresponding ORIGINAL_INSTANCE_SYNC_ID
{
String[] id = { Long.toString(task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID)) };
- Cursor cursor = db.query(TaskDatabaseHelper.Tables.TASKS, TASK_SYNC_ID_PROJECTION, TASK_ID_SELECTION, id, null, null, null);
- try
+ try (Cursor cursor = db.query(TaskDatabaseHelper.Tables.TASKS, TASK_SYNC_ID_PROJECTION, TASK_ID_SELECTION, id, null, null, null))
{
if (cursor.moveToNext())
{
@@ -157,13 +148,6 @@ public final class AutoCompleting implements EntityProcessor
task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, originalSyncId);
}
}
- finally
- {
- if (cursor != null)
- {
- cursor.close();
- }
- }
}
// check that PERCENT_COMPLETE is an Integer between 0 and 100 if supplied also update status and completed accordingly
@@ -202,8 +186,8 @@ public final class AutoCompleting implements EntityProcessor
task.set(TaskAdapter.STATUS, status);
}
- task.set(TaskAdapter.IS_NEW, status == null || status == TaskContract.Tasks.STATUS_NEEDS_ACTION);
- task.set(TaskAdapter.IS_CLOSED, status != null && (status == TaskContract.Tasks.STATUS_COMPLETED || status == TaskContract.Tasks.STATUS_CANCELLED));
+ task.set(TaskAdapter.IS_NEW, status == TaskContract.Tasks.STATUS_NEEDS_ACTION);
+ task.set(TaskAdapter.IS_CLOSED, status == TaskContract.Tasks.STATUS_COMPLETED || status == TaskContract.Tasks.STATUS_CANCELLED);
/*
* Update PERCENT_COMPLETE and COMPLETED (if not given). Sync adapters should know what they're doing, so don't update anything if caller is a sync
@@ -212,7 +196,7 @@ public final class AutoCompleting implements EntityProcessor
if (status == TaskContract.Tasks.STATUS_COMPLETED && !isSyncAdapter)
{
task.set(TaskAdapter.PERCENT_COMPLETE, 100);
- if (!task.isUpdated(TaskAdapter.COMPLETED))
+ if (!task.isUpdated(TaskAdapter.COMPLETED) || task.valueOf(TaskAdapter.COMPLETED) == null)
{
task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis()));
}
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 c2a18fd7047933b8f642b8adcadeac8479311dbc..1891ff381bc14e7265a5f1baa29f593cae860a8b 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
@@ -20,6 +20,7 @@ import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import org.dmfs.jems.function.elementary.DiffMap;
import org.dmfs.jems.iterable.composite.Diff;
import org.dmfs.jems.iterable.decorators.Mapped;
import org.dmfs.jems.optional.Optional;
@@ -33,14 +34,17 @@ import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter;
import org.dmfs.provider.tasks.model.TaskAdapter;
import org.dmfs.provider.tasks.model.adapters.BooleanFieldAdapter;
import org.dmfs.provider.tasks.processors.EntityProcessor;
-import org.dmfs.provider.tasks.processors.tasks.instancedata.TaskRelated;
import org.dmfs.provider.tasks.utils.InstanceValuesIterable;
import org.dmfs.provider.tasks.utils.Limited;
+import org.dmfs.provider.tasks.utils.OverrideValuesFunction;
import org.dmfs.provider.tasks.utils.Range;
+import org.dmfs.provider.tasks.utils.RowIterator;
import org.dmfs.tasks.contract.TaskContract;
import java.util.Locale;
+import static org.dmfs.provider.tasks.model.TaskAdapter.IS_CLOSED;
+
/**
* A processor that creates or updates the instance values of a task.
@@ -49,6 +53,19 @@ import java.util.Locale;
*/
public final class Instantiating implements EntityProcessor
{
+ /**
+ * Projection we use to read the overrides of a task
+ */
+ private final static String[] OVERRIDE_PROJECTION = {
+ TaskContract.Tasks._ID,
+ TaskContract.Tasks.DTSTART,
+ TaskContract.Tasks.DUE,
+ TaskContract.Tasks.DURATION,
+ TaskContract.Tasks.TZ,
+ TaskContract.Tasks.IS_ALLDAY,
+ TaskContract.Tasks.IS_CLOSED,
+ TaskContract.Tasks.ORIGINAL_INSTANCE_TIME,
+ TaskContract.Tasks.ORIGINAL_INSTANCE_ALLDAY };
/**
* This is a field adapter for a pseudo column to indicate that the instances may need an update, even if no relevant value has changed. This is useful to
@@ -113,7 +130,7 @@ public final class Instantiating implements EntityProcessor
if (!result.isUpdated(TaskAdapter.DTSTART) && !result.isUpdated(TaskAdapter.DUE) && !result.isUpdated(TaskAdapter.DURATION)
&& !result.isUpdated(TaskAdapter.STATUS) && !result.isUpdated(TaskAdapter.RDATE) && !result.isUpdated(TaskAdapter.RRULE) && !result.isUpdated(
- TaskAdapter.EXDATE) && !result.isUpdated(TaskAdapter.IS_CLOSED) && !updateRequested)
+ TaskAdapter.EXDATE) && !result.isUpdated(IS_CLOSED) && !updateRequested)
{
// date values didn't change and update not requested -> no need to update the instances table
return result;
@@ -154,46 +171,43 @@ public final class Instantiating implements EntityProcessor
{
long origId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID);
int count = 0;
- for (Single values : new InstanceValuesIterable(taskAdapter))
+ if (!taskAdapter.isUpdated(IS_CLOSED))
{
- if (count++ > 1)
+ // task status was not updated, we can take the shortcut and only update any existing instance values
+ for (Single values : new InstanceValuesIterable(id, taskAdapter))
{
- throw new RuntimeException("more than one instance returned for task which was supposed to have exactly one");
- }
- try (Cursor c = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, new String[] { TaskContract.Instances._ID },
- String.format(Locale.ENGLISH, "(%s = %d or %s = %d) and (%s = %d) ",
- TaskContract.Instances.TASK_ID,
- origId,
- TaskContract.Instances.ORIGINAL_INSTANCE_ID,
- origId,
- TaskContract.Instances.INSTANCE_ORIGINAL_TIME,
- taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME).getTimestamp()),
- null, null, null, null))
- {
- if (c.moveToFirst())
+ if (count++ > 1)
{
- db.update(TaskDatabaseHelper.Tables.INSTANCES, new TaskRelated(id, values).value(), String.format(Locale.ENGLISH, "%s = %d",
- TaskContract.Instances._ID, c.getLong(0)), null);
- }
- else
- {
- db.insert(TaskDatabaseHelper.Tables.INSTANCES, "", new TaskRelated(id, values).value());
+ throw new RuntimeException("more than one instance returned for task instance which was supposed to have exactly one");
}
+ ContentValues contentValues = values.value();
+ // we don't know the current distance, but it for sure hasn't changed either, so just make sure we don't change it
+ contentValues.remove(TaskContract.Instances.DISTANCE_FROM_CURRENT);
+ // TASK_ID hasn't changed either
+ contentValues.remove(TaskContract.Instances.TASK_ID);
+
+ db.update(TaskDatabaseHelper.Tables.INSTANCES,
+ contentValues,
+ String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances.TASK_ID, id),
+ null);
+ }
+ if (count == 0)
+ {
+ throw new RuntimeException("no instance returned for task which was supposed to have exactly one");
}
}
- if (count == 0)
- {
- throw new RuntimeException("no instance returned for task which was supposed to have exactly one");
- }
-
- // ensure the distance from current is set properly for all sibling instances
- try (Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null,
- String.format(Locale.ENGLISH, "(%s = %d)", TaskContract.Tasks._ID, origId), null, null, null, null))
+ else
{
- if (c.moveToFirst())
+ // task status was updated, this might affect other instances, update them all
+ // ensure the distance from current is set properly for all sibling instances
+ try (Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null,
+ String.format(Locale.ENGLISH, "(%s = %d)", TaskContract.Tasks._ID, origId), null, null, null, null))
{
- TaskAdapter ta = new CursorContentValuesTaskAdapter(c, new ContentValues());
- updateMasterInstances(db, ta, ta.id());
+ if (c.moveToFirst())
+ {
+ TaskAdapter ta = new CursorContentValuesTaskAdapter(c, new ContentValues());
+ updateMasterInstances(db, ta, ta.id());
+ }
}
}
}
@@ -201,8 +215,6 @@ public final class Instantiating implements EntityProcessor
/**
* Updates the instances of an existing task
- *
- * TODO: take instance overrides into account
*
* @param db
* An {@link SQLiteDatabase}.
@@ -213,24 +225,39 @@ public final class Instantiating implements EntityProcessor
*/
private void updateMasterInstances(SQLiteDatabase db, TaskAdapter taskAdapter, long id)
{
- final Cursor existingInstances = db.query(
+ try (Cursor existingInstances = db.query(
TaskDatabaseHelper.Tables.INSTANCE_VIEW,
new String[] {
- TaskContract.Instances._ID, TaskContract.Instances.INSTANCE_ORIGINAL_TIME, TaskContract.Instances.TASK_ID,
- TaskContract.Instances.IS_CLOSED, TaskContract.Instances.DISTANCE_FROM_CURRENT },
- String.format(Locale.ENGLISH, "%s = %d or %s = %d", TaskContract.Instances.TASK_ID, id, TaskContract.Instances.ORIGINAL_INSTANCE_ID, id),
- null,
+ TaskContract.Instances._ID,
+ TaskContract.InstanceColumns.INSTANCE_ORIGINAL_TIME,
+ TaskContract.InstanceColumns.INSTANCE_START,
+ TaskContract.InstanceColumns.INSTANCE_START_SORTING,
+ TaskContract.InstanceColumns.INSTANCE_DUE,
+ TaskContract.InstanceColumns.INSTANCE_DUE_SORTING,
+ TaskContract.InstanceColumns.INSTANCE_DURATION,
+ TaskContract.InstanceColumns.TASK_ID,
+ TaskContract.InstanceColumns.DISTANCE_FROM_CURRENT,
+ TaskContract.Instances.IS_CLOSED },
+ String.format(Locale.ENGLISH, "%s = ? or %s = ?", TaskContract.Instances.TASK_ID, TaskContract.Instances.ORIGINAL_INSTANCE_ID),
+ new String[] { Long.toString(id), Long.toString(id) },
null,
null,
TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
-
- /*
- * The goal of the code below is to update existing instances in place (as opposed to delete and recreate all instances). We do this for two reasons:
- * 1) efficiency, in most cases existing instances don't change, deleting and recreating them would be overly expensive
- * 2) stable row ids, deleting and recreating instances would change their id and void any existing URIs to them
- */
- try
+ Cursor overrides = db.query(
+ TaskDatabaseHelper.Tables.TASKS,
+ OVERRIDE_PROJECTION,
+ String.format("%s = ? AND %s != 1", TaskContract.Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Tasks._DELETED),
+ new String[] { Long.toString(id) },
+ null,
+ null,
+ TaskContract.Tasks.ORIGINAL_INSTANCE_TIME);)
{
+
+ /*
+ * The goal of the code below is to update existing instances in place (as opposed to delete and recreate all instances). We do this for two reasons:
+ * 1) efficiency, in most cases existing instances don't change, deleting and recreating them would be overly expensive
+ * 2) stable row ids, deleting and recreating instances would change their id and void any existing URIs to them
+ */
final int idIdx = existingInstances.getColumnIndex(TaskContract.Instances._ID);
final int startIdx = existingInstances.getColumnIndex(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
final int taskIdIdx = existingInstances.getColumnIndex(TaskContract.Instances.TASK_ID);
@@ -241,14 +268,43 @@ public final class Instantiating implements EntityProcessor
// for very long or even infinite series we need to stop iterating at some point.
Iterable, Optional>> diff = new Diff<>(
- new Mapped<>(Single::value,
- new Limited<>(10000 /* hard limit for infinite rules*/, new InstanceValuesIterable(taskAdapter))),
+ new Mapped<>(Single::value, new Limited<>(10000 /* hard limit for infinite rules*/,
+ new Mapped<>(
+ new DiffMap<>(
+ (original, override) -> override, // we have both, a regular instance and an override -> take the override
+ original -> original,
+ override -> override // we only have an override :-o, not really valid but tolerated
+ ),
+ new Diff<>(
+ new InstanceValuesIterable(id, taskAdapter),
+ new Mapped<>(
+ cursor ->
+ new OverrideValuesFunction()
+ .value(new CursorContentValuesTaskAdapter(cursor, new ContentValues())),
+ () -> new RowIterator(overrides)),
+ (left, right) -> {
+ Long leftLong = left.value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
+ Long rightLong = right.value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
+ // null is always smaller
+ if (leftLong == null)
+ {
+ return rightLong == null ? 0 : -1;
+ }
+ if (rightLong == null)
+ {
+ return 1;
+ }
+
+ long ldiff = leftLong - rightLong;
+ return ldiff < 0 ? -1 : (ldiff > 0 ? 1 : 0);
+ })))),
new Range(existingInstances.getCount()),
(newInstanceValues, cursorRow) ->
{
existingInstances.moveToPosition(cursorRow);
- return (int) (new Backed<>(new NullSafe<>(newInstanceValues.getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME)), 0L).value()
- - existingInstances.getLong(startIdx));
+ long ldiff = new Backed<>(new NullSafe<>(newInstanceValues.getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME)), 0L).value()
+ - existingInstances.getLong(startIdx);
+ return ldiff < 0 ? -1 : (ldiff > 0 ? 1 : 0);
});
int distance = -1;
@@ -257,10 +313,17 @@ public final class Instantiating implements EntityProcessor
{
if (distance >= UPCOMING_INSTANCE_COUNT_LIMIT - 1)
{
- // if we already expanded enough instances, we pretend no other instance exists
+ // we already expanded enough instances
if (!next.right().isPresent())
{
- // actually no instance exists, no need to do anything
+ // if no further instances exist, stop here
+ Long original = next.left().value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
+ if (original != null && existingInstances.moveToLast() && existingInstances.getLong(startIdx) < original)
+ {
+ break;
+ }
+
+ // we may have to delete a few future instances
continue;
}
next = new RightSidedPair<>(next.right());
@@ -277,8 +340,7 @@ public final class Instantiating implements EntityProcessor
{
// there is no old instance for this new one, add it
ContentValues values = next.left().value();
- values.put(TaskContract.Instances.TASK_ID, taskAdapter.id());
- if (distance >= 0 || !taskAdapter.valueOf(TaskAdapter.IS_CLOSED))
+ if (distance >= 0 || values.getAsLong(TaskContract.Instances.DISTANCE_FROM_CURRENT) >= 0)
{
distance += 1;
}
@@ -289,42 +351,47 @@ public final class Instantiating implements EntityProcessor
{
// update this instance
existingInstances.moveToPosition(next.right().value());
- // only update if the instance belongs to this task
- if (existingInstances.getLong(taskIdIdx) == id)
+ ContentValues values = next.left().value();
+ if (distance >= 0 || values.getAsLong(TaskContract.Instances.DISTANCE_FROM_CURRENT) >= 0)
{
- ContentValues values = next.left().value();
- if (distance >= 0 ||
- taskAdapter.isUpdated(TaskAdapter.IS_CLOSED) && !taskAdapter.valueOf(TaskAdapter.IS_CLOSED) ||
- !taskAdapter.isUpdated(TaskAdapter.IS_CLOSED) && existingInstances.getInt(isClosedIdx) == 0)
- {
- // the distance needs to be updated
- distance += 1;
- values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance);
- }
-
- // TODO: only update if something actually changed
- db.update(TaskDatabaseHelper.Tables.INSTANCES, values,
- String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances._ID, existingInstances.getLong(idIdx)), null);
+ // the distance needs to be updated
+ distance += 1;
+ values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance);
}
- else if (distance >= 0 || existingInstances.getInt(isClosedIdx) == 0)
+
+ ContentValues updates = updatedOnly(values, existingInstances);
+ if (updates.size() > 0)
{
- // this is an override and we need to check the distance value
- distance += 1;
- if (distance != existingInstances.getInt(distanceIdx))
- {
- ContentValues contentValues = new ContentValues(1);
- contentValues.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance);
- db.update(TaskDatabaseHelper.Tables.INSTANCES, contentValues,
- String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances._ID, existingInstances.getLong(idIdx)), null);
- }
+ db.update(TaskDatabaseHelper.Tables.INSTANCES,
+ updates,
+ String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances._ID, existingInstances.getLong(idIdx)),
+ null);
}
}
}
}
- finally
+ }
+
+
+ private static ContentValues updatedOnly(ContentValues newValues, Cursor oldValues)
+ {
+ ContentValues result = new ContentValues(newValues);
+ for (String key : newValues.keySet())
{
- existingInstances.close();
+ int columnIdx = oldValues.getColumnIndex(key);
+ if (columnIdx < 0)
+ {
+ throw new RuntimeException("Missing column " + key + " in Cursor ");
+ }
+ if (oldValues.isNull(columnIdx) && newValues.get(key) == null)
+ {
+ result.remove(key);
+ }
+ else if (!oldValues.isNull(columnIdx) && newValues.get(key) != null && oldValues.getLong(columnIdx) == newValues.getAsLong(key))
+ {
+ result.remove(key);
+ }
}
+ return result;
}
-
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java
index 350935d79272b700fc1d4cb56bc66731be1a3a55..d64177950b53b9703d06e60abf38b45970c0627d 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java
@@ -17,6 +17,7 @@
package org.dmfs.provider.tasks.processors.tasks;
import android.content.ContentValues;
+import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import org.dmfs.provider.tasks.TaskDatabaseHelper;
@@ -35,7 +36,6 @@ import org.dmfs.tasks.contract.TaskContract;
* It also updates {@link TaskContract.Property.Relation#RELATED_UID} when a tasks
* is synced the first time and a UID has been set.
*
- * TODO: update {@link TaskContract.Tasks#PARENT_ID} of related tasks.
*
* @author Marten Gajda
*/
@@ -69,9 +69,35 @@ public final class Relating implements EntityProcessor
ContentValues v = new ContentValues(1);
v.put(TaskContract.Property.Relation.RELATED_ID, result.id());
- db.update(TaskDatabaseHelper.Tables.PROPERTIES, v,
+ int updates = db.update(TaskDatabaseHelper.Tables.PROPERTIES, v,
TaskContract.Property.Relation.MIMETYPE + "= ? AND " + TaskContract.Property.Relation.RELATED_UID + "=?", new String[] {
TaskContract.Property.Relation.CONTENT_ITEM_TYPE, uid });
+
+ if (updates > 0)
+ {
+ // there were other relations pointing towards this task, update PARENT_IDs if necessary
+ ContentValues parentIdValues = new ContentValues(1);
+ parentIdValues.put(TaskContract.Tasks.PARENT_ID, result.id());
+ // iterate over all tasks which refer to this as their parent and update their PARENT_ID
+ try (Cursor c = db.query(
+ TaskDatabaseHelper.Tables.PROPERTIES, new String[] { TaskContract.Property.Relation.TASK_ID },
+ String.format("%s = ? and %s = ? and %s = ?",
+ TaskContract.Property.Relation.MIMETYPE,
+ TaskContract.Property.Relation.RELATED_ID,
+ TaskContract.Property.Relation.RELATED_TYPE),
+ new String[] {
+ TaskContract.Property.Relation.CONTENT_ITEM_TYPE,
+ String.valueOf(result.id()),
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT) },
+ null, null, null))
+ {
+ while (c.moveToNext())
+ {
+ db.update(TaskDatabaseHelper.Tables.TASKS, parentIdValues, TaskContract.Tasks._ID + " = ?", new String[] { c.getString(0) });
+ }
+ }
+ // TODO, way also may have to do this for all the siblings of these tasks.
+ }
}
return result;
}
@@ -82,6 +108,7 @@ public final class Relating implements EntityProcessor
{
TaskAdapter result = mDelegate.update(db, task, isSyncAdapter);
// A task has been updated and may have received a UID by the sync adapter. Update all by-id references to this task.
+ // in this case we don't need to update any PARENT_ID because it should already be set.
if (!isSyncAdapter)
{
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java
new file mode 100644
index 0000000000000000000000000000000000000000..a08f9b7ab44cc7df67f8b47c727375d69540d928
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2020 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.processors.tasks;
+
+import android.content.ContentValues;
+import android.database.sqlite.SQLiteDatabase;
+
+import org.dmfs.provider.tasks.TaskDatabaseHelper;
+import org.dmfs.provider.tasks.model.TaskAdapter;
+import org.dmfs.provider.tasks.processors.EntityProcessor;
+import org.dmfs.tasks.contract.TaskContract;
+
+
+/**
+ * An {@link EntityProcessor} which updates a task's parent-child relations when its {@link TaskContract.Tasks#PARENT_ID} is updated.
+ *
+ * @author Marten Gajda
+ */
+public final class Reparenting implements EntityProcessor
+{
+ private final EntityProcessor mDelegate;
+
+
+ public Reparenting(EntityProcessor delegate)
+ {
+ mDelegate = delegate;
+ }
+
+
+ @Override
+ public TaskAdapter insert(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
+ {
+ TaskAdapter result = mDelegate.insert(db, entityAdapter, isSyncAdapter);
+ if (entityAdapter.isUpdated(TaskAdapter.PARENT_ID))
+ {
+ linkParent(db, result);
+ }
+ return result;
+ }
+
+
+ @Override
+ public TaskAdapter update(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
+ {
+ if (entityAdapter.isUpdated(TaskAdapter.PARENT_ID))
+ {
+ unlinkParent(db, entityAdapter);
+ TaskAdapter result = mDelegate.update(db, entityAdapter, isSyncAdapter);
+ linkParent(db, entityAdapter);
+ return result;
+ }
+ else
+ {
+ return mDelegate.update(db, entityAdapter, isSyncAdapter);
+ }
+ }
+
+
+ @Override
+ public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
+ {
+ unlinkParent(db, entityAdapter);
+ mDelegate.delete(db, entityAdapter, isSyncAdapter);
+ }
+
+
+ private void unlinkParent(SQLiteDatabase db, TaskAdapter taskAdapter)
+ {
+ if (taskAdapter.oldValueOf(TaskAdapter.PARENT_ID) != null)
+ {
+ // delete any parent, child or sibling relation with this task
+ db.delete(TaskDatabaseHelper.Tables.PROPERTIES,
+ String.format("%s = ? AND (%s = ? and %s in (?, ?) or %s = ? and %s in (?, ?))",
+ TaskContract.Property.Relation.MIMETYPE,
+ TaskContract.Property.Relation.TASK_ID,
+ TaskContract.Property.Relation.RELATED_TYPE,
+ TaskContract.Property.Relation.RELATED_ID,
+ TaskContract.Property.Relation.RELATED_TYPE),
+ new String[] {
+ TaskContract.Property.Relation.CONTENT_ITEM_TYPE,
+ String.valueOf(taskAdapter.valueOf(TaskAdapter._ID)),
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_SIBLING),
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT),
+ String.valueOf(taskAdapter.valueOf(TaskAdapter._ID)),
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_SIBLING),
+ String.valueOf(TaskContract.Property.Relation.RELTYPE_CHILD) });
+ }
+ }
+
+
+ private void linkParent(SQLiteDatabase db, TaskAdapter taskAdapter)
+ {
+ if (taskAdapter.valueOf(TaskAdapter.PARENT_ID) != null)
+ {
+ ContentValues values = new ContentValues();
+ values.put(TaskContract.Property.Relation.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE);
+ values.put(TaskContract.Property.Relation.TASK_ID, taskAdapter.id());
+ values.put(TaskContract.Property.Relation.RELATED_TYPE, TaskContract.Property.Relation.RELTYPE_PARENT);
+ values.put(TaskContract.Property.Relation.RELATED_ID, taskAdapter.valueOf(TaskAdapter.PARENT_ID));
+ values.put(TaskContract.Property.Relation.RELATED_UID, taskAdapter.valueOf(TaskAdapter._UID));
+ db.insert(TaskDatabaseHelper.Tables.PROPERTIES, "", values);
+ }
+ }
+
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java
index 018ed54fff9aac9b4779dba6a36c42777f9e3bb7..a28a97ea5a6d24184197b1d6f1d518e47b475595 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java
@@ -88,6 +88,13 @@ public final class Validating implements EntityProcessor
{
throw new IllegalArgumentException("ORIGINAL_INSTANCE_SYNC_ID and ORIGINAL_INSTANCE_ID can be modified by sync adapters only");
}
+
+ // only sync adapters are allowed to change the UID of existing tasks
+ if (!isSyncAdapter && task.isUpdated(TaskAdapter._UID))
+ {
+ throw new IllegalArgumentException("modification of _UID is not allowed to non-sync adapters");
+ }
+
return mDelegate.update(db, task, isSyncAdapter);
}
@@ -144,13 +151,6 @@ public final class Validating implements EntityProcessor
throw new IllegalArgumentException("modification of _DELETE is not allowed");
}
- // only sync adapters are allowed to change the UID
- // TODO: we probably should allow clients to set a UID on inserts
- if (!isSyncAdapter && task.isUpdated(TaskAdapter._UID))
- {
- throw new IllegalArgumentException("modification of _UID is not allowed");
- }
-
// only sync adapters are allowed to remove the dirty flag
if (!isSyncAdapter && task.isUpdated(TaskAdapter._DIRTY))
{
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 a4905f3ac7e2d620f9e799b355fc202bc5e438cc..793ffa1e6f9fd65d87ef3a859bc5ac171f8b2f67 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
@@ -18,21 +18,18 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata;
import android.content.ContentValues;
-import org.dmfs.iterables.elementary.Seq;
import org.dmfs.jems.optional.Optional;
-import org.dmfs.jems.optional.adapters.FirstPresent;
import org.dmfs.jems.optional.decorators.Mapped;
-import org.dmfs.jems.optional.elementary.NullSafe;
+import org.dmfs.jems.optional.elementary.Present;
+import org.dmfs.jems.procedure.composite.ForEach;
import org.dmfs.jems.single.Single;
-import org.dmfs.jems.single.combined.Backed;
import org.dmfs.rfc5545.DateTime;
import org.dmfs.tasks.contract.TaskContract;
/**
* A decorator for {@link Single}s of Instance {@link ContentValues} which populates the {@link TaskContract.Instances#INSTANCE_ORIGINAL_TIME} field based on
- * the given {@link Optional} original start and the already populated {@link TaskContract.Instances#INSTANCE_START} and {@link
- * TaskContract.Instances#INSTANCE_DUE} fields.
+ * the given {@link Optional} original start.
*
* @author Marten Gajda
*/
@@ -42,6 +39,12 @@ public final class Overridden implements Single
private final Single mDelegate;
+ public Overridden(DateTime originalTime, ContentValues delegate)
+ {
+ this(new Present<>(originalTime), () -> delegate);
+ }
+
+
public Overridden(Optional originalTime, Single delegate)
{
mOriginalTime = originalTime;
@@ -53,14 +56,7 @@ public final class Overridden implements Single
public ContentValues value()
{
ContentValues values = mDelegate.value();
- values.put(TaskContract.Instances.INSTANCE_ORIGINAL_TIME,
- new Backed(
- new FirstPresent<>(
- new Seq<>(
- new Mapped<>(DateTime::getTimestamp, mOriginalTime),
- new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_START)),
- new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_DUE)))),
- () -> null).value());
+ new ForEach<>(new Mapped<>(DateTime::getTimestamp, mOriginalTime)).process(time -> values.put(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, time));
return values;
}
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java
index 6d7501cc574c8cb51db2deda0d85062aba0ac0ba..65be8e90fc5b6a306d9ca8ceafa8a1b670f5bf2b 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java
@@ -19,6 +19,7 @@ package org.dmfs.provider.tasks.processors.tasks.instancedata;
import android.content.ContentValues;
import org.dmfs.jems.single.Single;
+import org.dmfs.provider.tasks.model.TaskAdapter;
import org.dmfs.tasks.contract.TaskContract;
@@ -33,6 +34,12 @@ public final class TaskRelated implements Single
private final Single mDelegate;
+ public TaskRelated(TaskAdapter taskAdapter, Single delegate)
+ {
+ this(taskAdapter.id(), delegate);
+ }
+
+
public TaskRelated(long taskId, Single delegate)
{
mTaskId = taskId;
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java
index 5c95cbe57528cef9df4ebbb1a7c3fb14a720afe4..b46d3cc2401cc5d3646732d0a24dab9035c0a145 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java
@@ -33,7 +33,7 @@ public final class VanillaInstanceData implements Single
@Override
public ContentValues value()
{
- ContentValues values = new ContentValues(6);
+ ContentValues values = new ContentValues(10);
values.putNull(TaskContract.Instances.INSTANCE_START);
values.putNull(TaskContract.Instances.INSTANCE_START_SORTING);
values.putNull(TaskContract.Instances.INSTANCE_DUE);
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java
index 3613a83f05959787c8fe24e99b41d532af5af924..5ee653d96722c6d5e15d06be96e33eb924dca1cb 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java
@@ -18,8 +18,8 @@ package org.dmfs.provider.tasks.utils;
import android.content.ContentValues;
-import org.dmfs.iterables.elementary.Seq;
import org.dmfs.iterators.SingletonIterator;
+import org.dmfs.jems.iterable.elementary.Seq;
import org.dmfs.jems.iterator.decorators.Mapped;
import org.dmfs.jems.optional.Optional;
import org.dmfs.jems.optional.adapters.FirstPresent;
@@ -27,13 +27,13 @@ import org.dmfs.jems.optional.composite.Zipped;
import org.dmfs.jems.optional.elementary.NullSafe;
import org.dmfs.jems.optional.elementary.Present;
import org.dmfs.jems.single.Single;
-import org.dmfs.jems.single.elementary.ValueSingle;
import org.dmfs.provider.tasks.model.TaskAdapter;
import org.dmfs.provider.tasks.processors.tasks.instancedata.Distant;
import org.dmfs.provider.tasks.processors.tasks.instancedata.DueDated;
import org.dmfs.provider.tasks.processors.tasks.instancedata.Enduring;
import org.dmfs.provider.tasks.processors.tasks.instancedata.Overridden;
import org.dmfs.provider.tasks.processors.tasks.instancedata.StartDated;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.TaskRelated;
import org.dmfs.provider.tasks.processors.tasks.instancedata.VanillaInstanceData;
import org.dmfs.rfc5545.DateTime;
import org.dmfs.rfc5545.Duration;
@@ -49,11 +49,13 @@ import java.util.Iterator;
// TODO: replace Single with Generator
public final class InstanceValuesIterable implements Iterable>
{
+ private final long mId;
private final TaskAdapter mTaskAdapter;
- public InstanceValuesIterable(TaskAdapter taskAdapter)
+ public InstanceValuesIterable(long id, TaskAdapter taskAdapter)
{
+ mId = id;
mTaskAdapter = taskAdapter;
}
@@ -64,12 +66,11 @@ public final class InstanceValuesIterable implements Iterable start = new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DTSTART));
// effective due is either the actual due, start + duration or absent
Optional effectiveDue = new FirstPresent<>(
- new Seq<>(
- new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DUE)),
- new Zipped<>(start, new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration)));
+ new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DUE)),
+ new Zipped<>(start, new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration));
Single baseData = new Distant(mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0,
- new Enduring(new DueDated(effectiveDue, new StartDated(start, new VanillaInstanceData()))));
+ new Enduring(new DueDated(effectiveDue, new StartDated(start, new TaskRelated(mId, new VanillaInstanceData())))));
if (!mTaskAdapter.isRecurring())
{
@@ -78,7 +79,7 @@ public final class InstanceValuesIterable implements Iterable(
new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)),
baseData,
- (DateTime time, ContentValues data) -> new Overridden(new Present<>(time), new ValueSingle<>(data)).value()));
+ (DateTime time, ContentValues data) -> new Overridden(time, data).value()));
}
if (start.isPresent())
@@ -91,15 +92,17 @@ public final class InstanceValuesIterable implements Iterable(dateTime -> new Distant(mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0,
new Overridden(new Present<>(dateTime),
- new Enduring(new DueDated(new Zipped<>(new Present<>(dateTime), effectiveDuration, this::addDuration),
- new StartDated(new Present<>(dateTime), new VanillaInstanceData()))))),
+ new Enduring(
+ new DueDated(new Zipped<>(new Present<>(dateTime), effectiveDuration, this::addDuration),
+ new StartDated(new Present<>(dateTime),
+ new TaskRelated(mId, new VanillaInstanceData())))))),
new TaskInstanceIterable(mTaskAdapter).iterator());
}
// special treatment for recurring tasks without a DTSTART:
return new Mapped<>(dateTime -> new Distant(mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0,
new Overridden(new Present<>(dateTime),
- new DueDated(new Present<>(dateTime), new VanillaInstanceData()))),
+ new DueDated(new Present<>(dateTime), new TaskRelated(mId, new VanillaInstanceData())))),
new TaskInstanceIterable(mTaskAdapter).iterator());
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java
new file mode 100644
index 0000000000000000000000000000000000000000..b8984b6f2b721dff8cdfa6ae91ae90caf04378dd
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.utils;
+
+import android.content.ContentValues;
+
+import org.dmfs.jems.function.Function;
+import org.dmfs.jems.optional.Optional;
+import org.dmfs.jems.optional.adapters.FirstPresent;
+import org.dmfs.jems.optional.composite.Zipped;
+import org.dmfs.jems.optional.elementary.NullSafe;
+import org.dmfs.jems.single.Single;
+import org.dmfs.provider.tasks.model.TaskAdapter;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.Distant;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.DueDated;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.Enduring;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.Overridden;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.StartDated;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.TaskRelated;
+import org.dmfs.provider.tasks.processors.tasks.instancedata.VanillaInstanceData;
+import org.dmfs.rfc5545.DateTime;
+
+
+/**
+ * An {@link Iterable} of {@link Single} {@link ContentValues} of the overrides of a task.
+ *
+ * @author Marten Gajda
+ */
+public final class OverrideValuesFunction implements Function>
+{
+
+ @Override
+ public Single value(TaskAdapter taskAdapter)
+ {
+ Optional start = new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DTSTART));
+ // effective due is either the actual due, start + duration or absent
+ Optional effectiveDue = new FirstPresent<>(
+ new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DUE)),
+ new Zipped<>(start, new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration));
+
+ Single baseData = new Distant(taskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0,
+ new Enduring(new DueDated(effectiveDue, new StartDated(start, new TaskRelated(taskAdapter, new VanillaInstanceData())))));
+
+ // apply the Overridden decorator only if this task has an ORIGINAL_INSTANCE_TIME
+ return new org.dmfs.provider.tasks.utils.Zipped<>(
+ new NullSafe<>(taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)),
+ baseData,
+ (DateTime time, ContentValues data) -> new Overridden(time, data).value());
+ }
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java
new file mode 100644
index 0000000000000000000000000000000000000000..e49ac383564207968e962d7e18e1335dc131ff2e
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.utils;
+
+import android.database.Cursor;
+
+import org.dmfs.iterators.AbstractBaseIterator;
+
+import java.util.NoSuchElementException;
+
+
+/**
+ * @author Marten Gajda
+ */
+public final class RowIterator extends AbstractBaseIterator
+{
+ private final Cursor mCursor;
+
+
+ public RowIterator(Cursor cursor)
+ {
+ mCursor = cursor;
+ }
+
+
+ @Override
+ public boolean hasNext()
+ {
+ return mCursor.getCount() > 0 && !mCursor.isClosed() && !mCursor.isLast();
+ }
+
+
+ @Override
+ public Cursor next()
+ {
+ if (!hasNext())
+ {
+ throw new NoSuchElementException("No other rows to iterate.");
+ }
+ mCursor.moveToNext();
+ return mCursor;
+ }
+}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java
index 24cb9e3c62944fd8584b904264368763337501cf..b3620b395383f4b8a2b4b175d711f1a1b90f6fed 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java
@@ -16,15 +16,14 @@
package org.dmfs.provider.tasks.utils;
+import org.dmfs.jems.optional.elementary.NullSafe;
import org.dmfs.jems.single.combined.Backed;
-import org.dmfs.optional.NullSafe;
import org.dmfs.provider.tasks.model.TaskAdapter;
import org.dmfs.rfc5545.DateTime;
import org.dmfs.rfc5545.recur.RecurrenceRule;
import org.dmfs.rfc5545.recurrenceset.RecurrenceList;
import org.dmfs.rfc5545.recurrenceset.RecurrenceRuleAdapter;
import org.dmfs.rfc5545.recurrenceset.RecurrenceSet;
-import org.dmfs.rfc5545.recurrenceset.RecurrenceSetIterator;
import java.util.Iterator;
import java.util.TimeZone;
@@ -73,29 +72,9 @@ public final class TaskInstanceIterable implements Iterable
set.addInstances(new RecurrenceRuleAdapter(rule));
}
- set.addInstances(new RecurrenceList(toLongArray(mTaskAdapter.valueOf(TaskAdapter.RDATE))));
- set.addExceptions(new RecurrenceList(toLongArray(mTaskAdapter.valueOf(TaskAdapter.EXDATE))));
+ set.addInstances(new RecurrenceList(new Timestamps(mTaskAdapter.valueOf(TaskAdapter.RDATE)).value()));
+ set.addExceptions(new RecurrenceList(new Timestamps(mTaskAdapter.valueOf(TaskAdapter.EXDATE)).value()));
- RecurrenceSetIterator setIterator = set.iterator(dtstart.getTimeZone(), dtstart.getTimestamp(),
- System.currentTimeMillis() + 10L * 356L * 3600L * 1000L);
-
- return new TaskInstanceIterator(dtstart, setIterator, mTaskAdapter.valueOf(TaskAdapter.TIMEZONE_RAW));
- }
-
-
- private long[] toLongArray(Iterable dates)
- {
- int count = 0;
- for (DateTime ignored : dates)
- {
- count += 1;
- }
- long[] timeStamps = new long[count];
- int i = 0;
- for (DateTime dt : dates)
- {
- timeStamps[i++] = dt.getTimestamp();
- }
- return timeStamps;
+ return new TaskInstanceIterator(dtstart, set);
}
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java
index 6421ca7c4e147652a86788d9dd645c254edd16c9..5b74cfb63ca3dc2fe4fdac98454b8d2600ef5d9e 100644
--- a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java
@@ -17,10 +17,16 @@
package org.dmfs.provider.tasks.utils;
import org.dmfs.iterators.AbstractBaseIterator;
+import org.dmfs.jems.optional.decorators.Mapped;
+import org.dmfs.jems.optional.elementary.NullSafe;
+import org.dmfs.jems.single.combined.Backed;
import org.dmfs.rfc5545.DateTime;
+import org.dmfs.rfc5545.recurrenceset.RecurrenceSet;
import org.dmfs.rfc5545.recurrenceset.RecurrenceSetIterator;
import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.TimeZone;
/**
@@ -37,6 +43,13 @@ public final class TaskInstanceIterator extends AbstractBaseIterator
private final String mTimezone;
+ TaskInstanceIterator(DateTime start, RecurrenceSet set)
+ {
+ this(start, set.iterator(start.getTimeZone(), start.getTimestamp()),
+ new Backed<>(new Mapped<>(TimeZone::getID, new NullSafe<>(start.getTimeZone())), () -> null).value());
+ }
+
+
TaskInstanceIterator(DateTime start, RecurrenceSetIterator setIterator, String timezone)
{
mStart = start;
@@ -55,6 +68,10 @@ public final class TaskInstanceIterator extends AbstractBaseIterator
@Override
public DateTime next()
{
+ if (!hasNext())
+ {
+ throw new NoSuchElementException("No more elements to iterate");
+ }
DateTime result = new DateTime(mStart.getTimeZone(), mSetIterator.next());
return mStart.isAllDay() ? result.toAllDay() : mTimezone == null ? result.swapTimeZone(null) : result;
}
diff --git a/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java
new file mode 100644
index 0000000000000000000000000000000000000000..84f0949dee116df7c9e5a092a445523130e1fef1
--- /dev/null
+++ b/opentasks-provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.utils;
+
+import org.dmfs.jems.single.Single;
+import org.dmfs.rfc5545.DateTime;
+
+
+/**
+ * A {@link Single} of an array of timestamp values of a given {@link Iterable} of {@link DateTime}s.
+ *
+ * @author Marten Gajda
+ */
+public final class Timestamps implements Single
+{
+ private final Iterable mDateTimes;
+
+
+ public Timestamps(Iterable dateTimes)
+ {
+ mDateTimes = dateTimes;
+ }
+
+
+ @Override
+ public long[] value()
+ {
+ int count = 0;
+ for (DateTime ignored : mDateTimes)
+ {
+ count += 1;
+ }
+ long[] timeStamps = new long[count];
+ int i = 0;
+ for (DateTime dt : mDateTimes)
+ {
+ timeStamps[i++] = dt.getTimestamp();
+ }
+ return timeStamps;
+ }
+}
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java
index 442e2e4d374ad193a055310d90afdc0980c54334..e56d8a416292ab1dbaf0669f496c072d79680554 100644
--- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java
@@ -45,7 +45,7 @@ public class OverriddenTest
{
ContentValues instanceData = new Overridden(absent(), ContentValues::new).value();
assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class)));
- assertThat(instanceData.size(), is(1));
+ assertThat(instanceData.size(), is(0));
}
@@ -56,8 +56,8 @@ public class OverriddenTest
values.put(TaskContract.Instances.INSTANCE_START, 10);
ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value();
- assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 10));
- assertThat(instanceData.size(), is(2));
+ assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class)));
+ assertThat(instanceData.size(), is(1));
}
@@ -68,8 +68,8 @@ public class OverriddenTest
values.put(TaskContract.Instances.INSTANCE_DUE, 20);
ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value();
- assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 20));
- assertThat(instanceData.size(), is(2));
+ assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class)));
+ assertThat(instanceData.size(), is(1));
}
@@ -81,8 +81,8 @@ public class OverriddenTest
values.put(TaskContract.Instances.INSTANCE_DUE, 20);
ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value();
- assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 10));
- assertThat(instanceData.size(), is(3));
+ assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class)));
+ assertThat(instanceData.size(), is(2));
}
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java
index 8d84e538f6006d6c2b81c2fafc65905ef1d4312e..81eff9c6f157ebab3ba29ddc043a60b4b6c74df4 100644
--- a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java
@@ -18,6 +18,7 @@ package org.dmfs.provider.tasks.utils;
import android.content.ContentValues;
+import org.dmfs.iterables.elementary.Seq;
import org.dmfs.provider.tasks.model.ContentValuesTaskAdapter;
import org.dmfs.provider.tasks.model.TaskAdapter;
import org.dmfs.rfc5545.DateTime;
@@ -105,4 +106,83 @@ public class TaskInstanceIterableTest
DateTime.parse("20170624T121314")
));
}
+
+
+ @Test
+ public void testRDate() throws Exception
+ {
+ TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues());
+ taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314"));
+ taskAdapter.set(TaskAdapter.RDATE, new Seq<>(
+ DateTime.parse("Europe/Berlin", "20170606T121314"),
+ DateTime.parse("Europe/Berlin", "20170608T121314"),
+ DateTime.parse("Europe/Berlin", "20170610T121314"),
+ DateTime.parse("Europe/Berlin", "20170612T121314"),
+ DateTime.parse("Europe/Berlin", "20170614T121314"),
+ DateTime.parse("Europe/Berlin", "20170616T121314"),
+ DateTime.parse("Europe/Berlin", "20170618T121314"),
+ DateTime.parse("Europe/Berlin", "20170620T121314"),
+ DateTime.parse("Europe/Berlin", "20170622T121314"),
+ DateTime.parse("Europe/Berlin", "20170624T121314")
+ ));
+
+ assertThat(new TaskInstanceIterable(taskAdapter),
+ iteratesTo(
+ DateTime.parse("Europe/Berlin", "20170606T121314"),
+ DateTime.parse("Europe/Berlin", "20170608T121314"),
+ DateTime.parse("Europe/Berlin", "20170610T121314"),
+ DateTime.parse("Europe/Berlin", "20170612T121314"),
+ DateTime.parse("Europe/Berlin", "20170614T121314"),
+ DateTime.parse("Europe/Berlin", "20170616T121314"),
+ DateTime.parse("Europe/Berlin", "20170618T121314"),
+ DateTime.parse("Europe/Berlin", "20170620T121314"),
+ DateTime.parse("Europe/Berlin", "20170622T121314"),
+ DateTime.parse("Europe/Berlin", "20170624T121314")
+ ));
+ }
+
+
+ @Test
+ public void testRDateAndRRule() throws Exception
+ {
+ TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues());
+ taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314"));
+ taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10"));
+ taskAdapter.set(TaskAdapter.RDATE, new Seq<>(
+ DateTime.parse("Europe/Berlin", "20170606T121313"),
+ DateTime.parse("Europe/Berlin", "20170608T121313"),
+ DateTime.parse("Europe/Berlin", "20170610T121313"),
+ DateTime.parse("Europe/Berlin", "20170612T121313"),
+ DateTime.parse("Europe/Berlin", "20170614T121313"),
+ DateTime.parse("Europe/Berlin", "20170616T121313"),
+ DateTime.parse("Europe/Berlin", "20170618T121313"),
+ DateTime.parse("Europe/Berlin", "20170620T121313"),
+ DateTime.parse("Europe/Berlin", "20170622T121313"),
+ DateTime.parse("Europe/Berlin", "20170624T121313")
+ ));
+
+ assertThat(new TaskInstanceIterable(taskAdapter),
+ iteratesTo(
+ DateTime.parse("Europe/Berlin", "20170606T121313"),
+ DateTime.parse("Europe/Berlin", "20170606T121314"),
+ DateTime.parse("Europe/Berlin", "20170608T121313"),
+ DateTime.parse("Europe/Berlin", "20170608T121314"),
+ DateTime.parse("Europe/Berlin", "20170610T121313"),
+ DateTime.parse("Europe/Berlin", "20170610T121314"),
+ DateTime.parse("Europe/Berlin", "20170612T121313"),
+ DateTime.parse("Europe/Berlin", "20170612T121314"),
+ DateTime.parse("Europe/Berlin", "20170614T121313"),
+ DateTime.parse("Europe/Berlin", "20170614T121314"),
+ DateTime.parse("Europe/Berlin", "20170616T121313"),
+ DateTime.parse("Europe/Berlin", "20170616T121314"),
+ DateTime.parse("Europe/Berlin", "20170618T121313"),
+ DateTime.parse("Europe/Berlin", "20170618T121314"),
+ DateTime.parse("Europe/Berlin", "20170620T121313"),
+ DateTime.parse("Europe/Berlin", "20170620T121314"),
+ DateTime.parse("Europe/Berlin", "20170622T121313"),
+ DateTime.parse("Europe/Berlin", "20170622T121314"),
+ DateTime.parse("Europe/Berlin", "20170624T121313"),
+ DateTime.parse("Europe/Berlin", "20170624T121314")
+ ));
+ }
}
\ No newline at end of file
diff --git a/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..552f43de4c936a6b2e7e6f4f69b65f57ac30ab7b
--- /dev/null
+++ b/opentasks-provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.provider.tasks.utils;
+
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException;
+import org.dmfs.rfc5545.recur.RecurrenceRule;
+import org.dmfs.rfc5545.recurrenceset.RecurrenceRuleAdapter;
+import org.dmfs.rfc5545.recurrenceset.RecurrenceSet;
+import org.junit.Test;
+
+import java.util.TimeZone;
+
+import static org.dmfs.jems.hamcrest.matchers.iterator.IteratorMatcher.iteratorOf;
+import static org.junit.Assert.assertThat;
+
+
+/**
+ * @author Marten Gajda
+ */
+public class TaskInstanceIteratorTest
+{
+ private final static String TIMEZONE = "Europe/Berlin";
+
+
+ @Test
+ public void testAbsolute() throws InvalidRecurrenceRuleException
+ {
+ RecurrenceSet recurrenceSet = new RecurrenceSet();
+ recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3")));
+ DateTime start = DateTime.parse(TIMEZONE, "20210201T120000");
+
+ assertThat(
+ () -> new TaskInstanceIterator(start, recurrenceSet.iterator(TimeZone.getTimeZone(TIMEZONE), start.getTimestamp()), TIMEZONE),
+ iteratorOf(
+ DateTime.parse(TIMEZONE, "20210201T120000"),
+ DateTime.parse(TIMEZONE, "20210202T120000"),
+ DateTime.parse(TIMEZONE, "20210203T120000")
+ )
+ );
+
+ assertThat(
+ () -> new TaskInstanceIterator(start, recurrenceSet),
+ iteratorOf(
+ DateTime.parse(TIMEZONE, "20210201T120000"),
+ DateTime.parse(TIMEZONE, "20210202T120000"),
+ DateTime.parse(TIMEZONE, "20210203T120000")
+ )
+ );
+ }
+
+
+ @Test
+ public void testFloating() throws InvalidRecurrenceRuleException
+ {
+
+ RecurrenceSet recurrenceSet = new RecurrenceSet();
+ recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3")));
+ DateTime start = DateTime.parse("20210201T120000");
+
+ assertThat(
+ () -> new TaskInstanceIterator(start, recurrenceSet.iterator(null, start.getTimestamp()), null),
+ iteratorOf(
+ DateTime.parse("20210201T120000"),
+ DateTime.parse("20210202T120000"),
+ DateTime.parse("20210203T120000")
+ )
+ );
+
+ assertThat(
+ () -> new TaskInstanceIterator(start, recurrenceSet),
+ iteratorOf(
+ DateTime.parse("20210201T120000"),
+ DateTime.parse("20210202T120000"),
+ DateTime.parse("20210203T120000")
+ )
+ );
+ }
+
+
+ @Test
+ public void testAllDay() throws InvalidRecurrenceRuleException
+ {
+
+ RecurrenceSet recurrenceSet = new RecurrenceSet();
+ recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3")));
+ DateTime start = DateTime.parse("20210201");
+
+ assertThat(
+ () -> new TaskInstanceIterator(start, recurrenceSet.iterator(null, start.getTimestamp()), null),
+ iteratorOf(
+ DateTime.parse("20210201"),
+ DateTime.parse("20210202"),
+ DateTime.parse("20210203")
+ )
+ );
+
+ assertThat(
+ () -> new TaskInstanceIterator(start, recurrenceSet),
+ iteratorOf(
+ DateTime.parse("20210201"),
+ DateTime.parse("20210202"),
+ DateTime.parse("20210203")
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/opentasks-theme/.gitignore b/opentasks-theme/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..42afabfd2abebf31384ca7797186a27a4b7dbee8
--- /dev/null
+++ b/opentasks-theme/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/opentasks-theme/build.gradle b/opentasks-theme/build.gradle
new file mode 100644
index 0000000000000000000000000000000000000000..4021aacf989c2d6a792921c5bd9655edb25a7c27
--- /dev/null
+++ b/opentasks-theme/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+ id 'com.android.library'
+}
+
+android {
+ compileSdkVersion COMPILE_SDK_VERSION.toInteger()
+ buildToolsVersion "29.0.3"
+
+ defaultConfig {
+ minSdkVersion MIN_SDK_VERSION
+ targetSdkVersion TARGET_SDK_VERSION
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'com.google.android.material:material:1.2.1'
+ implementation 'foundation.e:elib:0.0.1-alpha11'
+}
\ No newline at end of file
diff --git a/opentasks-theme/src/main/AndroidManifest.xml b/opentasks-theme/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7e97eb3a46ac1307a491dfa22d3d7287e9e71014
--- /dev/null
+++ b/opentasks-theme/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/opentasks-theme/src/main/res/values-night-v23/theme_daynight.xml b/opentasks-theme/src/main/res/values-night-v23/theme_daynight.xml
new file mode 100644
index 0000000000000000000000000000000000000000..16ac728cc5eb5b76a7d641d2f97e45a0c4e4f3d4
--- /dev/null
+++ b/opentasks-theme/src/main/res/values-night-v23/theme_daynight.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values-night-v27/theme_daynight.xml b/opentasks-theme/src/main/res/values-night-v27/theme_daynight.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5d867026fde1412e7501a9a722f51dba74447ac6
--- /dev/null
+++ b/opentasks-theme/src/main/res/values-night-v27/theme_daynight.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values-night/theme_daynight.xml b/opentasks-theme/src/main/res/values-night/theme_daynight.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e28d907db0abc615820f84f009b399b317d3dce1
--- /dev/null
+++ b/opentasks-theme/src/main/res/values-night/theme_daynight.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values-v23/theme_dark.xml b/opentasks-theme/src/main/res/values-v23/theme_dark.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1a8c87a4b8496ddd376e76cb87368148fcd6f612
--- /dev/null
+++ b/opentasks-theme/src/main/res/values-v23/theme_dark.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values-v23/theme_daynight.xml b/opentasks-theme/src/main/res/values-v23/theme_daynight.xml
new file mode 100644
index 0000000000000000000000000000000000000000..68978c9e82ff51a456beff8459f100aab7de732a
--- /dev/null
+++ b/opentasks-theme/src/main/res/values-v23/theme_daynight.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values-v23/theme_light.xml b/opentasks-theme/src/main/res/values-v23/theme_light.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d259f6db863c40eb7a6e476e322882d34240adf2
--- /dev/null
+++ b/opentasks-theme/src/main/res/values-v23/theme_light.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values-v27/theme_dark.xml b/opentasks-theme/src/main/res/values-v27/theme_dark.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c97df206d2f2ba306365bbb7dedc22e3476f52db
--- /dev/null
+++ b/opentasks-theme/src/main/res/values-v27/theme_dark.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values-v27/theme_daynight.xml b/opentasks-theme/src/main/res/values-v27/theme_daynight.xml
new file mode 100644
index 0000000000000000000000000000000000000000..58c68efd09690afb75d34839830917c4bab17f38
--- /dev/null
+++ b/opentasks-theme/src/main/res/values-v27/theme_daynight.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values-v27/theme_light.xml b/opentasks-theme/src/main/res/values-v27/theme_light.xml
new file mode 100644
index 0000000000000000000000000000000000000000..870ec4ee32a24aba98cac6de646bc883bfbf5532
--- /dev/null
+++ b/opentasks-theme/src/main/res/values-v27/theme_light.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values/attrs.xml b/opentasks-theme/src/main/res/values/attrs.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cf367a11d1bf804eff3f4f2483b587aef6145308
--- /dev/null
+++ b/opentasks-theme/src/main/res/values/attrs.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/opentasks-theme/src/main/res/values/colors.xml b/opentasks-theme/src/main/res/values/colors.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b8bd0d662cd5031def793ccd7de28247a2851c88
--- /dev/null
+++ b/opentasks-theme/src/main/res/values/colors.xml
@@ -0,0 +1,33 @@
+
+
+
+
+ @color/e_accent
+ @color/e_accent
+ @color/e_accent
+ @color/e_accent
+
+
+
+
+ #4caf50
+
+
+ #f44336
+ #d32f2f
+ #b71c1c
+
+
+ #009688
+ #00796B
+
+
+ #ffc107
+
+
+ #ff5722
+ #bf360c
+
+ #08000000
+ #10ffffff
+
\ No newline at end of file
diff --git a/opentasks-theme/src/main/res/values/dimens.xml b/opentasks-theme/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000000000000000000000000000000000..301e9bf1cb88867d73d27bb1591ca5644d4ca131
--- /dev/null
+++ b/opentasks-theme/src/main/res/values/dimens.xml
@@ -0,0 +1,7 @@
+
+
+
+ 0dp
+ 2dp
+ 4dp
+
\ No newline at end of file
diff --git a/opentasks-theme/src/main/res/values/theme.xml b/opentasks-theme/src/main/res/values/theme.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6de0d827126e87e1cb5ef13554fb6de42b6df007
--- /dev/null
+++ b/opentasks-theme/src/main/res/values/theme.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values/theme_dark.xml b/opentasks-theme/src/main/res/values/theme_dark.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ee11720cefab8b24593e512f765a5f8eda1e2078
--- /dev/null
+++ b/opentasks-theme/src/main/res/values/theme_dark.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values/theme_daynight.xml b/opentasks-theme/src/main/res/values/theme_daynight.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2ed6c66cdb2518b20f04d4315817b4702c7fa53a
--- /dev/null
+++ b/opentasks-theme/src/main/res/values/theme_daynight.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opentasks-theme/src/main/res/values/theme_light.xml b/opentasks-theme/src/main/res/values/theme_light.xml
new file mode 100644
index 0000000000000000000000000000000000000000..48f55c76a33c102502426ea3e862e26c3f31310e
--- /dev/null
+++ b/opentasks-theme/src/main/res/values/theme_light.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/opentasks/build.gradle b/opentasks/build.gradle
index fb22e9898bcb772f294c2ffac3017bd3ebc5832a..446507fcaba5c29fcd3e89f7a953902fbe1b14d4 100644
--- a/opentasks/build.gradle
+++ b/opentasks/build.gradle
@@ -5,11 +5,17 @@ if (project.hasProperty('PLAY_STORE_SERVICE_ACCOUNT_CREDENTIALS')) {
// commit number is only relevant to the application project
def gitCommitNo = { ref ->
def stdout = new ByteArrayOutputStream()
- exec {
- commandLine 'git', 'rev-list', '--count', ref
- standardOutput = stdout
+ try {
+ exec {
+ commandLine 'git', 'rev-list', '--count', ref
+ standardOutput = stdout
+ }
+
+ return Integer.parseInt(stdout.toString().trim())
+ }
+ catch (Exception e) {
+ return 0
}
- return Integer.parseInt(stdout.toString().trim())
}
android {
@@ -63,6 +69,7 @@ android {
}
dependencies {
+ implementation project(':opentasks-theme')
implementation project(':opentasks-provider')
implementation deps.support_appcompat
implementation deps.support_design
@@ -94,6 +101,10 @@ dependencies {
implementation project(path: ':opentaskspal')
implementation 'foundation.e:elib:0.0.1-alpha11'
+ implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
+ implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
+ implementation 'androidx.preference:preference:1.1.1'
+ implementation 'com.maltaisn:recurpicker:2.1.4'
}
if (project.hasProperty('PLAY_STORE_SERVICE_ACCOUNT_CREDENTIALS')) {
diff --git a/opentasks/release/opentasks-release.apk b/opentasks/release/opentasks-release.apk
new file mode 100644
index 0000000000000000000000000000000000000000..5a0004d805142b3b7758ead78f4e2fdf3b272038
Binary files /dev/null and b/opentasks/release/opentasks-release.apk differ
diff --git a/opentasks/release/output.json b/opentasks/release/output.json
new file mode 100644
index 0000000000000000000000000000000000000000..cbe26bbd392939b096d6ac48f9c2f7a4876dad7f
--- /dev/null
+++ b/opentasks/release/output.json
@@ -0,0 +1 @@
+[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":78000,"versionName":"1.2.3-4-g8431d4a0-dirty","enabled":true,"outputFile":"opentasks-release.apk","fullName":"release","baseName":"release"},"path":"opentasks-release.apk","properties":{}}]
\ No newline at end of file
diff --git a/opentasks/src/main/AndroidManifest.xml b/opentasks/src/main/AndroidManifest.xml
index 42341a7799691b8265edb92539b28211766972bd..c2018b27db07e86ac68258c752d3146593989b00 100644
--- a/opentasks/src/main/AndroidManifest.xml
+++ b/opentasks/src/main/AndroidManifest.xml
@@ -23,7 +23,7 @@
android:label="@string/app_name"
android:supportsRtl="false"
android:taskAffinity="org.dmfs.tasks.TaskListActivity"
- android:theme="@style/OpenTasksAppTheme">
+ android:theme="@style/OpenTasks_Theme.Default">
+ android:parentActivityName=".TaskListActivity">
@@ -166,7 +165,7 @@
+ android:theme="@style/OpenTasks_BaseTheme.DayNight.Dialog">
@@ -270,7 +269,7 @@
+ android:theme="@style/OpenTasks_Theme.Default.Dialog">
@@ -298,8 +297,7 @@
+ android:label="@string/title_activity_settings" />
+
+
+
+
+ = 29)
+ {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
+ }
+ else
+ {
+ getActivity().recreate();
+ }
+ }
+ return super.onPreferenceTreeClick(preference);
+ }
+}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/AppNotificationSettingsFragment.java b/opentasks/src/main/java/org/dmfs/tasks/AppNotificationSettingsFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..22da0afe6ef90d288e8e9e0ac452e45509dfa8bf
--- /dev/null
+++ b/opentasks/src/main/java/org/dmfs/tasks/AppNotificationSettingsFragment.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.tasks;
+
+import android.os.Bundle;
+
+import androidx.preference.PreferenceFragmentCompat;
+
+
+/**
+ * Fragment for the app notifications settings on Android <8.
+ */
+public final class AppNotificationSettingsFragment extends PreferenceFragmentCompat
+{
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey)
+ {
+ setPreferencesFromResource(R.xml.notification_preferences, rootKey);
+ }
+}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/AppSettingsActivity.java b/opentasks/src/main/java/org/dmfs/tasks/AppSettingsActivity.java
index 0b5c930b6c00317f0c40616e31f751cdb6ed5b9b..182eaa995e3db2750e494bba53c79034bba68d8e 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/AppSettingsActivity.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/AppSettingsActivity.java
@@ -16,30 +16,49 @@
package org.dmfs.tasks;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Build;
import android.os.Bundle;
+import android.provider.Settings;
import android.view.MenuItem;
import org.dmfs.tasks.utils.BaseActivity;
+import java.util.Objects;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceManager;
+
/**
* Activity for the general app settings screen.
*
* @author Gabor Keszthelyi
*/
-public final class AppSettingsActivity extends BaseActivity
+public final class AppSettingsActivity extends BaseActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
{
+ private SharedPreferences mPrefs;
+
@Override
protected void onCreate(Bundle savedInstanceState)
{
- super.onCreate(savedInstanceState);
-
- getFragmentManager().beginTransaction()
- .replace(android.R.id.content, new AppSettingsFragment())
- .commit();
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.opentasks_activity_preferences);
+ if (savedInstanceState == null)
+ {
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.content, new AppSettingsFragment())
+ .commit();
+ }
+ setSupportActionBar(findViewById(R.id.toolbar));
+ Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_arrow_back);
}
@@ -54,4 +73,35 @@ public final class AppSettingsActivity extends BaseActivity
return super.onOptionsItemSelected(item);
}
+
+ @Override
+ public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref)
+ {
+ if (Build.VERSION.SDK_INT >= 26 && "notifications".equalsIgnoreCase(pref.getKey()))
+ {
+ // open the system notification settings
+ Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
+ intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
+ startActivity(intent);
+ return true;
+ }
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.content, getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment()))
+ .addToBackStack("")
+ .commit();
+ return true;
+ }
+
+
+ @Override
+ public Resources.Theme getTheme()
+ {
+ Resources.Theme theme = super.getTheme();
+ if (Build.VERSION.SDK_INT < 29)
+ {
+ theme.applyStyle(R.style.OpenTasks_Theme_Default, true);
+ }
+ return theme;
+ }
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/AppSettingsFragment.java b/opentasks/src/main/java/org/dmfs/tasks/AppSettingsFragment.java
index d184e7f332eb7d65645cd7d36b5545beda95b5ca..8a4ea58e54c1c327a71e4247e79d59b54da65c86 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/AppSettingsFragment.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/AppSettingsFragment.java
@@ -17,23 +17,18 @@
package org.dmfs.tasks;
import android.os.Bundle;
-import android.preference.PreferenceFragment;
-import androidx.annotation.Nullable;
+
+import androidx.preference.PreferenceFragmentCompat;
/**
* Fragment for the general app settings.
- *
- * @author Gabor Keszthelyi
*/
-public final class AppSettingsFragment extends PreferenceFragment
+public final class AppSettingsFragment extends PreferenceFragmentCompat
{
-
@Override
- public void onCreate(@Nullable Bundle savedInstanceState)
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey)
{
- super.onCreate(savedInstanceState);
-
- addPreferencesFromResource(R.xml.app_preferences);
+ setPreferencesFromResource(R.xml.app_preferences, rootKey);
}
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java
index 59595f19b6763b92944baffa626c040b0933079c..e03c35e8fa3dad75c67154a06b02370bc089b9d7 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/EditTaskFragment.java
@@ -24,7 +24,6 @@ import android.content.Intent;
import android.database.Cursor;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
-import android.os.Build.VERSION;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
@@ -144,9 +143,10 @@ public class EditTaskFragment extends SupportFragment implements LoaderManager.L
static final ContentValueMapper CONTENT_VALUE_MAPPER = new ContentValueMapper()
.addString(Tasks.ACCOUNT_TYPE, Tasks.ACCOUNT_NAME, Tasks.TITLE, Tasks.LOCATION, Tasks.DESCRIPTION, Tasks.GEO, Tasks.URL, Tasks.TZ, Tasks.DURATION,
- Tasks.LIST_NAME)
+ Tasks.LIST_NAME, Tasks.RRULE, Tasks.RDATE)
.addInteger(Tasks.PRIORITY, Tasks.LIST_COLOR, Tasks.TASK_COLOR, Tasks.STATUS, Tasks.CLASSIFICATION, Tasks.PERCENT_COMPLETE, Tasks.IS_ALLDAY,
- Tasks.IS_CLOSED, Tasks.PINNED).addLong(Tasks.LIST_ID, Tasks.DTSTART, Tasks.DUE, Tasks.COMPLETED, Tasks._ID);
+ Tasks.IS_CLOSED, Tasks.PINNED)
+ .addLong(Tasks.LIST_ID, Tasks.DTSTART, Tasks.DUE, Tasks.COMPLETED, Tasks._ID, Tasks.ORIGINAL_INSTANCE_ID);
private boolean mAppForEdit = true;
private TasksListCursorSpinnerAdapter mTaskListAdapter;
@@ -650,7 +650,6 @@ public class EditTaskFragment extends SupportFragment implements LoaderManager.L
}
int alpha = (int) ((0.5 + 0.5 * percentage) * 255);
viewAccountColor.setBackgroundColor(mListColor);
-
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java
index 110cd2278e91fd4de54fa816c548ccb15dca96f8..b31672119d7edce81a99b0024aaaf879b22c560f 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/QuickAddDialogFragment.java
@@ -23,9 +23,6 @@ import android.database.Cursor;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.CursorLoader;
-import androidx.loader.content.Loader;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.ContextThemeWrapper;
@@ -58,6 +55,13 @@ import org.dmfs.tasks.utils.RecentlyUsedLists;
import org.dmfs.tasks.utils.SafeFragmentUiRunnable;
import org.dmfs.tasks.utils.TasksListCursorSpinnerAdapter;
+import java.util.Objects;
+
+import androidx.core.content.ContextCompat;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.CursorLoader;
+import androidx.loader.content.Loader;
+
/**
* A quick add dialog. It allows the user to enter a new task without having to deal with the full blown editor interface. At present it support task with a
@@ -200,16 +204,12 @@ public class QuickAddDialogFragment extends SupportDialogFragment
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
-
- // create ContextThemeWrapper from the original Activity Context with the custom theme
- final Context contextThemeWrapperLight = new ContextThemeWrapper(getActivity(), R.style.ThemeOverlay_AppCompat_Light);
final Context contextThemeWrapperDark = new ContextThemeWrapper(getActivity(), R.style.Base_Theme_AppCompat);
- LayoutInflater localInflater = inflater.cloneInContext(contextThemeWrapperLight);
- View view = localInflater.inflate(R.layout.fragment_quick_add_dialog, container);
+ View view = inflater.inflate(R.layout.fragment_quick_add_dialog, container);
ViewGroup headerContainer = (ViewGroup) view.findViewById(R.id.header_container);
- localInflater = inflater.cloneInContext(contextThemeWrapperDark);
+ LayoutInflater localInflater = inflater.cloneInContext(contextThemeWrapperDark);
localInflater.inflate(R.layout.fragment_quick_add_dialog_header, headerContainer);
if (savedInstanceState == null)
@@ -238,7 +238,7 @@ public class QuickAddDialogFragment extends SupportDialogFragment
mContent = view.findViewById(R.id.content);
mSaveButton = (TextView)view.findViewById(android.R.id.button1);
- mSaveButton.setTextColor(TaskListActivity.ACCENT_COLOR);
+ mSaveButton.setTextColor(ContextCompat.getColor(requireContext(), R.color.accent));
mSaveButton.setOnClickListener(this);
mSaveAndNextButton = view.findViewById(android.R.id.button2);
mSaveAndNextButton.setOnClickListener(this);
diff --git a/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java b/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java
index 8ec1c64e3e845dcfdb57a2dbd7cd2594c3a33eab..31ea5cc1dd5d8c8a5fed67711b1c1154dc604dc9 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/SettingsListFragment.java
@@ -25,14 +25,9 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.OperationApplicationException;
import android.database.Cursor;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.RemoteException;
-import androidx.fragment.app.ListFragment;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.CursorLoader;
-import androidx.loader.content.Loader;
-import androidx.cursoradapter.widget.CursorAdapter;
-import androidx.appcompat.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -46,6 +41,7 @@ import android.widget.BaseAdapter;
import android.widget.TextView;
import android.widget.Toast;
+import org.dmfs.android.bolts.color.colors.AttributeColor;
import org.dmfs.android.widgets.ColoredShapeCheckBox;
import org.dmfs.provider.tasks.AuthorityUtil;
import org.dmfs.tasks.contract.TaskContract;
@@ -57,6 +53,15 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.graphics.drawable.DrawableCompat;
+import androidx.cursoradapter.widget.CursorAdapter;
+import androidx.fragment.app.ListFragment;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.CursorLoader;
+import androidx.loader.content.Loader;
+
/**
* This fragment is used to display a list of task-providers. It show the task-providers which are visible in main {@link TaskListFragment} and also the
@@ -188,12 +193,9 @@ public class SettingsListFragment extends ListFragment implements AbsListView.On
* Adds an action to the ActionBar to create local lists.
*/
@Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
+ public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater)
{
inflater.inflate(R.menu.list_settings_menu, menu);
-
-
-
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/StaleListBroadcastReceiver.java b/opentasks/src/main/java/org/dmfs/tasks/StaleListBroadcastReceiver.java
index 08540d593c60f82193c8d2ef68758ed22f88763c..3134a4e17f29e926aac638a2df7e239b6cf60ddb 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/StaleListBroadcastReceiver.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/StaleListBroadcastReceiver.java
@@ -18,11 +18,17 @@ package org.dmfs.tasks;
import android.accounts.Account;
import android.accounts.AccountManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Resources;
import android.os.Build;
+import org.dmfs.android.bolts.color.colors.AttributeColor;
import org.dmfs.android.contentpal.RowDataSnapshot;
import org.dmfs.android.contentpal.RowSnapshot;
import org.dmfs.android.contentpal.predicates.AccountEq;
@@ -31,12 +37,13 @@ import org.dmfs.android.contentpal.predicates.EqArg;
import org.dmfs.android.contentpal.predicates.Not;
import org.dmfs.android.contentpal.projections.MultiProjection;
import org.dmfs.android.contentpal.rowsets.QueryRowSet;
-import org.dmfs.iterables.elementary.Seq;
import org.dmfs.jems.iterable.composite.Joined;
import org.dmfs.jems.iterable.decorators.Mapped;
+import org.dmfs.jems.iterable.elementary.Seq;
import org.dmfs.opentaskspal.views.TaskListsView;
import org.dmfs.provider.tasks.AuthorityUtil;
import org.dmfs.tasks.contract.TaskContract;
+import org.dmfs.tasks.utils.ManifestAppName;
import java.util.ArrayList;
@@ -58,25 +65,52 @@ public final class StaleListBroadcastReceiver extends BroadcastReceiver
}
AccountManager accountManager = AccountManager.get(context);
String authority = AuthorityUtil.taskAuthority(context);
- String description = String.format("Please give %s access to the following account", context.getString(R.string.app_name));
+ String description = String.format("Please give %s access to the following account", new ManifestAppName(context).value());
// request access to each account we don't know yet individually
for (Intent accountRequestIntent :
new Mapped<>(
account -> AccountManager.newChooseAccountIntent(account, new ArrayList(singletonList(account)), null,
description, null,
null, null),
- new Mapped, Account>(
+ new Mapped<>(
this::account,
new Mapped<>(RowSnapshot::values,
new QueryRowSet<>(
new TaskListsView(authority, context.getContentResolver().acquireContentProviderClient(authority)),
new MultiProjection<>(TaskContract.TaskLists.ACCOUNT_NAME, TaskContract.TaskLists.ACCOUNT_TYPE),
new Not<>(new AnyOf<>(
- new Joined<>(new Seq<>(
- new EqArg<>(TaskContract.TaskLists.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE)),
+ new Joined<>(
+ new Seq<>(new EqArg<>(TaskContract.TaskLists.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE)),
new Mapped<>(AccountEq::new, new Seq<>(accountManager.getAccounts()))))))))))
{
- context.startActivity(accountRequestIntent);
+ if (Build.VERSION.SDK_INT < 28)
+ {
+ context.startActivity(accountRequestIntent);
+ }
+ else
+ {
+ // on newer Android versions post a notification instead because we can't launch activities from the background anymore
+ String notificationDescription = String.format("%s needs your permission", new ManifestAppName(context).value());
+ NotificationManager nm = context.getSystemService(NotificationManager.class);
+ if (nm != null)
+ {
+ NotificationChannel errorChannel = new NotificationChannel("provider_messages", "Sync Messages", NotificationManager.IMPORTANCE_HIGH);
+ nm.createNotificationChannel(errorChannel);
+ Resources.Theme theme = context.getTheme();
+ theme.applyStyle(context.getApplicationInfo().theme, true);
+
+ nm.notify("stale_list_broadacast", 0,
+ new Notification.Builder(context, "provider_messages")
+ .setContentText(notificationDescription)
+ .setContentIntent(PendingIntent.getActivity(context, 0, accountRequestIntent, PendingIntent.FLAG_UPDATE_CURRENT))
+ .addAction(new Notification.Action.Builder(null, "Grant",
+ PendingIntent.getActivity(context, 0, accountRequestIntent, PendingIntent.FLAG_UPDATE_CURRENT)).build())
+ .setColor(new AttributeColor(theme, R.attr.colorPrimary).argb())
+ .setColorized(true)
+ .setSmallIcon(R.drawable.ic_24_opentasks)
+ .build());
+ }
+ }
}
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java b/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java
index 806ace81c7c415f10757ad1334a16519aa05b462..cfa1520d6d629733017b53ae2f12948862929d27 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/SyncSettingsActivity.java
@@ -36,6 +36,8 @@ import android.text.style.ForegroundColorSpan;
import android.view.View;
import android.widget.Button;
+import com.google.android.material.appbar.AppBarLayout;
+
import org.dmfs.tasks.contract.TaskContract;
import org.dmfs.tasks.utils.BaseActivity;
@@ -56,6 +58,10 @@ public class SyncSettingsActivity extends BaseActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
// Show the Up button in the action bar.
+ AppBarLayout mAppBarLayout = findViewById(R.id.appbar);
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mManager = getSupportFragmentManager();
diff --git a/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java b/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java
index d7a846f664e324cb85a69c7a022549c91b944f50..28081814c1ba0d4940ff9d94923163d484bdf936 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/TaskListActivity.java
@@ -24,8 +24,10 @@ import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
import android.net.Uri;
-import android.os.Build.VERSION;
+import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.Settings;
@@ -36,6 +38,11 @@ import android.view.Window;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.ImageView;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@@ -61,6 +68,20 @@ import org.dmfs.tasks.utils.ExpandableGroupDescriptor;
import org.dmfs.tasks.utils.SearchHistoryHelper;
import org.dmfs.tasks.utils.colors.DarkenedForStatusBar;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.appcompat.widget.SearchView;
+import androidx.appcompat.widget.SearchView.OnQueryTextListener;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.view.MenuItemCompat;
+import androidx.core.view.MenuItemCompat.OnActionExpandListener;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.preference.PreferenceManager;
+import androidx.viewpager.widget.ViewPager;
+import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
+
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SearchView;
@@ -105,6 +126,7 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
public static final String EXTRA_FORCE_LIST_SELECTION = "org.dmfs.tasks.FORCE_LIST_SELECTION";
private final static int REQUEST_CODE_NEW_TASK = 2924;
+ private final static int REQUEST_CODE_PREFS = 2925;
/**
* The time to wait for a new key before updating the search view.
@@ -185,17 +207,15 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
private AppBarLayout mAppBarLayout;
private FloatingActionButton mFloatingActionButton;
- public static int ACCENT_COLOR;
- public static int color_default_green1;
- public static int color_default_blue1;
- public static int color_default_primary_text;
- public static int color_default_primary;
- Toolbar toolbar;
+ private SharedPreferences mPrefs;
@Override
protected void onCreate(Bundle savedInstanceState)
{
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ updateTheme();
super.onCreate(savedInstanceState);
if (mLastUsedColor == android.graphics.Color.TRANSPARENT)
@@ -228,7 +248,7 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
setContentView(R.layout.activity_task_list);
mAppBarLayout = (AppBarLayout) findViewById(R.id.appbar);
- toolbar = (Toolbar) findViewById(R.id.toolbar);
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mAuthority = AuthorityUtil.taskAuthority(this);
@@ -367,9 +387,15 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
{
mFloatingActionButton.setOnClickListener(v -> onAddNewTask());
}
+ }
- ACCENT_COLOR = fetchAccentColor(this);
+ private void updateTheme()
+ {
+ if (Build.VERSION.SDK_INT >= 29)
+ {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
+ }
}
@@ -549,7 +575,7 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent)
{
- if (resultCode == RESULT_OK && intent != null && intent.getData() != null)
+ if (requestCode == REQUEST_CODE_NEW_TASK && resultCode == RESULT_OK && intent != null && intent.getData() != null)
{
// Use the same flow to display the new task as if it was opened from the widget
Intent displayIntent = new Intent(this, TaskListActivity.class);
@@ -563,7 +589,17 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
See https://github.com/dmfs/opentasks/issues/643
and https://stackoverflow.com/questions/42209046/tablayout-icons-disappear-after-viewpager-refresh */
setupTabIcons();
+ return;
}
+ if (requestCode == REQUEST_CODE_PREFS)
+ {
+ updateTheme();
+ if (Build.VERSION.SDK_INT < 29)
+ {
+ recreate();
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, intent);
}
@@ -611,25 +647,7 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
@Override
public void onListColorLoaded(@NonNull Color color)
{
- mLastUsedColor = color.argb();
- if (mTwoPane)
- {
- int colorInt = color.argb();
- getSupportActionBar().setBackgroundDrawable(new ColorDrawable(colorInt));
- mTabs.setBackgroundColor(colorInt);
-
- if (mAppBarLayout != null)
- {
- mAppBarLayout.setBackgroundColor(colorInt);
- }
-
- if (VERSION.SDK_INT >= 21)
- {
- Window window = getWindow();
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- window.setStatusBarColor(new DarkenedForStatusBar(color).argb());
- }
- }
+ // nothing to do
}
@@ -670,18 +688,7 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
}
else if (item.getItemId() == R.id.opentasks_menu_app_settings)
{
- if (VERSION.SDK_INT < 26)
- {
- startActivity(new Intent(this, AppSettingsActivity.class));
- }
- else
- {
- // for now just open the notification settings, which is all we currently support anyway
- Intent intent = new Intent();
- intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
- intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
- startActivity(intent);
- }
+ startActivityForResult(new Intent(this, AppSettingsActivity.class), REQUEST_CODE_PREFS);
return true;
}
else if (item.getItemId() == R.id.opentasks_menu_app_about)
@@ -734,7 +741,7 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
searchClose.setImageResource(R.drawable.ic_close);
ImageView searchIcon = searchView.findViewById(R.id.search_mag_icon);
searchIcon.setImageDrawable(null);
- ImageViewCompat.setImageTintList(searchIcon, ColorStateList.valueOf(ContextCompat.getColor(this, R.color.accent)));
+ ImageViewCompat.setImageTintList(searchIcon, ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_default_primary_text)));
searchView.findViewById(R.id.search_plate).setBackground(null);
@@ -825,54 +832,16 @@ public class TaskListActivity extends BaseActivity implements TaskListFragment.C
return mTransientState;
}
-
- /*
- * get Accent color from OS
- * */
- private int fetchAccentColor(Context context)
- {
-
- color_default_primary_text = ContextCompat.getColor(context, R.color.color_default_primary_text);
- ACCENT_COLOR = ContextCompat.getColor(context, R.color.accent);
- color_default_blue1 = ContextCompat.getColor(context, R.color.color_default_blue1);
- color_default_green1 = ContextCompat.getColor(context, R.color.color_default_green1);
- color_default_primary = ContextCompat.getColor(context, R.color.primary);
- //getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color_accent));
- getSupportActionBar().getThemedContext();
- toolbar.setTitleTextColor(color_default_primary_text);
- toolbar.setOverflowIcon(ContextCompat.getDrawable(context, R.drawable.e_ic_more));
- return ACCENT_COLOR;
- }
-
-
- @ColorInt
- int darkenColor(@ColorInt int color)
- {
- float[] hsv = new float[3];
- android.graphics.Color.colorToHSV(color, hsv);
- hsv[2] *= 0.8f;
- return android.graphics.Color.HSVToColor(hsv);
- }
-
-
- @ColorInt
- int darkenColor20(@ColorInt int color)
- {
- float[] hsv = new float[3];
- android.graphics.Color.colorToHSV(color, hsv);
- hsv[2] *= 0.6f;
- return android.graphics.Color.HSVToColor(hsv);
- }
-
-
- public static void setOverflowButtonColor(final Toolbar toolbar, final int color)
+ @Override
+ public Resources.Theme getTheme()
{
- Drawable drawable = toolbar.getOverflowIcon();
- if (drawable != null)
+ Resources.Theme theme = super.getTheme();
+ if (Build.VERSION.SDK_INT < 29)
{
- drawable = DrawableCompat.wrap(drawable);
- DrawableCompat.setTint(drawable.mutate(), color);
- toolbar.setOverflowIcon(drawable);
+ theme.applyStyle(
+ R.style.OpenTasks_Theme_Default,
+ true);
}
+ return theme;
}
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java b/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java
index 21988a55e62b448f8b587d90ec9ee39bcab86c6f..7c28c20320092dbd24b6286a50920c232a895722 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/TaskListFragment.java
@@ -286,12 +286,6 @@ public class TaskListFragment extends SupportFragment
View rootView = inflater.inflate(R.layout.fragment_expandable_task_list, container, false);
mExpandableListView = (RetainExpandableListView) rootView.findViewById(android.R.id.list);
- if (!mTwoPaneLayout)
- {
- // Add a footer to make sure the floating action button doesn't hide anything.
- mExpandableListView.addFooterView(inflater.inflate(R.layout.task_list_group, mExpandableListView, false));
- }
-
if (mGroupDescriptor == null)
{
loadGroupDescriptor();
diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java
index ccfe035b4da43c3b9170a5f3d9744dd77bd892e2..4862d53ee8ba1f6d6eeba9b4ce41d55e3e8eadff 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskActivity.java
@@ -16,15 +16,10 @@
package org.dmfs.tasks;
-import android.annotation.SuppressLint;
import android.content.Intent;
import android.net.Uri;
-import android.os.Build.VERSION;
import android.os.Bundle;
import android.os.Handler;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.fragment.app.Fragment;
import android.view.MenuItem;
import android.view.Window;
import android.view.WindowManager;
@@ -36,6 +31,10 @@ import org.dmfs.tasks.model.ContentSet;
import org.dmfs.tasks.utils.BaseActivity;
import org.dmfs.tasks.utils.colors.DarkenedForStatusBar;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
/**
* An activity representing a single Task detail screen. This activity is only used on handset devices. On tablet-size devices, item details are presented
@@ -152,16 +151,12 @@ public class ViewTaskActivity extends BaseActivity implements ViewTaskFragment.C
}
- @SuppressLint("NewApi")
@Override
public void onListColorLoaded(@NonNull Color color)
{
- if (VERSION.SDK_INT >= 21)
- {
- Window window = getWindow();
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- window.setStatusBarColor(color.argb());
- }
+ Window window = getWindow();
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ window.setStatusBarColor(color.argb());
}
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java
index 58f304904618dad0f6709ec0059c00073878f05b..26a6f3414d86383e28d0ffc8174352bffebd1109 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java
@@ -21,29 +21,17 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
+import android.content.OperationApplicationException;
import android.content.res.ColorStateList;
import android.database.ContentObserver;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.google.android.material.appbar.AppBarLayout;
-import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-import com.google.android.material.snackbar.Snackbar;
-import androidx.core.app.ActivityCompat;
-import androidx.core.view.MenuItemCompat;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.ShareActionProvider;
-import androidx.appcompat.widget.Toolbar;
-import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener;
+import android.os.RemoteException;
import android.text.TextUtils;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -53,11 +41,28 @@ import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.widget.TextView;
+import com.google.android.material.appbar.AppBarLayout;
+import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.google.android.material.snackbar.Snackbar;
+
import org.dmfs.android.bolts.color.Color;
import org.dmfs.android.bolts.color.elementary.ValueColor;
+import org.dmfs.android.contentpal.Operation;
+import org.dmfs.android.contentpal.operations.BulkDelete;
+import org.dmfs.android.contentpal.predicates.AnyOf;
+import org.dmfs.android.contentpal.predicates.EqArg;
+import org.dmfs.android.contentpal.predicates.IdIn;
+import org.dmfs.android.contentpal.transactions.BaseTransaction;
import org.dmfs.android.retentionmagic.SupportFragment;
import org.dmfs.android.retentionmagic.annotations.Parameter;
import org.dmfs.android.retentionmagic.annotations.Retain;
+import org.dmfs.jems.iterable.adapters.PresentValues;
+import org.dmfs.jems.optional.elementary.NullSafe;
+import org.dmfs.jems.single.combined.Backed;
+import org.dmfs.opentaskspal.tables.InstanceTable;
+import org.dmfs.opentaskspal.tables.TasksTable;
+import org.dmfs.tasks.contract.TaskContract;
import org.dmfs.tasks.contract.TaskContract.Tasks;
import org.dmfs.tasks.model.ContentSet;
import org.dmfs.tasks.model.Model;
@@ -72,9 +77,18 @@ import org.dmfs.tasks.utils.SafeFragmentUiRunnable;
import org.dmfs.tasks.utils.colors.AdjustedForFab;
import org.dmfs.tasks.widget.TaskView;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.ShareActionProvider;
+import androidx.appcompat.widget.Toolbar;
+import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.app.ActivityCompat;
+import androidx.core.view.MenuItemCompat;
/**
@@ -90,16 +104,16 @@ public class ViewTaskFragment extends SupportFragment
private final static String ARG_URI = "uri";
private static final String ARG_STARTING_COLOR = "starting_color";
- /**
- * A set of values that may affect the recurrence set of a task. If one of these values changes we have to submit all of them.
- */
- private final static Set RECURRENCE_VALUES = new HashSet(
- Arrays.asList(Tasks.DUE, Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY, Tasks.RRULE, Tasks.RDATE, Tasks.EXDATE));
-
/**
* The {@link ContentValueMapper} that knows how to map the values in a cursor to {@link ContentValues}.
*/
- private static final ContentValueMapper CONTENT_VALUE_MAPPER = EditTaskFragment.CONTENT_VALUE_MAPPER;
+
+ private static final ContentValueMapper CONTENT_VALUE_MAPPER = new ContentValueMapper()
+ .addString(Tasks.ACCOUNT_TYPE, Tasks.ACCOUNT_NAME, Tasks.TITLE, Tasks.LOCATION, Tasks.DESCRIPTION, Tasks.GEO, Tasks.URL, Tasks.TZ, Tasks.DURATION,
+ Tasks.LIST_NAME, Tasks.RRULE, Tasks.RDATE)
+ .addInteger(Tasks.PRIORITY, Tasks.LIST_COLOR, Tasks.TASK_COLOR, Tasks.STATUS, Tasks.CLASSIFICATION, Tasks.PERCENT_COMPLETE, Tasks.IS_ALLDAY,
+ Tasks.IS_CLOSED, Tasks.PINNED, TaskContract.Instances.IS_RECURRING)
+ .addLong(Tasks.LIST_ID, Tasks.DTSTART, Tasks.DUE, Tasks.COMPLETED, Tasks._ID, Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Instances.TASK_ID);
private static final float PERCENTAGE_TO_HIDE_TITLE_DETAILS = 0.3f;
private static final int ALPHA_ANIMATIONS_DURATION = 200;
@@ -560,38 +574,80 @@ public class ViewTaskFragment extends SupportFragment
}
else if (itemId == R.id.delete_task)
{
- new AlertDialog.Builder(getActivity(), R.style.customAlertDialog)
- .setTitle(R.string.confirm_delete_title)
+ long originalInstanceId = new Backed<>(TaskFieldAdapters.ORIGINAL_INSTANCE_ID.get(mContentSet), () ->
+ Long.valueOf(TaskFieldAdapters.INSTANCE_TASK_ID.get(mContentSet))).value();
+ boolean isRecurring = TaskFieldAdapters.IS_RECURRING_INSTANCE.get(mContentSet);
+ AtomicReference> operation = new AtomicReference<>(
+ new BulkDelete<>(
+ new InstanceTable(mTaskUri.getAuthority()),
+ new IdIn<>(mTaskUri.getLastPathSegment())));
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.customAlertDialog)
.setCancelable(true)
- .setNegativeButton(android.R.string.cancel, new OnClickListener()
- {
- @Override
- public void onClick(DialogInterface dialog, int which)
+ .setNegativeButton(android.R.string.cancel, (dialog, which) -> {
+ // nothing to do here
+ })
+ .setTitle(isRecurring ? R.string.opentasks_task_details_delete_recurring_task : R.string.confirm_delete_title)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ if (mContentSet != null)
{
- // nothing to do here
+ // TODO: remove the task in a background task
+ try
+ {
+ new BaseTransaction()
+ .with(new PresentValues<>(new NullSafe<>(operation.get())))
+ .commit(getContext().getContentResolver().acquireContentProviderClient(mTaskUri));
+ }
+ catch (RemoteException | OperationApplicationException e)
+ {
+ Log.e(ViewTaskFragment.class.getSimpleName(), "Unable to delete task ", e);
+ }
+
+ mCallback.onTaskDeleted(mTaskUri);
+ mTaskUri = null;
}
- }).setPositiveButton(android.R.string.ok, new OnClickListener()
+ });
+ if (isRecurring)
{
- @Override
- public void onClick(DialogInterface dialog, int which)
- {
- if (mContentSet != null)
- {
- // TODO: remove the task in a background task
- mContentSet.delete(mAppContext);
- mCallback.onTaskDeleted(mTaskUri);
- mTaskUri = null;
- }
- }
- }).setMessage(R.string.confirm_delete_message).create().show();
+ builder.setSingleChoiceItems(
+ new CharSequence[] {
+ getString(R.string.opentasks_task_details_delete_this_task),
+ getString(R.string.opentasks_task_details_delete_all_tasks)
+ },
+ 0,
+ (dialog, which) -> {
+ switch (which)
+ {
+ case 0:
+ operation.set(new BulkDelete<>(
+ new InstanceTable(mTaskUri.getAuthority()),
+ new IdIn<>(mTaskUri.getLastPathSegment())));
+ case 1:
+ operation.set(new BulkDelete<>(
+ new TasksTable(mTaskUri.getAuthority()),
+ new AnyOf<>(
+ new IdIn<>(originalInstanceId),
+ new EqArg<>(Tasks.ORIGINAL_INSTANCE_ID, originalInstanceId))));
+
+ }
+ });
+ }
+ else
+ {
+ builder.setMessage(R.string.confirm_delete_message);
+ }
+ builder.create().show();
+
return true;
+
}
else if (itemId == R.id.complete_task)
+
{
completeTask();
return true;
}
else if (itemId == R.id.pin_task)
+
{
if (TaskFieldAdapters.PINNED.get(mContentSet))
{
@@ -607,14 +663,17 @@ public class ViewTaskFragment extends SupportFragment
return true;
}
else if (itemId == R.id.opentasks_send_task)
+
{
setSendMenuIntent();
return false;
}
else
+
{
return super.onOptionsItemSelected(item);
}
+
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyAction.java
index 8bdb226dbb60c331137eb2634f1828be1813ce7e..072f075026d90aaf1072ed0f189e814681fd8cb6 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyAction.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/actions/NotifyAction.java
@@ -29,7 +29,7 @@ import android.os.Build;
import android.os.RemoteException;
import android.text.format.DateUtils;
-import org.dmfs.android.bolts.color.colors.ResourceColor;
+import org.dmfs.android.bolts.color.colors.AttributeColor;
import org.dmfs.android.contentpal.RowDataSnapshot;
import org.dmfs.jems.function.Function;
import org.dmfs.jems.optional.Optional;
@@ -50,6 +50,7 @@ import org.dmfs.tasks.notification.ActionService;
import org.dmfs.tasks.notification.signals.Conditional;
import org.dmfs.tasks.utils.DateFormatter;
+import androidx.appcompat.view.ContextThemeWrapper;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
@@ -171,11 +172,7 @@ public final class NotifyAction implements TaskAction
builder.setOnlyAlertOnce(!mRepost);
builder.setOngoing(pin);
builder.setShowWhen(false);
- if (Build.VERSION.SDK_INT >= 21)
- {
- // don't execute this on Android 4, otherwise no notification will show up
- builder.setGroup(pin ? GROUP_PINS : GROUP_ALERTS);
- }
+ builder.setGroup(pin ? GROUP_PINS : GROUP_ALERTS);
builder.setPriority(pin ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_HIGH);
if (Build.VERSION.SDK_INT < 26)
@@ -183,7 +180,7 @@ public final class NotifyAction implements TaskAction
builder.setDefaults(new Conditional(mRepost, context).value());
}
// TODO: for now we only use the primary app color, later we allow the user to select how to color notifications: default, list, priority
- builder.setColor(new ResourceColor(context, R.color.primary).argb());
+ builder.setColor(new AttributeColor(new ContextThemeWrapper(context, R.style.OpenTasks_Theme_Default), R.attr.colorPrimary).argb());
//builder.setColor(new EffectiveTaskColor(data).argb());
NotificationManagerCompat.from(context).notify("tasks", notificationId, builder.build());
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/PostUndoAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/PostUndoAction.java
index 3395e6f9d614a7e80f161285458f94b3765171e3..e705a278102c280587a4a9b2e77e5bc1d8c7e37b 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/actions/PostUndoAction.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/actions/PostUndoAction.java
@@ -23,13 +23,10 @@ import android.content.Context;
import android.content.Intent;
import android.content.OperationApplicationException;
import android.net.Uri;
-import android.os.Build;
import android.os.RemoteException;
-import androidx.core.app.NotificationCompat;
-import androidx.core.app.NotificationManagerCompat;
import android.widget.RemoteViews;
-import org.dmfs.android.bolts.color.colors.ResourceColor;
+import org.dmfs.android.bolts.color.colors.AttributeColor;
import org.dmfs.android.contentpal.RowDataSnapshot;
import org.dmfs.tasks.R;
import org.dmfs.tasks.contract.TaskContract;
@@ -37,6 +34,10 @@ import org.dmfs.tasks.notification.ActionReceiver;
import org.dmfs.tasks.notification.ActionService;
import org.dmfs.tasks.notification.signals.NoSignal;
+import androidx.appcompat.view.ContextThemeWrapper;
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+
/**
* Post a notification.
@@ -79,12 +80,8 @@ public final class PostUndoAction implements TaskAction
new Intent(context, ActionReceiver.class).setData(taskUri).setAction(ActionService.ACTION_FINISH_COMPLETE),
PendingIntent.FLAG_CANCEL_CURRENT));
builder.setShowWhen(false);
- if (Build.VERSION.SDK_INT >= 21)
- {
- // don't execute this on Android 4, otherwise no notification will show up
- builder.setGroup(GROUP_UNDO);
- }
- builder.setColor(new ResourceColor(context, R.color.primary).argb());
+ builder.setGroup(GROUP_UNDO);
+ builder.setColor(new AttributeColor(new ContextThemeWrapper(context, R.style.OpenTasks_Theme_Default), R.attr.colorPrimary).argb());
NotificationManagerCompat.from(context).notify("tasks.undo", id, builder.build());
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateWidgetsAction.java b/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateWidgetsAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..6541b08008408a3ca48e70c6bf348aa64f65ac5d
--- /dev/null
+++ b/opentasks/src/main/java/org/dmfs/tasks/actions/UpdateWidgetsAction.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.tasks.actions;
+
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.net.Uri;
+
+import org.dmfs.android.contentpal.RowDataSnapshot;
+import org.dmfs.tasks.contract.TaskContract;
+import org.dmfs.tasks.homescreen.TaskListWidgetProvider;
+import org.dmfs.tasks.homescreen.TaskListWidgetProviderLarge;
+import org.dmfs.tasks.R;
+
+
+/**
+ * A {@link TaskAction} that updates the widgets.
+ *
+ * @author Trogel
+ */
+public final class UpdateWidgetsAction implements TaskAction
+{
+ public UpdateWidgetsAction()
+ {
+ }
+
+
+ @Override
+ public void execute(Context context, ContentProviderClient contentProviderClient, RowDataSnapshot rowSnapshot, Uri taskUri)
+ {
+ AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ notifyTaskListDataChanged(appWidgetManager, new ComponentName(context, TaskListWidgetProvider.class));
+ notifyTaskListDataChanged(appWidgetManager, new ComponentName(context, TaskListWidgetProviderLarge.class));
+ }
+
+
+ private void notifyTaskListDataChanged(AppWidgetManager appWidgetManager, ComponentName componentName)
+ {
+ final int[] appWidgetIds = appWidgetManager.getAppWidgetIds(componentName);
+ appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.task_list_widget_lv);
+ }
+}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java
index b5f91de5918a0907805c3fb17f095db7226d0db4..3f7046dafa7a0a8fe3eba91ef639b7ff7ff31184 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java
@@ -17,21 +17,47 @@
package org.dmfs.tasks.groupings;
import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
import android.database.Cursor;
-import androidx.collection.SparseArrayCompat;
-import android.text.TextUtils;
+import android.graphics.drawable.Drawable;
+import android.text.Spannable;
+import android.text.SpannableString;
import android.text.format.Time;
+import android.text.style.DynamicDrawableSpan;
+import android.text.style.ImageSpan;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
+import com.google.android.material.card.MaterialCardView;
+
+import org.dmfs.android.bolts.color.colors.AttributeColor;
+import org.dmfs.iterables.decorators.Sieved;
+import org.dmfs.jems.single.elementary.Reduced;
+import org.dmfs.provider.tasks.model.TaskAdapter;
import org.dmfs.tasks.R;
+import org.dmfs.tasks.model.DescriptionItem;
import org.dmfs.tasks.model.TaskFieldAdapters;
import org.dmfs.tasks.utils.DateFormatter;
import org.dmfs.tasks.utils.DateFormatter.DateFormatContext;
import org.dmfs.tasks.utils.ViewDescriptor;
+import java.util.List;
import java.util.TimeZone;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.collection.SparseArrayCompat;
+import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
+
+import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
+import static org.dmfs.tasks.model.TaskFieldAdapters.IS_CLOSED;
+import static org.dmfs.tasks.model.TaskFieldAdapters.LIST_COLOR_RAW;
/**
@@ -42,6 +68,8 @@ import java.util.TimeZone;
public abstract class BaseTaskViewDescriptor implements ViewDescriptor
{
+ private final static int[] DRAWABLES = new int[] { R.drawable.ic_outline_check_box_24, R.drawable.ic_outline_check_box_outline_blank_24 };
+ private final static Pattern DRAWABLE_PATTERN = Pattern.compile("((?:-\\s*)?\\[[xX]])|((?:-\\s*)?\\[\\s?])");
/**
* We use this to get the current time.
*/
@@ -83,6 +111,10 @@ public abstract class BaseTaskViewDescriptor implements ViewDescriptor
{
view.setTextAppearance(view.getContext(), R.style.task_list_overdue_text);
}
+ else if (isClosed)
+ {
+ view.setTextAppearance(view.getContext(), R.style.task_list_due_text_closed);
+ }
else
{
view.setTextAppearance(view.getContext(), R.style.task_list_due_text);
@@ -101,47 +133,164 @@ public abstract class BaseTaskViewDescriptor implements ViewDescriptor
protected void setOverlay(View view, int position, int count)
{
- View overlayTop = getView(view, R.id.overlay_top);
- View overlayBottom = getView(view, R.id.overlay_bottom);
+ }
+
- if (overlayTop != null)
+ protected void setDescription(View view, Cursor cursor)
+ {
+ Context context = view.getContext();
+ Resources res = context.getResources();
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean isClosed = TaskAdapter.IS_CLOSED.getFrom(cursor);
+ TextView descriptionView = getView(view, android.R.id.text1);
+ int maxDescriptionLines = prefs.getInt(context.getString(R.string.opentasks_pref_appearance_list_description_lines),
+ context.getResources().getInteger(R.integer.opentasks_preferences_description_lines_default));
+
+ List checkList = TaskFieldAdapters.DESCRIPTION_CHECKLIST.get(cursor);
+ if (maxDescriptionLines > 0 && checkList.size() > 0 && !checkList.get(0).checkbox && !isClosed)
+ {
+ descriptionView.setVisibility(View.VISIBLE);
+ descriptionView.setText(withCheckBoxes(descriptionView, checkList.get(0).text));
+ descriptionView.setMaxLines(maxDescriptionLines);
+ }
+ else
{
- overlayTop.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
+ descriptionView.setVisibility(View.GONE);
}
- if (overlayBottom != null)
+ boolean showCheckListSummary = prefs.getBoolean(
+ context.getString(R.string.opentasks_pref_appearance_check_list_summary),
+ res.getBoolean(R.bool.opentasks_list_check_list_summary_default));
+ TextView checkboxItemCountView = getView(view, R.id.checkbox_item_count);
+ Iterable checkedItems = new Sieved<>(item -> item.checkbox, checkList);
+ int checkboxItemCount = new Reduced(() -> 0, (count, ignored) -> count + 1, checkedItems).value();
+ if (checkboxItemCount == 0 || isClosed || !showCheckListSummary)
+ {
+ checkboxItemCountView.setVisibility(View.GONE);
+ }
+ else
{
- overlayBottom.setVisibility(position == count - 1 ? View.VISIBLE : View.GONE);
+ checkboxItemCountView.setVisibility(View.VISIBLE);
+ int checked = new Reduced(() -> 0, (count, ignored) -> count + 1,
+ new Sieved<>(item -> item.checked, checkedItems)).value();
+ if (checked == 0)
+ {
+ checkboxItemCountView.setText(
+ withCheckBoxes(checkboxItemCountView,
+ context.getString(R.string.opentasks_checkbox_item_count_none_checked, checkboxItemCount)));
+ }
+ else if (checked == checkboxItemCount)
+ {
+ checkboxItemCountView.setText(
+ withCheckBoxes(checkboxItemCountView,
+ context.getString(R.string.opentasks_checkbox_item_count_all_checked, checkboxItemCount)));
+ }
+ else
+ {
+ checkboxItemCountView.setText(withCheckBoxes(checkboxItemCountView,
+ context.getString(R.string.opentasks_checkbox_item_count_partially_checked, checkboxItemCount - checked, checked)));
+ }
+ }
+
+ View progressGradient = view.findViewById(R.id.task_progress_background);
+ if (!isClosed && TaskFieldAdapters.PERCENT_COMPLETE.get(cursor) > 0
+ && prefs.getBoolean(context.getString(R.string.opentasks_pref_appearance_progress_gradient),
+ res.getBoolean(R.bool.opentasks_list_progress_gradient_default)))
+ {
+ progressGradient.setVisibility(View.VISIBLE);
+ progressGradient.setPivotX(0);
+ progressGradient.setScaleX(TaskFieldAdapters.PERCENT_COMPLETE.get(cursor) / 100f);
+ }
+ else
+ {
+ progressGradient.setVisibility(View.GONE);
}
}
- protected void setDescription(View view, Cursor cursor)
+ private Spannable withCheckBoxes(
+ @NonNull TextView view,
+ @NonNull String s)
{
- String description = TaskFieldAdapters.DESCRIPTION.get(cursor);
- TextView descriptionView = getView(view, android.R.id.text1);
- if (TextUtils.isEmpty(description))
+ return withDrawable(
+ view,
+ new SpannableString(s),
+ DRAWABLE_PATTERN,
+ DRAWABLES);
+ }
+
+
+ private Spannable withDrawable(
+ @NonNull TextView view,
+ @NonNull Spannable s,
+ @NonNull Pattern pattern,
+ @DrawableRes int[] drawable)
+ {
+ Context context = view.getContext();
+ Matcher matcher = pattern.matcher(s.toString());
+ while (matcher.find())
{
- descriptionView.setVisibility(View.GONE);
+ int idx = matcher.group(1) == null ? 1 : 0;
+ Drawable drawable1 = ContextCompat.getDrawable(context, drawable[idx]);
+ int lineHeight = view.getLineHeight();
+ int additionalSpace = (int) ((lineHeight - view.getTextSize()) / 2);
+ drawable1.setBounds(0, 0, lineHeight + additionalSpace, lineHeight + additionalSpace);
+ drawable1.setTint(view.getCurrentTextColor());
+ s.setSpan(new ImageSpan(drawable1, DynamicDrawableSpan.ALIGN_BOTTOM), matcher.start(), matcher.end(), SPAN_EXCLUSIVE_EXCLUSIVE);
}
- else
+
+ return s;
+ }
+
+
+ protected void setPrio(SharedPreferences prefs, View view, Cursor cursor)
+ {
+ // display priority
+ View prioLabel = getView(view, R.id.priority_label);
+ prioLabel.setAlpha(IS_CLOSED.get(cursor) ? 0.4f : 1f);
+ int priority = TaskFieldAdapters.PRIORITY.get(cursor);
+ if (priority > 0 &&
+ prefs.getBoolean(prioLabel.getContext().getString(R.string.opentasks_pref_appearance_list_show_priority), true))
{
- descriptionView.setVisibility(View.VISIBLE);
- if (description.length() > 150)
+ if (priority < 5)
{
- description = description.substring(0, 150);
+ prioLabel.setBackgroundColor(new AttributeColor(prioLabel.getContext(), R.attr.colorHighPriority).argb());
}
- descriptionView.setText(description);
+ if (priority == 5)
+ {
+ prioLabel.setBackgroundColor(new AttributeColor(prioLabel.getContext(), R.attr.colorMediumPriority).argb());
+ }
+ if (priority > 5)
+ {
+ prioLabel.setBackgroundColor(new AttributeColor(prioLabel.getContext(), R.attr.colorLowPriority).argb());
+ }
+ prioLabel.setVisibility(View.VISIBLE);
+ }
+ else
+ {
+ prioLabel.setVisibility(View.GONE);
}
}
protected void setColorBar(View view, Cursor cursor)
{
- View colorbar = getView(view, R.id.colorbar);
- if (colorbar != null)
+ MaterialCardView cardView = getView(view, R.id.flingContentView);
+ if (cardView != null)
{
- colorbar.setBackgroundColor(TaskFieldAdapters.LIST_COLOR.get(cursor));
+ boolean isClosed = IS_CLOSED.get(cursor);
+ cardView.findViewById(R.id.color_label).setBackgroundColor(LIST_COLOR_RAW.get(cursor));
+ cardView.findViewById(R.id.card_background).setVisibility(isClosed ? View.VISIBLE : View.GONE);
+ cardView.findViewById(R.id.color_label).setAlpha(isClosed ? 0.4f : 1f);
+ cardView.setCardElevation(view.getResources().getDimensionPixelSize(
+ isClosed ?
+ R.dimen.opentasks_tasklist_card_elevation_closed :
+ R.dimen.opentasks_tasklist_card_elevation));
+ ((TextView) cardView.findViewById(android.R.id.title))
+ .setTextColor(new AttributeColor(view.getContext(),
+ isClosed ?
+ android.R.attr.textColorTertiary :
+ android.R.attr.textColorPrimary).argb());
}
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java
index 7d6602ba57a40fb2c56b415346cc7a5c7e68aabb..a6ce2893baf9cf2a915341b18546af152cda9e29 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByDueDate.java
@@ -17,6 +17,7 @@
package org.dmfs.tasks.groupings;
import android.content.Context;
+import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Paint;
@@ -24,23 +25,22 @@ import android.view.View;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
-import org.dmfs.jems.optional.elementary.NullSafe;
-import org.dmfs.jems.single.combined.Backed;
import org.dmfs.tasks.R;
import org.dmfs.tasks.TaskListActivity;
import org.dmfs.tasks.contract.TaskContract.Instances;
import org.dmfs.tasks.groupings.cursorloaders.TimeRangeCursorFactory;
import org.dmfs.tasks.groupings.cursorloaders.TimeRangeCursorLoaderFactory;
import org.dmfs.tasks.groupings.cursorloaders.TimeRangeShortCursorFactory;
-import org.dmfs.tasks.model.TaskFieldAdapters;
import org.dmfs.tasks.utils.ExpandableChildDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter;
import org.dmfs.tasks.utils.ViewDescriptor;
-import org.dmfs.tasks.widget.ProgressBackgroundView;
import java.text.DateFormatSymbols;
+import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
+
/**
* Definition of the by-due grouping.
@@ -78,6 +78,7 @@ public class ByDueDate extends AbstractGroupingFactory
@Override
public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags)
{
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext());
TextView title = getView(view, android.R.id.title);
boolean isClosed = cursor.getInt(13) > 0;
@@ -99,33 +100,7 @@ public class ByDueDate extends AbstractGroupingFactory
setDueDate(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed);
- View divider = getView(view, R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE);
- }
-
- // display priority
- int priority = TaskFieldAdapters.PRIORITY.get(cursor);
- View priorityView = getView(view, R.id.task_priority_view_medium);
- priorityView.setBackgroundResource(android.R.color.transparent);
- priorityView.setVisibility(View.VISIBLE);
-
- if (priority > 0 && priority < 5)
- {
- priorityView.setBackgroundResource(R.color.priority_red);
- }
- if (priority == 5)
- {
- priorityView.setBackgroundResource(R.color.priority_yellow);
- }
- if (priority > 5 && priority <= 9)
- {
- priorityView.setBackgroundResource(R.color.priority_green);
- }
-
- new ProgressBackgroundView(getView(view, R.id.percentage_background_view))
- .update(new NullSafe<>(TaskFieldAdapters.PERCENT_COMPLETE.get(cursor)));
+ setPrio(prefs, view, cursor);
setColorBar(view, cursor);
setDescription(view, cursor);
@@ -180,7 +155,7 @@ public class ByDueDate extends AbstractGroupingFactory
if (title != null)
{
title.setText(getTitle(cursor, view.getContext()));
- title.setTextColor(TaskListActivity.color_default_primary_text);
+ title.setTextColor(ContextCompat.getColor(view.getContext(), R.color.color_default_primary_text));
}
// set list elements
@@ -192,19 +167,6 @@ public class ByDueDate extends AbstractGroupingFactory
text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount));
}
-
- // show/hide divider
- View divider = view.findViewById(R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE);
- }
-
- View colorbar = view.findViewById(R.id.colorbar1);
- if (colorbar != null)
- {
- colorbar.setVisibility(View.GONE);
- }
}
@@ -305,7 +267,6 @@ public class ByDueDate extends AbstractGroupingFactory
}
-
@Override
ExpandableGroupDescriptor makeExpandableGroupDescriptor(String authority)
{
diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java
index 71e98c60d8afeb7e671ea4933671408f31f7d66b..fc9fd4cfdd2603eece584c44b15bc6163df9311d 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByList.java
@@ -17,28 +17,29 @@
package org.dmfs.tasks.groupings;
import android.content.Context;
+import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Paint;
-import androidx.fragment.app.FragmentActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
-import org.dmfs.jems.optional.elementary.NullSafe;
import org.dmfs.tasks.QuickAddDialogFragment;
import org.dmfs.tasks.R;
import org.dmfs.tasks.TaskListActivity;
import org.dmfs.tasks.contract.TaskContract.Instances;
import org.dmfs.tasks.contract.TaskContract.TaskLists;
import org.dmfs.tasks.groupings.cursorloaders.CursorLoaderFactory;
-import org.dmfs.tasks.model.TaskFieldAdapters;
import org.dmfs.tasks.utils.ExpandableChildDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter;
import org.dmfs.tasks.utils.ViewDescriptor;
-import org.dmfs.tasks.widget.ProgressBackgroundView;
+
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.FragmentActivity;
+import androidx.preference.PreferenceManager;
/**
@@ -74,6 +75,7 @@ public class ByList extends AbstractGroupingFactory
@Override
public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags)
{
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext());
TextView title = getView(view, android.R.id.title);
boolean isClosed = cursor.getInt(13) > 0;
@@ -95,34 +97,7 @@ public class ByList extends AbstractGroupingFactory
setDueDate(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed);
- View divider = getView(view, R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE);
- }
-
- // display priority
- int priority = TaskFieldAdapters.PRIORITY.get(cursor);
- View priorityView = getView(view, R.id.task_priority_view_medium);
- priorityView.setBackgroundResource(android.R.color.transparent);
- priorityView.setVisibility(View.VISIBLE);
-
- if (priority > 0 && priority < 5)
- {
- priorityView.setBackgroundResource(R.color.priority_red);
- }
- if (priority == 5)
- {
- priorityView.setBackgroundResource(R.color.priority_yellow);
- }
- if (priority > 5 && priority <= 9)
- {
- priorityView.setBackgroundResource(R.color.priority_green);
- }
-
- new ProgressBackgroundView(getView(view, R.id.percentage_background_view))
- .update(new NullSafe<>(TaskFieldAdapters.PERCENT_COMPLETE.get(cursor)));
-
+ setPrio(prefs, view, cursor);
setColorBar(view, cursor);
setDescription(view, cursor);
setOverlay(view, cursor.getPosition(), cursor.getCount());
@@ -173,7 +148,7 @@ public class ByList extends AbstractGroupingFactory
if (title != null)
{
title.setText(getTitle(cursor, view.getContext()));
- title.setTextColor(TaskListActivity.color_default_primary_text);
+ title.setTextColor(ContextCompat.getColor(view.getContext(), R.color.color_default_primary_text));
}
// set list account
@@ -193,15 +168,6 @@ public class ByList extends AbstractGroupingFactory
text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount));
}
- // show/hide divider
- View divider = view.findViewById(R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE);
- }
-
- View colorbar1 = view.findViewById(R.id.colorbar1);
- View colorbar2 = view.findViewById(R.id.colorbar2);
View quickAddTask = view.findViewById(R.id.quick_add_task);
if (quickAddTask != null)
{
@@ -211,16 +177,6 @@ public class ByList extends AbstractGroupingFactory
if ((flags & FLAG_IS_EXPANDED) != 0)
{
- if (colorbar1 != null)
- {
- colorbar1.setBackgroundColor(TaskFieldAdapters.LIST_COLOR.get(cursor));
- colorbar1.setVisibility(View.VISIBLE);
- }
- if (colorbar2 != null)
- {
- colorbar2.setVisibility(View.GONE);
- }
-
// show quick add and hide task count
if (quickAddTask != null)
{
@@ -233,16 +189,6 @@ public class ByList extends AbstractGroupingFactory
}
else
{
- if (colorbar1 != null)
- {
- colorbar1.setVisibility(View.INVISIBLE);
- }
- if (colorbar2 != null)
- {
- colorbar2.setBackgroundColor(TaskFieldAdapters.LIST_COLOR.get(cursor));
- colorbar2.setVisibility(View.VISIBLE);
- }
-
// hide quick add and show task count
if (quickAddTask != null)
{
diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java
index 89c52a3db14a453a58aa69d8df83dd8135bed855..fc228bc6a2f647b5b3a63e40c6d78b3879afd2e6 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByPriority.java
@@ -17,16 +17,15 @@
package org.dmfs.tasks.groupings;
import android.content.Context;
+import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Paint;
-import androidx.fragment.app.FragmentActivity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
-import org.dmfs.jems.optional.elementary.NullSafe;
import org.dmfs.provider.tasks.AuthorityUtil;
import org.dmfs.tasks.QuickAddDialogFragment;
import org.dmfs.tasks.R;
@@ -41,7 +40,10 @@ import org.dmfs.tasks.utils.ExpandableChildDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter;
import org.dmfs.tasks.utils.ViewDescriptor;
-import org.dmfs.tasks.widget.ProgressBackgroundView;
+
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.FragmentActivity;
+import androidx.preference.PreferenceManager;
/**
@@ -66,6 +68,7 @@ public class ByPriority extends AbstractGroupingFactory
@Override
public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags)
{
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext());
TextView title = getView(view, android.R.id.title);
boolean isClosed = cursor.getInt(13) > 0;
@@ -87,33 +90,7 @@ public class ByPriority extends AbstractGroupingFactory
setDueDate(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed);
- View divider = getView(view, R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE);
- }
-
- // display priority
- int priority = TaskFieldAdapters.PRIORITY.get(cursor);
- View priorityView = getView(view, R.id.task_priority_view_medium);
- priorityView.setBackgroundResource(android.R.color.transparent);
- priorityView.setVisibility(View.VISIBLE);
-
- if (priority > 0 && priority < 5)
- {
- priorityView.setBackgroundResource(R.color.priority_red);
- }
- if (priority == 5)
- {
- priorityView.setBackgroundResource(R.color.priority_yellow);
- }
- if (priority > 5 && priority <= 9)
- {
- priorityView.setBackgroundResource(R.color.priority_green);
- }
-
- new ProgressBackgroundView(getView(view, R.id.percentage_background_view))
- .update(new NullSafe<>(TaskFieldAdapters.PERCENT_COMPLETE.get(cursor)));
+ setPrio(prefs, view, cursor);
setColorBar(view, cursor);
setDescription(view, cursor);
@@ -165,7 +142,7 @@ public class ByPriority extends AbstractGroupingFactory
if (title != null)
{
title.setText(getTitle(cursor, view.getContext()));
- title.setTextColor(TaskListActivity.color_default_primary_text);
+ title.setTextColor(ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text));
}
// set list elements
@@ -178,15 +155,6 @@ public class ByPriority extends AbstractGroupingFactory
text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount));
}
- // show/hide divider
- View divider = view.findViewById(R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE);
- }
-
- View colorbar1 = view.findViewById(R.id.colorbar1);
- View colorbar2 = view.findViewById(R.id.colorbar2);
View quickAddTask = view.findViewById(R.id.quick_add_task);
if (quickAddTask != null)
{
@@ -196,16 +164,6 @@ public class ByPriority extends AbstractGroupingFactory
if ((flags & FLAG_IS_EXPANDED) != 0)
{
- if (colorbar1 != null)
- {
- colorbar1.setBackgroundColor(cursor.getInt(2));
- colorbar1.setVisibility(View.VISIBLE);
- }
- if (colorbar2 != null)
- {
- colorbar2.setVisibility(View.GONE);
- }
-
// show quick add and hide task count
if (quickAddTask != null)
{
@@ -218,16 +176,6 @@ public class ByPriority extends AbstractGroupingFactory
}
else
{
- if (colorbar1 != null)
- {
- colorbar1.setVisibility(View.INVISIBLE);
- }
- if (colorbar2 != null)
- {
- colorbar2.setBackgroundColor(cursor.getInt(2));
- colorbar2.setVisibility(View.VISIBLE);
- }
-
// hide quick add and show task count
if (quickAddTask != null)
{
diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java
index 1816e278d4278a79313e3723e5f1b297f742e882..54916e9292d8a15acbc5eb6267d8534634a3c776 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByProgress.java
@@ -17,6 +17,7 @@
package org.dmfs.tasks.groupings;
import android.content.Context;
+import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Paint;
@@ -24,18 +25,18 @@ import android.view.View;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
-import org.dmfs.jems.optional.elementary.NullSafe;
import org.dmfs.tasks.R;
import org.dmfs.tasks.TaskListActivity;
import org.dmfs.tasks.contract.TaskContract.Instances;
import org.dmfs.tasks.groupings.cursorloaders.ProgressCursorFactory;
import org.dmfs.tasks.groupings.cursorloaders.ProgressCursorLoaderFactory;
-import org.dmfs.tasks.model.TaskFieldAdapters;
import org.dmfs.tasks.utils.ExpandableChildDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter;
import org.dmfs.tasks.utils.ViewDescriptor;
-import org.dmfs.tasks.widget.ProgressBackgroundView;
+
+import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
/**
@@ -59,6 +60,7 @@ public class ByProgress extends AbstractGroupingFactory
@Override
public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags)
{
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext());
TextView title = getView(view, android.R.id.title);
boolean isClosed = cursor.getInt(13) > 0;
@@ -80,33 +82,7 @@ public class ByProgress extends AbstractGroupingFactory
setDueDate(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed);
- View divider = getView(view, R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE);
- }
-
- // display priority
- int priority = TaskFieldAdapters.PRIORITY.get(cursor);
- View priorityView = getView(view, R.id.task_priority_view_medium);
- priorityView.setBackgroundResource(android.R.color.transparent);
- priorityView.setVisibility(View.VISIBLE);
-
- if (priority > 0 && priority < 5)
- {
- priorityView.setBackgroundResource(R.color.priority_red);
- }
- if (priority == 5)
- {
- priorityView.setBackgroundResource(R.color.priority_yellow);
- }
- if (priority > 5 && priority <= 9)
- {
- priorityView.setBackgroundResource(R.color.priority_green);
- }
-
- new ProgressBackgroundView(getView(view, R.id.percentage_background_view))
- .update(new NullSafe<>(TaskFieldAdapters.PERCENT_COMPLETE.get(cursor)));
+ setPrio(prefs, view, cursor);
setColorBar(view, cursor);
setDescription(view, cursor);
@@ -158,7 +134,7 @@ public class ByProgress extends AbstractGroupingFactory
if (title != null)
{
title.setText(getTitle(cursor, view.getContext()));
- title.setTextColor(TaskListActivity.color_default_primary_text);
+ title.setTextColor(ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text));
}
// set list elements
@@ -170,41 +146,6 @@ public class ByProgress extends AbstractGroupingFactory
text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount));
}
-
- // show/hide divider
- View divider = view.findViewById(R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE);
- }
-
- View colorbar1 = view.findViewById(R.id.colorbar1);
- View colorbar2 = view.findViewById(R.id.colorbar2);
-
- if ((flags & FLAG_IS_EXPANDED) != 0)
- {
- if (colorbar1 != null)
- {
- colorbar1.setBackgroundColor(cursor.getInt(2));
- colorbar1.setVisibility(View.GONE);
- }
- if (colorbar2 != null)
- {
- colorbar2.setVisibility(View.GONE);
- }
- }
- else
- {
- if (colorbar1 != null)
- {
- colorbar1.setVisibility(View.GONE);
- }
- if (colorbar2 != null)
- {
- colorbar2.setBackgroundColor(cursor.getInt(2));
- colorbar2.setVisibility(View.GONE);
- }
- }
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java
index f3079bedda9e1b9735eb1d372b0a66c50b7c1e0b..568209258644b7f93b49e097c2892ad0c1959c22 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/BySearch.java
@@ -18,6 +18,7 @@ package org.dmfs.tasks.groupings;
import android.annotation.SuppressLint;
import android.content.Context;
+import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Paint;
@@ -30,7 +31,6 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
-import org.dmfs.jems.optional.elementary.NullSafe;
import org.dmfs.tasks.R;
import org.dmfs.tasks.TaskListActivity;
import org.dmfs.tasks.contract.TaskContract.Instances;
@@ -45,7 +45,9 @@ import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper;
import org.dmfs.tasks.utils.SearchHistoryDatabaseHelper.SearchHistoryColumns;
import org.dmfs.tasks.utils.SearchHistoryHelper;
import org.dmfs.tasks.utils.ViewDescriptor;
-import org.dmfs.tasks.widget.ProgressBackgroundView;
+
+import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
/**
@@ -71,6 +73,7 @@ public class BySearch extends AbstractGroupingFactory
@Override
public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags)
{
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext());
TextView title = getView(view, android.R.id.title);
boolean isClosed = TaskFieldAdapters.IS_CLOSED.get(cursor);
@@ -93,33 +96,7 @@ public class BySearch extends AbstractGroupingFactory
setDueDate(getView(view, R.id.task_due_date), null, INSTANCE_DUE_ADAPTER.get(cursor), isClosed);
- View divider = getView(view, R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE);
- }
-
- // display priority
- int priority = TaskFieldAdapters.PRIORITY.get(cursor);
- View priorityView = getView(view, R.id.task_priority_view_medium);
- priorityView.setBackgroundResource(android.R.color.transparent);
- priorityView.setVisibility(View.VISIBLE);
-
- if (priority > 0 && priority < 5)
- {
- priorityView.setBackgroundResource(R.color.priority_red);
- }
- if (priority == 5)
- {
- priorityView.setBackgroundResource(R.color.priority_yellow);
- }
- if (priority > 5 && priority <= 9)
- {
- priorityView.setBackgroundResource(R.color.priority_green);
- }
-
- new ProgressBackgroundView(getView(view, R.id.percentage_background_view))
- .update(new NullSafe<>(TaskFieldAdapters.PERCENT_COMPLETE.get(cursor)));
+ setPrio(prefs, view, cursor);
setColorBar(view, cursor);
setDescription(view, cursor);
@@ -173,7 +150,7 @@ public class BySearch extends AbstractGroupingFactory
if (title != null)
{
title.setText(groupTitle);
- title.setTextColor(TaskListActivity.color_default_primary_text);
+ title.setTextColor(ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text));
}
// set search time
@@ -194,25 +171,6 @@ public class BySearch extends AbstractGroupingFactory
text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount));
}
- // show/hide divider
- View divider = view.findViewById(R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE);
- }
-
- View colorbar1 = view.findViewById(R.id.colorbar1);
- View colorbar2 = view.findViewById(R.id.colorbar2);
-
- if (colorbar1 != null)
- {
- colorbar1.setVisibility(View.GONE);
- }
- if (colorbar2 != null)
- {
- colorbar2.setVisibility(View.GONE);
- }
-
View removeSearch = view.findViewById(R.id.quick_add_task);
if (removeSearch != null)
{
diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java
index 516a01b8a75268427071ace9d4b8dd5b2316d576..1b2cd7af0f6f5e2294cf0270da5d6825875f913d 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java
@@ -17,6 +17,7 @@
package org.dmfs.tasks.groupings;
import android.content.Context;
+import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Paint;
@@ -26,21 +27,21 @@ import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.TextView;
-import org.dmfs.jems.optional.elementary.NullSafe;
import org.dmfs.tasks.R;
import org.dmfs.tasks.TaskListActivity;
import org.dmfs.tasks.contract.TaskContract.Instances;
import org.dmfs.tasks.groupings.cursorloaders.TimeRangeCursorFactory;
import org.dmfs.tasks.groupings.cursorloaders.TimeRangeStartCursorFactory;
import org.dmfs.tasks.groupings.cursorloaders.TimeRangeStartCursorLoaderFactory;
-import org.dmfs.tasks.model.TaskFieldAdapters;
import org.dmfs.tasks.utils.DateFormatter;
import org.dmfs.tasks.utils.DateFormatter.DateFormatContext;
import org.dmfs.tasks.utils.ExpandableChildDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptor;
import org.dmfs.tasks.utils.ExpandableGroupDescriptorAdapter;
import org.dmfs.tasks.utils.ViewDescriptor;
-import org.dmfs.tasks.widget.ProgressBackgroundView;
+
+import androidx.core.content.ContextCompat;
+import androidx.preference.PreferenceManager;
/**
@@ -64,6 +65,7 @@ public class ByStartDate extends AbstractGroupingFactory
@Override
public void populateView(View view, Cursor cursor, BaseExpandableListAdapter adapter, int flags)
{
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext());
TextView title = getView(view, android.R.id.title);
boolean isClosed = cursor.getInt(13) > 0;
@@ -112,33 +114,7 @@ public class ByStartDate extends AbstractGroupingFactory
}
}
- View divider = getView(view, R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_LAST_CHILD) != 0 ? View.GONE : View.VISIBLE);
- }
-
- // display priority
- int priority = TaskFieldAdapters.PRIORITY.get(cursor);
- View priorityView = getView(view, R.id.task_priority_view_medium);
- priorityView.setBackgroundResource(android.R.color.transparent);
- priorityView.setVisibility(View.VISIBLE);
-
- if (priority > 0 && priority < 5)
- {
- priorityView.setBackgroundResource(R.color.priority_red);
- }
- if (priority == 5)
- {
- priorityView.setBackgroundResource(R.color.priority_yellow);
- }
- if (priority > 5 && priority <= 9)
- {
- priorityView.setBackgroundResource(R.color.priority_green);
- }
-
- new ProgressBackgroundView(getView(view, R.id.percentage_background_view))
- .update(new NullSafe<>(TaskFieldAdapters.PERCENT_COMPLETE.get(cursor)));
+ setPrio(prefs, view, cursor);
setColorBar(view, cursor);
setDescription(view, cursor);
@@ -189,7 +165,7 @@ public class ByStartDate extends AbstractGroupingFactory
if (title != null)
{
title.setText(getTitle(cursor, view.getContext()));
- title.setTextColor(TaskListActivity.color_default_primary_text);
+ title.setTextColor(ContextCompat.getColor(title.getContext(), R.color.color_default_primary_text));
}
// set list elements
@@ -201,19 +177,6 @@ public class ByStartDate extends AbstractGroupingFactory
text2.setText(res.getQuantityString(R.plurals.number_of_tasks, childrenCount, childrenCount));
}
-
- // show/hide divider
- View divider = view.findViewById(R.id.divider);
- if (divider != null)
- {
- divider.setVisibility((flags & FLAG_IS_EXPANDED) != 0 && childrenCount > 0 ? View.VISIBLE : View.GONE);
- }
-
- View colorbar = view.findViewById(R.id.colorbar1);
- if (colorbar != null)
- {
- colorbar.setVisibility(View.GONE);
- }
}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java
index ea8b9c8e50557308ed8f04a092ffafb50c28d162..68c662898c4d7c444e34073b6938df77d5b043db 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java
@@ -233,7 +233,7 @@ public class TaskListWidgetUpdaterService extends RemoteViewsService
row.setTextViewText(android.R.id.text1, mDueDateFormatter.format(dueDate, DateFormatContext.WIDGET_VIEW));
// highlight overdue dates & times
- if ((!dueDate.allDay && dueDate.before(mNow) || dueDate.allDay
+ if ((!dueDate.allDay && Time.compare(dueDate, mNow) <= 0 || dueDate.allDay
&& (dueDate.year < mNow.year || dueDate.yearDay <= mNow.yearDay && dueDate.year == mNow.year))
&& !items[position].getIsClosed())
{
diff --git a/opentasks/src/main/java/org/dmfs/tasks/linkify/ActionModeLinkify.java b/opentasks/src/main/java/org/dmfs/tasks/linkify/ActionModeLinkify.java
new file mode 100644
index 0000000000000000000000000000000000000000..d11fc54fcf7c196e78d2bda070d2a0d956421170
--- /dev/null
+++ b/opentasks/src/main/java/org/dmfs/tasks/linkify/ActionModeLinkify.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2021 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.tasks.linkify;
+
+import android.annotation.SuppressLint;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Build;
+import android.telephony.PhoneNumberUtils;
+import android.text.Layout;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+
+import org.dmfs.jems.iterable.elementary.Seq;
+import org.dmfs.jems.procedure.composite.ForEach;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import androidx.annotation.NonNull;
+import io.reactivex.Completable;
+
+import static io.reactivex.Completable.ambArray;
+import static org.dmfs.tasks.linkify.ViewObservables.activityTouchEvents;
+import static org.dmfs.tasks.linkify.ViewObservables.textChanges;
+
+
+/**
+ * Adds clickable links that, on click, present a (floating) action mode to the user (unless running on Android 5).
+ */
+public class ActionModeLinkify
+{
+ private final static String TEL_PATTERN = "(?:\\+\\d{1,5}\\s*)?(?:\\(\\d{1,6}\\)\\s*)?\\d[-, \\.\\/\\d]{4,}\\d";
+ private final static String URL_PATTERN = "(?:https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|www\\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\\.[^\\s]{2,}|https?:\\/\\/(?:www\\.|(?!www))[a-zA-Z0-9]+\\.[^\\s]{2,}|www\\.[a-zA-Z0-9]+\\.[^\\s]{2,})";
+ private final static String MAIL_PATTERN = "(?:[a-zA-Z\\d!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-zA-Z\\d!#$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z\\d](?:[a-z\\d-]*[a-z\\d])?\\.)+[a-z\\d](?:[a-z\\d-]*[a-z\\d])?|\\[(?:(?:(?:2(?:5[0-5]|[0-4]\\d)|1\\d\\d|\\d?\\d))\\.){3}(?:(?:2(?:5[0-5]|[0-4]\\d)|1\\d\\d|\\d?\\d)|[a-z\\d-]*[a-z\\d]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])";
+ private final static Pattern LINK_PATTERN = Pattern.compile(String.format("(?:^|\\s+)((%s)|(%s)|(%s))(?:\\s+|$)", TEL_PATTERN, URL_PATTERN, MAIL_PATTERN));
+
+
+ public interface ActionModeListener
+ {
+ boolean prepareMenu(TextView view, Uri uri, Menu menu);
+
+ boolean onClick(TextView view, Uri uri, MenuItem item);
+ }
+
+
+ public static void linkify(TextView textView, ActionModeListener listener)
+ {
+ CharSequence text = textView.getText();
+ Matcher m = LINK_PATTERN.matcher(text);
+ SpannableString s = new SpannableString(text);
+ new ForEach<>(new Seq<>(s.getSpans(0, s.length(), ClickableSpan.class))).process(s::removeSpan);
+ int pos = 0;
+ while (m.find(pos))
+ {
+ int start = m.start(1);
+ int end = m.end(1);
+ pos = end;
+ Uri uri = null;
+ if (m.group(2) != null)
+ {
+ uri = Uri.parse("tel:" + PhoneNumberUtils.normalizeNumber(m.group(2)));
+ }
+ else if (m.group(3) != null)
+ {
+ uri = Uri.parse(m.group(3));
+ if (uri.getScheme() == null)
+ {
+ uri = uri.buildUpon().scheme("https").build();
+ }
+ }
+ else if (m.group(4) != null)
+ {
+ uri = Uri.parse("mailto:" + m.group(4));
+ }
+ Uri finalUri = uri;
+ s.setSpan(new ClickableSpan()
+ {
+ @SuppressLint("CheckResult")
+ @Override
+ public void onClick(@NonNull View widget)
+ {
+ if (Build.VERSION.SDK_INT >= 23)
+ {
+ Completable closeActionTrigger = ambArray(
+ textChanges(textView).firstElement().ignoreElement(),
+ activityTouchEvents(textView).firstElement().ignoreElement())
+ .cache();
+
+ ActionMode.Callback2 actionMode = new ActionMode.Callback2()
+ {
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu)
+ {
+ return listener.prepareMenu(textView, finalUri, menu);
+ }
+
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu)
+ {
+ return true;
+ }
+
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item)
+ {
+ return listener.onClick(textView, finalUri, item);
+ }
+
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode)
+ {
+ closeActionTrigger.subscribe().dispose();
+ }
+
+
+ @Override
+ public void onGetContentRect(ActionMode mode, View view, Rect outRect)
+ {
+ Layout layout = textView.getLayout();
+ int firstLine = layout.getLineForOffset(start);
+ int lastLine = layout.getLineForOffset(end);
+ layout.getLineBounds(firstLine, outRect);
+ if (firstLine == lastLine)
+ {
+ outRect.left = (int) layout.getPrimaryHorizontal(start);
+ outRect.right = (int) layout.getPrimaryHorizontal(end);
+ }
+ else
+ {
+ Rect lastLineBounds = new Rect();
+ layout.getLineBounds(lastLine, lastLineBounds);
+ outRect.bottom = lastLineBounds.bottom;
+ }
+ }
+ };
+
+ ActionMode am = textView.startActionMode(actionMode, android.view.ActionMode.TYPE_FLOATING);
+ if (am != null)
+ {
+ closeActionTrigger.subscribe(am::finish);
+ }
+ }
+ }
+ },
+ start,
+ end,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ textView.setText(s);
+ }
+}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/linkify/ViewObservables.java b/opentasks/src/main/java/org/dmfs/tasks/linkify/ViewObservables.java
new file mode 100644
index 0000000000000000000000000000000000000000..c6f65c393f80bb60412fe4ce7a3da6b37897e061
--- /dev/null
+++ b/opentasks/src/main/java/org/dmfs/tasks/linkify/ViewObservables.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2021 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.tasks.linkify;
+
+import android.annotation.SuppressLint;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import io.reactivex.Observable;
+import io.reactivex.disposables.Disposables;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
+
+public final class ViewObservables
+{
+ public static Observable textChanges(TextView view)
+ {
+ return Observable.create(emitter -> {
+ TextWatcher textWatcher = new TextWatcher()
+ {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after)
+ {
+ // nothing
+ }
+
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count)
+ {
+ // nothing
+ }
+
+
+ @Override
+ public void afterTextChanged(Editable s)
+ {
+ emitter.onNext(s);
+ }
+ };
+ emitter.setDisposable(Disposables.fromRunnable(() -> view.removeTextChangedListener(textWatcher)));
+ view.addTextChangedListener(textWatcher);
+ });
+ }
+
+
+ @SuppressLint("ClickableViewAccessibility")
+ public static Observable activityTouchEvents(View view)
+ {
+ return Observable.create(emitter -> {
+ // set up a trap to receive touch events outside the ActionMode view.
+ View touchTrap = new View(view.getContext());
+ touchTrap.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ ViewGroup root = (ViewGroup) view.getRootView();
+ root.addView(touchTrap);
+
+ emitter.setDisposable(Disposables.fromRunnable(() -> {
+ touchTrap.setOnTouchListener(null);
+ root.removeView(touchTrap);
+ }));
+
+ touchTrap.setOnTouchListener((v, event) -> {
+ emitter.onNext(event);
+ return false;
+ });
+ });
+ }
+}
\ No newline at end of file
diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java b/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java
index f49fd8af1122e6b5e4c17b667159907eedac1798..031ab11abde01d7777d3ee0f0f1ad2405e3aa7d1 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/model/DefaultModel.java
@@ -38,8 +38,8 @@ public class DefaultModel extends Model
private final static LayoutDescriptor TEXT_EDIT = new LayoutDescriptor(R.layout.text_field_editor);
private final static LayoutDescriptor TEXT_EDIT_SINGLE_LINE = new LayoutDescriptor(R.layout.text_field_editor).setOption(LayoutDescriptor.OPTION_MULTILINE,
false);
- private final static LayoutDescriptor CHECKLIST_VIEW = new LayoutDescriptor(R.layout.checklist_field_view);
- private final static LayoutDescriptor CHECKLIST_EDIT = new LayoutDescriptor(R.layout.checklist_field_editor);
+ private final static LayoutDescriptor DESCRIPTION_VIEW = new LayoutDescriptor(R.layout.description_field_view);
+ private final static LayoutDescriptor DESCRIPTION_EDIT = new LayoutDescriptor(R.layout.description_field_editor);
private final static LayoutDescriptor CHOICES_VIEW = new LayoutDescriptor(R.layout.choices_field_view);
private final static LayoutDescriptor CHOICES_EDIT = new LayoutDescriptor(R.layout.choices_field_editor);
private final static LayoutDescriptor PROGRESS_VIEW = new LayoutDescriptor(R.layout.percentage_field_view);
@@ -53,6 +53,7 @@ public class DefaultModel extends Model
private final static LayoutDescriptor BOOLEAN_EDIT = new LayoutDescriptor(R.layout.boolean_field_editor);
private final static LayoutDescriptor URL_VIEW = new LayoutDescriptor(R.layout.url_field_view);
private final static LayoutDescriptor URL_EDIT = new LayoutDescriptor(R.layout.url_field_editor);
+ private final static LayoutDescriptor RRULE_EDIT = new LayoutDescriptor(R.layout.opentasks_rrule_field_editor);
final static LayoutDescriptor LIST_COLOR_VIEW = new LayoutDescriptor(R.layout.list_color_view);
@@ -99,32 +100,33 @@ public class DefaultModel extends Model
// status
addField(new FieldDescriptor(context, R.id.task_field_status, R.string.task_status, TaskFieldAdapters.STATUS).setViewLayout(CHOICES_VIEW)
- .setEditorLayout(CHOICES_EDIT).setChoices(aca).setIcon(R.drawable.ic_detail_status));
+ .setEditorLayout(CHOICES_EDIT).setChoices(aca).setIcon(R.drawable.ic_info_24));
// location
addField(new FieldDescriptor(context, R.id.task_field_location, R.string.task_location, TaskFieldAdapters.LOCATION).setViewLayout(LOCATION_VIEW)
- .setEditorLayout(TEXT_EDIT).setIcon(R.drawable.ic_detail_location));
+ .setEditorLayout(TEXT_EDIT).setIcon(R.drawable.ic_location_on_24));
// description
- addField(new FieldDescriptor(context, R.id.task_field_description, R.string.task_description, TaskFieldAdapters.DESCRIPTION)
- .setViewLayout(TEXT_VIEW.setOption(LayoutDescriptor.OPTION_LINKIFY, Linkify.ALL)).setEditorLayout(TEXT_EDIT)
- .setIcon(R.drawable.ic_detail_description));
-
- // description
- addField(new FieldDescriptor(context, R.id.task_field_checklist, R.string.task_checklist, TaskFieldAdapters.CHECKLIST).setViewLayout(CHECKLIST_VIEW)
- .setEditorLayout(CHECKLIST_EDIT).setIcon(R.drawable.ic_detail_checklist));
+ addField(new FieldDescriptor(context, R.id.task_field_checklist, R.string.task_description, TaskFieldAdapters.DESCRIPTION_CHECKLIST)
+ .setViewLayout(DESCRIPTION_VIEW)
+ .setEditorLayout(DESCRIPTION_EDIT)
+ .setIcon(R.drawable.ic_notes_24));
// start
addField(new FieldDescriptor(context, R.id.task_field_dtstart, R.string.task_start, TaskFieldAdapters.DTSTART).setViewLayout(TIME_VIEW)
- .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_start));
+ .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_timer_24));
// due
addField(new FieldDescriptor(context, R.id.task_field_due, R.string.task_due, TaskFieldAdapters.DUE).setEditorLayout(TIME_EDIT).setIcon(
- R.drawable.ic_detail_due));
+ R.drawable.ic_alarm_24));
// all day flag
addField(new FieldDescriptor(context, R.id.task_field_all_day, R.string.task_all_day, TaskFieldAdapters.ALLDAY).setEditorLayout(BOOLEAN_EDIT));
+ // rrule
+ addField(new FieldDescriptor(context, R.id.task_field_rrule, R.string.task_recurrence, TaskFieldAdapters.RRULE)
+ .setEditorLayout(RRULE_EDIT).setIcon(R.drawable.ic_baseline_repeat_24));
+
TimeZoneChoicesAdapter tzaca = new TimeZoneChoicesAdapter(context);
// time zone
addField(new FieldDescriptor(context, R.id.task_field_timezone, R.string.task_timezone, TaskFieldAdapters.TIMEZONE).setEditorLayout(CHOICES_EDIT)
@@ -132,11 +134,11 @@ public class DefaultModel extends Model
// completed
addField(new FieldDescriptor(context, R.id.task_field_completed, R.string.task_completed, TaskFieldAdapters.COMPLETED).setViewLayout(TIME_VIEW)
- .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_completed));
+ .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_flag_24));
// percent complete
addField(new FieldDescriptor(context, R.id.task_field_percent_complete, R.string.task_percent_complete, TaskFieldAdapters.PERCENT_COMPLETE)
- .setViewLayout(PROGRESS_VIEW).setEditorLayout(PROGRESS_EDIT).setIcon(R.drawable.ic_detail_progress));
+ .setViewLayout(PROGRESS_VIEW).setEditorLayout(PROGRESS_EDIT).setIcon(R.drawable.ic_progress_done_24));
ArrayChoicesAdapter aca2 = new ArrayChoicesAdapter();
aca2.addChoice(null, context.getString(R.string.priority_undefined), null);
@@ -153,7 +155,7 @@ public class DefaultModel extends Model
// priority
addField(new FieldDescriptor(context, R.id.task_field_priority, R.string.task_priority, TaskFieldAdapters.PRIORITY).setViewLayout(CHOICES_VIEW)
- .setEditorLayout(CHOICES_EDIT).setChoices(aca2).setIcon(R.drawable.ic_detail_priority));
+ .setEditorLayout(CHOICES_EDIT).setChoices(aca2).setIcon(R.drawable.ic_error_24));
ArrayChoicesAdapter aca3 = new ArrayChoicesAdapter();
aca3.addChoice(null, context.getString(R.string.classification_not_specified), null);
@@ -163,15 +165,15 @@ public class DefaultModel extends Model
// privacy
addField(new FieldDescriptor(context, R.id.task_field_classification, R.string.task_classification, TaskFieldAdapters.CLASSIFICATION)
- .setViewLayout(CHOICES_VIEW).setEditorLayout(CHOICES_EDIT).setChoices(aca3).setIcon(R.drawable.ic_detail_visibility));
+ .setViewLayout(CHOICES_VIEW).setEditorLayout(CHOICES_EDIT).setChoices(aca3).setIcon(R.drawable.ic_lock_24));
// url
addField(new FieldDescriptor(context, R.id.task_field_url, R.string.task_url, TaskFieldAdapters.URL).setViewLayout(URL_VIEW).setEditorLayout(URL_EDIT)
- .setIcon(R.drawable.ic_detail_url));
+ .setIcon(R.drawable.ic_link_24));
// task list name
addField(new FieldDescriptor(context, R.id.task_field_list_and_account_name, R.string.task_list, null, TaskFieldAdapters.LIST_AND_ACCOUNT_NAME)
- .setViewLayout(TEXT_VIEW_NO_LINKS).setIcon(R.drawable.ic_detail_list));
+ .setViewLayout(TEXT_VIEW_NO_LINKS).setIcon(R.drawable.ic_list_24));
setAllowRecurrence(false);
setAllowExceptions(false);
diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java b/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java
new file mode 100644
index 0000000000000000000000000000000000000000..10019dbe62385894461c09c215309183582dd9c6
--- /dev/null
+++ b/opentasks/src/main/java/org/dmfs/tasks/model/DescriptionItem.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 dmfs GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dmfs.tasks.model;
+
+import androidx.annotation.Nullable;
+
+
+/**
+ * A bloody POJO o_O to store a description/check list item
+ */
+public final class DescriptionItem
+{
+ public boolean checkbox;
+ public boolean checked;
+ public String text;
+
+
+ public DescriptionItem(boolean checkbox, boolean checked, String text)
+ {
+ this.checkbox = checkbox;
+ this.checked = checked;
+ this.text = text;
+ }
+
+
+ @Override
+ public boolean equals(@Nullable Object obj)
+ {
+ return obj instanceof DescriptionItem
+ && ((DescriptionItem) obj).checkbox == checkbox
+ && ((DescriptionItem) obj).checked == checked
+ && ((DescriptionItem) obj).text.equals(text);
+ }
+
+
+ @Override
+ public int hashCode()
+ {
+ return text.hashCode() * 31 + (checkbox ? 1 : 0) + (checked ? 2 : 0);
+ }
+}
diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java b/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java
index 417ec55c532203fc8f32ac14c8252ffbf70f39ce..d6cddacafb2bc43dd6c59a1384d3bbf66d06eb12 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/model/MinimalModel.java
@@ -60,7 +60,7 @@ public class MinimalModel extends Model
// due
addField(new FieldDescriptor(context, R.id.task_field_due, R.string.task_due, TaskFieldAdapters.DUE).setViewLayout(TIME_VIEW_ADD_BUTTON)
- .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_detail_due));
+ .setEditorLayout(TIME_EDIT).setIcon(R.drawable.ic_alarm_24));
setAllowRecurrence(false);
setAllowExceptions(false);
diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/Model.java b/opentasks/src/main/java/org/dmfs/tasks/model/Model.java
index 2899c85c23df8e60de78943eef2efd7bc54d21bc..889b8b0216af748709770d22e0d91f61b2d73f92 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/model/Model.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/model/Model.java
@@ -22,16 +22,22 @@ import android.content.ComponentName;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
-import androidx.collection.SparseArrayCompat;
import android.text.TextUtils;
+import org.dmfs.iterables.decorators.Sieved;
+import org.dmfs.jems.optional.adapters.First;
+import org.dmfs.jems.single.combined.Backed;
import org.dmfs.provider.tasks.AuthorityUtil;
+import org.dmfs.provider.tasks.utils.Range;
import org.dmfs.tasks.ManageListActivity;
import org.dmfs.tasks.contract.TaskContract.TaskLists;
import java.util.ArrayList;
import java.util.List;
+import androidx.annotation.IdRes;
+import androidx.collection.SparseArrayCompat;
+
/**
* An abstract model class.
@@ -95,6 +101,24 @@ public abstract class Model
}
+ /**
+ * Adds another field (identified by its field descriptor) to this model.
+ *
+ * @param descriptor
+ * The {@link FieldDescriptor} of the field to add.
+ */
+ protected void addFieldAfter(@IdRes int id, FieldDescriptor descriptor)
+ {
+ mFields.add(
+ new Backed<>(
+ new First<>(
+ new Sieved<>(i -> mFields.get(i).getFieldId() == id,
+ new Range(0, mFields.size()))), mFields::size).value(),
+ descriptor);
+ mFieldIndex.put(descriptor.getFieldId(), descriptor);
+ }
+
+
public FieldDescriptor getField(int fieldId)
{
return mFieldIndex.get(fieldId, null);
diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java b/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java
index f1195736dcaad7335f5e78f701a278ac4b0fbf90..adbf0325b00edd2bad473b10b243ea50e0bf959a 100644
--- a/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java
+++ b/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java
@@ -18,17 +18,24 @@ package org.dmfs.tasks.model;
import android.text.format.Time;
+import org.dmfs.jems.optional.Optional;
+import org.dmfs.rfc5545.DateTime;
+import org.dmfs.rfc5545.recur.RecurrenceRule;
import org.dmfs.tasks.contract.TaskContract;
import org.dmfs.tasks.contract.TaskContract.Tasks;
import org.dmfs.tasks.model.adapters.BooleanFieldAdapter;
import org.dmfs.tasks.model.adapters.ChecklistFieldAdapter;
import org.dmfs.tasks.model.adapters.ColorFieldAdapter;
import org.dmfs.tasks.model.adapters.CustomizedDefaultFieldAdapter;
+import org.dmfs.tasks.model.adapters.DateTimeFieldAdapter;
+import org.dmfs.tasks.model.adapters.DescriptionFieldAdapter;
import org.dmfs.tasks.model.adapters.DescriptionStringFieldAdapter;
import org.dmfs.tasks.model.adapters.FieldAdapter;
import org.dmfs.tasks.model.adapters.FloatFieldAdapter;
import org.dmfs.tasks.model.adapters.FormattedStringFieldAdapter;
import org.dmfs.tasks.model.adapters.IntegerFieldAdapter;
+import org.dmfs.tasks.model.adapters.OptionalLongFieldAdapter;
+import org.dmfs.tasks.model.adapters.RRuleFieldAdapter;
import org.dmfs.tasks.model.adapters.StringFieldAdapter;
import org.dmfs.tasks.model.adapters.TimeFieldAdapter;
import org.dmfs.tasks.model.adapters.TimezoneFieldAdapter;
@@ -37,6 +44,7 @@ import org.dmfs.tasks.model.constraints.AdjustPercentComplete;
import org.dmfs.tasks.model.constraints.After;
import org.dmfs.tasks.model.constraints.BeforeOrShiftTime;
import org.dmfs.tasks.model.constraints.ChecklistConstraint;
+import org.dmfs.tasks.model.constraints.DescriptionConstraint;
import org.dmfs.tasks.model.defaults.DefaultAfter;
import org.dmfs.tasks.model.defaults.DefaultBefore;
@@ -110,6 +118,12 @@ public final class TaskFieldAdapters
public final static ChecklistFieldAdapter CHECKLIST = (ChecklistFieldAdapter) new ChecklistFieldAdapter(Tasks.DESCRIPTION)
.addContraint(new ChecklistConstraint(STATUS, PERCENT_COMPLETE));
+ /**
+ * Adapter for the checklist of a task.
+ */
+ public final static DescriptionFieldAdapter DESCRIPTION_CHECKLIST = (DescriptionFieldAdapter) new DescriptionFieldAdapter(Tasks.DESCRIPTION)
+ .addContraint(new DescriptionConstraint(STATUS, PERCENT_COMPLETE));
+
/**
* Private adapter for the start date of a task. We need this to reference DTSTART from DUE.
*/
@@ -127,10 +141,25 @@ public final class TaskFieldAdapters
public final static FieldAdapter