diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a648396f9..80fb2b2dc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,8 +34,8 @@ android { defaultConfig { testApplicationId = "org.tasks.test" applicationId = "org.tasks" - versionCode = 607 - versionName = "6.8.1" + versionCode = 608 + versionName = "6.9" targetSdkVersion(Versions.targetSdk) minSdkVersion(Versions.minSdk) multiDexEnabled = true diff --git a/app/src/androidTest/java/org/tasks/data/CaldavDaoTests.java b/app/src/androidTest/java/org/tasks/data/CaldavDaoTests.java new file mode 100644 index 000000000..725f90457 --- /dev/null +++ b/app/src/androidTest/java/org/tasks/data/CaldavDaoTests.java @@ -0,0 +1,63 @@ +package org.tasks.data; + +import static com.natpryce.makeiteasy.MakeItEasy.with; +import static java.util.Collections.singletonList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.tasks.makers.TagDataMaker.newTagData; +import static org.tasks.makers.TagMaker.TAGDATA; +import static org.tasks.makers.TagMaker.TASK; +import static org.tasks.makers.TagMaker.newTag; +import static org.tasks.makers.TaskMaker.ID; +import static org.tasks.makers.TaskMaker.newTask; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.todoroo.astrid.dao.TaskDao; +import com.todoroo.astrid.data.Task; +import javax.inject.Inject; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class CaldavDaoTests { + @Inject TaskDao taskDao; + @Inject TagDao tagDao; + @Inject TagDataDao tagDataDao; + @Inject CaldavDao caldavDao; + + @Test + public void getCaldavTasksWithTags() { + Task task = newTask(with(ID, 1L)); + taskDao.createNew(task); + TagData one = newTagData(); + TagData two = newTagData(); + tagDataDao.createNew(one); + tagDataDao.createNew(two); + tagDao.insert(newTag(with(TASK, task), with(TAGDATA, one))); + tagDao.insert(newTag(with(TASK, task), with(TAGDATA, two))); + caldavDao.insert(new CaldavTask(task.getId(), "calendar")); + + assertEquals(singletonList(task.getId()), caldavDao.getTasksWithTags()); + } + + @Test + public void ignoreNonCaldavTaskWithTags() { + Task task = newTask(with(ID, 1L)); + taskDao.createNew(task); + TagData tag = newTagData(); + tagDataDao.createNew(tag); + tagDao.insert(newTag(with(TASK, task), with(TAGDATA, tag))); + + assertTrue(caldavDao.getTasksWithTags().isEmpty()); + } + + @Test + public void ignoreCaldavTaskWithoutTags() { + Task task = newTask(with(ID, 1L)); + taskDao.createNew(task); + tagDataDao.createNew(newTagData()); + caldavDao.insert(new CaldavTask(task.getId(), "calendar")); + + assertTrue(caldavDao.getTasksWithTags().isEmpty()); + } +} diff --git a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java index 2b45865fa..c2136b4fb 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.java @@ -142,6 +142,9 @@ public abstract class TaskDao { @RawQuery(observedEntities = {Task.class, GoogleTask.class, CaldavTask.class, Tag.class}) public abstract DataSource.Factory getTaskFactory(SimpleSQLiteQuery query); + @Query("UPDATE tasks SET modified = datetime('now', 'localtime') WHERE _id in (:ids)") + public abstract void touch(List ids); + /** * Saves the given task to the database.getDatabase(). Task must already exist. Returns true on * success. diff --git a/app/src/main/java/com/todoroo/astrid/service/Upgrader.java b/app/src/main/java/com/todoroo/astrid/service/Upgrader.java index 1ab2e408f..eafaaaca0 100644 --- a/app/src/main/java/com/todoroo/astrid/service/Upgrader.java +++ b/app/src/main/java/com/todoroo/astrid/service/Upgrader.java @@ -1,6 +1,7 @@ package com.todoroo.astrid.service; import static com.google.common.base.Strings.isNullOrEmpty; +import static org.tasks.db.DbUtils.batch; import android.os.Environment; import com.google.common.base.Strings; @@ -9,12 +10,16 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimaps; import com.todoroo.astrid.api.GtasksFilter; +import com.todoroo.astrid.dao.TaskDao; import java.io.File; import java.util.List; import javax.inject.Inject; import org.tasks.R; import org.tasks.analytics.Tracker; import org.tasks.analytics.Tracking; +import org.tasks.caldav.CaldavUtils; +import org.tasks.data.CaldavDao; +import org.tasks.data.CaldavTaskContainer; import org.tasks.data.Filter; import org.tasks.data.FilterDao; import org.tasks.data.GoogleTaskAccount; @@ -41,6 +46,7 @@ public class Upgrader { private static final int V6_4 = 546; private static final int V6_7 = 585; private static final int V6_8_1 = 607; + private static final int V6_9 = 608; private final Preferences preferences; private final Tracker tracker; private final TagDataDao tagDataDao; @@ -50,6 +56,8 @@ public class Upgrader { private final GoogleTaskListDao googleTaskListDao; private final UserActivityDao userActivityDao; private final TaskAttachmentDao taskAttachmentDao; + private final CaldavDao caldavDao; + private final TaskDao taskDao; @Inject public Upgrader( @@ -61,7 +69,9 @@ public class Upgrader { DefaultFilterProvider defaultFilterProvider, GoogleTaskListDao googleTaskListDao, UserActivityDao userActivityDao, - TaskAttachmentDao taskAttachmentDao) { + TaskAttachmentDao taskAttachmentDao, + CaldavDao caldavDao, + TaskDao taskDao) { this.preferences = preferences; this.tracker = tracker; this.tagDataDao = tagDataDao; @@ -71,6 +81,8 @@ public class Upgrader { this.googleTaskListDao = googleTaskListDao; this.userActivityDao = userActivityDao; this.taskAttachmentDao = taskAttachmentDao; + this.caldavDao = caldavDao; + this.taskDao = taskDao; } public void upgrade(int from, int to) { @@ -83,6 +95,7 @@ public class Upgrader { run(from, V6_4, this::migrateUris); run(from, V6_7, this::migrateGoogleTaskFilters); run(from, V6_8_1, this::migrateCaldavFilters); + run(from, V6_9, this::applyCaldavCategories); tracker.reportEvent(Tracking.Events.UPGRADE, Integer.toString(from)); } preferences.setCurrentVersion(to); @@ -95,6 +108,18 @@ public class Upgrader { } } + private void applyCaldavCategories() { + List tasksWithTags = caldavDao.getTasksWithTags(); + for (CaldavTaskContainer container : caldavDao.getTasks()) { + at.bitfire.ical4android.Task remoteTask = + CaldavUtils.fromVtodo(container.caldavTask.getVtodo()); + if (remoteTask != null) { + tagDao.insert(container.task, CaldavUtils.getTags(tagDataDao, remoteTask.getCategories())); + } + } + batch(tasksWithTags, taskDao::touch); + } + private void performMarshmallowMigration() { try { // preserve pre-marshmallow default backup location diff --git a/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.java b/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.java index 73ba0c707..3be3a85bc 100644 --- a/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.java +++ b/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.java @@ -6,10 +6,7 @@ package com.todoroo.astrid.tags; -import static com.google.common.collect.Iterables.transform; -import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Lists.transform; -import static com.google.common.collect.Sets.difference; import static com.google.common.collect.Sets.newHashSet; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastJellybeanMR1; @@ -48,7 +45,6 @@ import java.util.List; import java.util.Set; import javax.inject.Inject; import org.tasks.R; -import org.tasks.data.Tag; import org.tasks.data.TagDao; import org.tasks.data.TagData; import org.tasks.data.TagDataDao; @@ -366,22 +362,12 @@ public final class TagsControlSet extends TaskEditControlFragment { } private boolean synchronizeTags(Task task) { - long taskId = task.getId(); for (TagData tagData : selectedTags) { if (Task.NO_UUID.equals(tagData.getRemoteId())) { tagDataDao.createNew(tagData); } } - Set existing = newHashSet(tagDataDao.getTagDataForTask(taskId)); - Set selected = newHashSet(selectedTags); - Set added = difference(selected, existing); - Set removed = difference(existing, selected); - tagDao.deleteTags(taskId, newArrayList(transform(removed, TagData::getRemoteId))); - for (TagData tagData : added) { - Tag newLink = new Tag(task, tagData); - tagDao.insert(newLink); - } - return !removed.isEmpty() || !added.isEmpty(); + return tagDao.applyTags(task, tagDataDao, selectedTags); } @Override diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java index 9d56de79a..196b800d7 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java +++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.java @@ -33,7 +33,6 @@ import com.todoroo.astrid.service.TaskCreator; import com.todoroo.astrid.service.TaskDeleter; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.StringReader; import java.net.ConnectException; import java.net.SocketTimeoutException; import java.net.UnknownHostException; @@ -56,7 +55,6 @@ import org.tasks.data.CaldavAccount; import org.tasks.data.CaldavCalendar; import org.tasks.data.CaldavDao; import org.tasks.data.CaldavTask; -import org.tasks.data.Tag; import org.tasks.data.TagDao; import org.tasks.data.TagData; import org.tasks.data.TagDataDao; @@ -350,58 +348,37 @@ public class CaldavSynchronizer { private void processVTodo( String fileName, CaldavCalendar caldavCalendar, String eTag, String vtodo) { - List tasks = - at.bitfire.ical4android.Task.Companion.fromReader(new StringReader(vtodo)); - - if (tasks.size() == 1) { - at.bitfire.ical4android.Task remote = tasks.get(0); - Task task; - CaldavTask caldavTask = caldavDao.getTask(caldavCalendar.getUuid(), fileName); - if (caldavTask == null) { - task = taskCreator.createWithValues(""); - taskDao.createNew(task); - caldavTask = - new CaldavTask(task.getId(), caldavCalendar.getUuid(), remote.getUid(), fileName); - } else { - task = taskDao.fetch(caldavTask.getTask()); - } - CaldavConverter.apply(task, remote); - applyCategories(task, remote.getCategories()); - task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true); - taskDao.save(task); - caldavTask.setVtodo(vtodo); - caldavTask.setEtag(eTag); - caldavTask.setLastSync(DateUtilities.now() + 1000L); - if (caldavTask.getId() == Task.NO_ID) { - caldavTask.setId(caldavDao.insert(caldavTask)); - Timber.d("NEW %s", caldavTask); - } else { - caldavDao.update(caldavTask); - Timber.d("UPDATE %s", caldavTask); - } - } else { - Timber.e("Received VCALENDAR with %s VTODOs; ignoring %s", tasks.size(), fileName); + + at.bitfire.ical4android.Task remote = CaldavUtils.fromVtodo(vtodo); + if (remote == null) { + Timber.e("Invalid VCALENDAR: %s", fileName); + return; } - } - private void applyCategories(Task task, List categories) { - long taskId = task.getId(); - List selectedTags = tagDataDao.getTags(categories); - Set toCreate = - difference(newHashSet(categories), newHashSet(transform(selectedTags, TagData::getName))); - for (String name : toCreate) { - TagData tag = new TagData(name); - tagDataDao.createNew(tag); - selectedTags.add(tag); + Task task; + CaldavTask caldavTask = caldavDao.getTask(caldavCalendar.getUuid(), fileName); + if (caldavTask == null) { + task = taskCreator.createWithValues(""); + taskDao.createNew(task); + caldavTask = + new CaldavTask(task.getId(), caldavCalendar.getUuid(), remote.getUid(), fileName); + } else { + task = taskDao.fetch(caldavTask.getTask()); } - Set existing = newHashSet(tagDataDao.getTagDataForTask(taskId)); - Set selected = newHashSet(selectedTags); - Set added = difference(selected, existing); - Set removed = difference(existing, selected); - tagDao.deleteTags(taskId, newArrayList(Iterables.transform(removed, TagData::getRemoteId))); - for (TagData tagData : added) { - Tag newLink = new Tag(task, tagData); - tagDao.insert(newLink); + CaldavConverter.apply(task, remote); + tagDao.applyTags(task, tagDataDao, CaldavUtils.getTags(tagDataDao, remote.getCategories())); + task.putTransitory(SyncFlags.GTASKS_SUPPRESS_SYNC, true); + taskDao.save(task); + caldavTask.setVtodo(vtodo); + caldavTask.setEtag(eTag); + caldavTask.setLastSync(DateUtilities.now() + 1000L); + if (caldavTask.getId() == Task.NO_ID) { + caldavTask.setId(caldavDao.insert(caldavTask)); + Timber.d("NEW %s", caldavTask); + } else { + caldavDao.update(caldavTask); + Timber.d("UPDATE %s", caldavTask); } } + } diff --git a/app/src/main/java/org/tasks/caldav/CaldavUtils.java b/app/src/main/java/org/tasks/caldav/CaldavUtils.java new file mode 100644 index 000000000..920401e43 --- /dev/null +++ b/app/src/main/java/org/tasks/caldav/CaldavUtils.java @@ -0,0 +1,36 @@ +package org.tasks.caldav; + +import static com.google.common.collect.Lists.transform; +import static com.google.common.collect.Sets.difference; +import static com.google.common.collect.Sets.newHashSet; + +import androidx.annotation.Nullable; +import java.io.StringReader; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import org.tasks.data.TagData; +import org.tasks.data.TagDataDao; + +public class CaldavUtils { + public static @Nullable at.bitfire.ical4android.Task fromVtodo(String vtodo) { + List tasks = + at.bitfire.ical4android.Task.Companion.fromReader(new StringReader(vtodo)); + return tasks.size() == 1 ? tasks.get(0) : null; + } + + public static List getTags(TagDataDao tagDataDao, List categories) { + if (categories.isEmpty()) { + return Collections.emptyList(); + } + List selectedTags = tagDataDao.getTags(categories); + Set toCreate = + difference(newHashSet(categories), newHashSet(transform(selectedTags, TagData::getName))); + for (String name : toCreate) { + TagData tag = new TagData(name); + tagDataDao.createNew(tag); + selectedTags.add(tag); + } + return selectedTags; + } +} diff --git a/app/src/main/java/org/tasks/data/CaldavDao.java b/app/src/main/java/org/tasks/data/CaldavDao.java index d3c4b2634..e2cd3e5e9 100644 --- a/app/src/main/java/org/tasks/data/CaldavDao.java +++ b/app/src/main/java/org/tasks/data/CaldavDao.java @@ -64,6 +64,12 @@ public interface CaldavDao { @Query("SELECT * FROM caldav_tasks WHERE cd_task = :taskId") List getTasks(long taskId); + @Query( + "SELECT task.*, caldav_task.* FROM tasks AS task " + + "INNER JOIN caldav_tasks AS caldav_task ON _id = cd_task " + + "WHERE cd_deleted = 0") + List getTasks(); + @Query("SELECT * FROM caldav_lists ORDER BY cdl_name COLLATE NOCASE") List getCalendars(); @@ -97,4 +103,11 @@ public interface CaldavDao { + " GROUP BY caldav_lists.cdl_uuid" + " ORDER BY caldav_accounts.cda_name COLLATE NOCASE, caldav_lists.cdl_name COLLATE NOCASE") List getCaldavFilters(long now); + + @Query( + "SELECT tasks._id FROM tasks " + + "INNER JOIN tags ON tags.task = tasks._id " + + "INNER JOIN caldav_tasks ON cd_task = tasks._id " + + "GROUP BY tasks._id") + List getTasksWithTags(); } diff --git a/app/src/main/java/org/tasks/data/CaldavTaskContainer.java b/app/src/main/java/org/tasks/data/CaldavTaskContainer.java new file mode 100644 index 000000000..eb6d8be79 --- /dev/null +++ b/app/src/main/java/org/tasks/data/CaldavTaskContainer.java @@ -0,0 +1,9 @@ +package org.tasks.data; + +import androidx.room.Embedded; +import com.todoroo.astrid.data.Task; + +public class CaldavTaskContainer { + @Embedded public Task task; + @Embedded public CaldavTask caldavTask; +} diff --git a/app/src/main/java/org/tasks/data/Tag.java b/app/src/main/java/org/tasks/data/Tag.java index c7e6ba807..4bdb064ae 100644 --- a/app/src/main/java/org/tasks/data/Tag.java +++ b/app/src/main/java/org/tasks/data/Tag.java @@ -45,8 +45,13 @@ public class Tag { @Ignore public Tag(Task task, String name, String tagUid) { - this.task = task.getId(); - this.taskUid = task.getUuid(); + this(task.getId(), task.getUuid(), name, tagUid); + } + + @Ignore + public Tag(long taskId, String taskUid, String name, String tagUid) { + this.task = taskId; + this.taskUid = taskUid; this.name = name; this.tagUid = tagUid; } diff --git a/app/src/main/java/org/tasks/data/TagDao.java b/app/src/main/java/org/tasks/data/TagDao.java index ad0666921..44769756c 100644 --- a/app/src/main/java/org/tasks/data/TagDao.java +++ b/app/src/main/java/org/tasks/data/TagDao.java @@ -1,40 +1,67 @@ package org.tasks.data; +import static com.google.common.collect.Iterables.transform; +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Sets.difference; +import static com.google.common.collect.Sets.newHashSet; + import androidx.room.Dao; import androidx.room.Insert; import androidx.room.Query; +import androidx.room.Transaction; +import com.todoroo.astrid.data.Task; +import java.util.Collection; import java.util.List; +import java.util.Set; @Dao -public interface TagDao { +public abstract class TagDao { @Query("UPDATE tags SET name = :name WHERE tag_uid = :tagUid") - void rename(String tagUid, String name); + public abstract void rename(String tagUid, String name); @Query("DELETE FROM tags WHERE tag_uid = :tagUid") - void deleteTag(String tagUid); + public abstract void deleteTag(String tagUid); @Insert - void insert(Tag tag); + public abstract void insert(Tag tag); @Insert - void insert(Iterable tags); + public abstract void insert(Iterable tags); @Query("DELETE FROM tags WHERE task = :taskId AND tag_uid in (:tagUids)") - void deleteTags(long taskId, List tagUids); + public abstract void deleteTags(long taskId, List tagUids); @Query("SELECT name FROM tags WHERE task = :taskId ORDER BY UPPER(name) ASC") - List getTagNames(long taskId); + public abstract List getTagNames(long taskId); @Query("SELECT * FROM tags WHERE tag_uid = :tagUid") - List getByTagUid(String tagUid); + public abstract List getByTagUid(String tagUid); @Query("SELECT * FROM tags WHERE task = :taskId") - List getTagsForTask(long taskId); + public abstract List getTagsForTask(long taskId); @Query("SELECT * FROM tags WHERE task = :taskId AND tag_uid = :tagUid") - Tag getTagByTaskAndTagUid(long taskId, String tagUid); + public abstract Tag getTagByTaskAndTagUid(long taskId, String tagUid); @Query("DELETE FROM tags WHERE _id = :id") - void deleteById(long id); + public abstract void deleteById(long id); + + @Transaction + public boolean applyTags(Task task, TagDataDao tagDataDao,List current) { + long taskId = task.getId(); + Set existing = newHashSet(tagDataDao.getTagDataForTask(taskId)); + Set selected = newHashSet(current); + Set added = difference(selected, existing); + Set removed = difference(existing, selected); + deleteTags(taskId, newArrayList(transform(removed, TagData::getRemoteId))); + insert(task, added); + return !(removed.isEmpty() && added.isEmpty()); + } + + public void insert(Task task, Collection tags) { + if (!tags.isEmpty()) { + insert(transform(tags, td -> new Tag(task, td))); + } + } } diff --git a/app/src/main/java/org/tasks/db/DbUtils.java b/app/src/main/java/org/tasks/db/DbUtils.java new file mode 100644 index 000000000..825cfa37b --- /dev/null +++ b/app/src/main/java/org/tasks/db/DbUtils.java @@ -0,0 +1,28 @@ +package org.tasks.db; + +import static com.google.common.collect.Lists.partition; + +import java.util.List; +import org.tasks.Callback; + +public class DbUtils { + + private static final int MAX_SQLITE_ARGS = 999; + + public static void batch(List items, Callback> callback) { + batch(items, MAX_SQLITE_ARGS, callback); + } + + public static void batch(List items, int size, Callback> callback) { + if (items.isEmpty()) { + return; + } + if (items.size() <= size) { + callback.call(items); + } else { + for (List sublist : partition(items, size)) { + callback.call(sublist); + } + } + } +}