package com.todoroo.astrid.service; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Lists.newArrayList; import static org.tasks.caldav.CaldavUtils.getParent; import static org.tasks.db.DbUtils.batch; import android.os.Environment; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; 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.CaldavTask; import org.tasks.data.CaldavTaskContainer; import org.tasks.data.Filter; import org.tasks.data.FilterDao; import org.tasks.data.GoogleTaskAccount; import org.tasks.data.GoogleTaskList; import org.tasks.data.GoogleTaskListDao; import org.tasks.data.Tag; import org.tasks.data.TagDao; import org.tasks.data.TagData; import org.tasks.data.TagDataDao; import org.tasks.data.TaskAttachment; import org.tasks.data.TaskAttachmentDao; import org.tasks.data.UserActivity; import org.tasks.data.UserActivityDao; import org.tasks.preferences.DefaultFilterProvider; import org.tasks.preferences.Preferences; public class Upgrader { private static final int V4_8_0 = 380; private static final int V4_9_5 = 434; private static final int V5_3_0 = 491; private static final int V6_0_beta_1 = 522; private static final int V6_0_beta_2 = 523; 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 static final int V7_0 = 617; private final Preferences preferences; private final Tracker tracker; private final TagDataDao tagDataDao; private final TagDao tagDao; private final FilterDao filterDao; private final DefaultFilterProvider defaultFilterProvider; private final GoogleTaskListDao googleTaskListDao; private final UserActivityDao userActivityDao; private final TaskAttachmentDao taskAttachmentDao; private final CaldavDao caldavDao; private final TaskDao taskDao; @Inject public Upgrader( Preferences preferences, Tracker tracker, TagDataDao tagDataDao, TagDao tagDao, FilterDao filterDao, DefaultFilterProvider defaultFilterProvider, GoogleTaskListDao googleTaskListDao, UserActivityDao userActivityDao, TaskAttachmentDao taskAttachmentDao, CaldavDao caldavDao, TaskDao taskDao) { this.preferences = preferences; this.tracker = tracker; this.tagDataDao = tagDataDao; this.tagDao = tagDao; this.filterDao = filterDao; this.defaultFilterProvider = defaultFilterProvider; this.googleTaskListDao = googleTaskListDao; this.userActivityDao = userActivityDao; this.taskAttachmentDao = taskAttachmentDao; this.caldavDao = caldavDao; this.taskDao = taskDao; } public void upgrade(int from, int to) { if (from > 0) { run(from, V4_8_0, this::performMarshmallowMigration); run(from, V4_9_5, this::removeDuplicateTags); run(from, V5_3_0, this::migrateFilters); run(from, V6_0_beta_1, this::migrateDefaultSyncList); run(from, V6_0_beta_2, this::migrateGoogleTaskAccount); 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); run(from, V7_0, this::applyCaldavSubtasks); tracker.reportEvent(Tracking.Events.UPGRADE, Integer.toString(from)); } preferences.setCurrentVersion(to); } private void run(int from, int version, Runnable runnable) { if (from < version) { runnable.run(); preferences.setCurrentVersion(version); } } private void applyCaldavSubtasks() { List updated = newArrayList(); for (CaldavTask task : transform(caldavDao.getTasks(), CaldavTaskContainer::getCaldavTask)) { at.bitfire.ical4android.Task remoteTask = CaldavUtils.fromVtodo(task.getVtodo()); if (remoteTask == null) { continue; } task.setRemoteParent(getParent(remoteTask)); if (!Strings.isNullOrEmpty(task.getRemoteParent())) { updated.add(task); } } caldavDao.update(updated); caldavDao.updateParents(); } 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 if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { if (!preferences.isStringValueSet(R.string.p_backup_dir)) { String directory = String.format("%s/astrid", Environment.getExternalStorageDirectory()); File file = new File(directory); if (file.exists() && file.isDirectory()) { preferences.setString(R.string.p_backup_dir, directory); } } } } catch (Exception e) { tracker.reportException(e); } } private void removeDuplicateTags() { ListMultimap tagsByUuid = Multimaps.index(tagDataDao.tagDataOrderedByName(), TagData::getRemoteId); for (String uuid : tagsByUuid.keySet()) { removeDuplicateTagData(tagsByUuid.get(uuid)); removeDuplicateTagMetadata(uuid); } } private void migrateGoogleTaskFilters() { for (Filter filter : filterDao.getAll()) { filter.setSql(migrateGoogleTaskFilters(filter.getSql())); filter.setCriterion(migrateGoogleTaskFilters(filter.getCriterion())); filterDao.update(filter); } } private void migrateCaldavFilters() { for (Filter filter : filterDao.getAll()) { filter.setSql(migrateCaldavFilters(filter.getSql())); filter.setCriterion(migrateCaldavFilters(filter.getCriterion())); filterDao.update(filter); } } private void migrateFilters() { for (Filter filter : filterDao.getFilters()) { filter.setSql(migrateMetadata(filter.getSql())); filter.setCriterion(migrateMetadata(filter.getCriterion())); filterDao.update(filter); } } private void migrateDefaultSyncList() { String account = preferences.getStringValue("gtasks_user"); if (isNullOrEmpty(account)) { return; } String defaultGoogleTaskList = preferences.getStringValue("gtasks_defaultlist"); if (isNullOrEmpty(defaultGoogleTaskList)) { // TODO: look up default list } else { GoogleTaskList googleTaskList = googleTaskListDao.getByRemoteId(defaultGoogleTaskList); if (googleTaskList != null) { defaultFilterProvider.setDefaultRemoteList(new GtasksFilter(googleTaskList)); } } } private void migrateGoogleTaskAccount() { String account = preferences.getStringValue("gtasks_user"); if (!isNullOrEmpty(account)) { GoogleTaskAccount googleTaskAccount = new GoogleTaskAccount(); googleTaskAccount.setAccount(account); googleTaskListDao.insert(googleTaskAccount); for (GoogleTaskList list : googleTaskListDao.getAllLists()) { list.setAccount(account); googleTaskListDao.insertOrReplace(list); } } } private void migrateUris() { migrateUriPreference(R.string.p_backup_dir); migrateUriPreference(R.string.p_attachment_dir); for (UserActivity userActivity : userActivityDao.getComments()) { userActivity.convertPictureUri(); userActivityDao.update(userActivity); } for (TaskAttachment attachment : taskAttachmentDao.getAttachments()) { attachment.convertPathUri(); taskAttachmentDao.update(attachment); } } private void migrateUriPreference(int pref) { String path = preferences.getStringValue(pref); if (Strings.isNullOrEmpty(path)) { return; } File file = new File(path); try { if (file.canWrite()) { preferences.setUri(pref, file.toURI()); } else { preferences.remove(pref); } } catch (SecurityException ignored) { preferences.remove(pref); } } private String migrateGoogleTaskFilters(String input) { return input .replace("SELECT task FROM google_tasks", "SELECT gt_task as task FROM google_tasks") .replace("(list_id", "(gt_list_id") .replace("google_tasks.list_id", "google_tasks.gt_list_id") .replace("google_tasks.task", "google_tasks.gt_task"); } private String migrateCaldavFilters(String input) { return input .replace("SELECT task FROM caldav_tasks", "SELECT cd_task as task FROM caldav_tasks") .replace("(calendar", "(cd_calendar"); } private String migrateMetadata(String input) { return input .replaceAll( "SELECT metadata\\.task AS task FROM metadata INNER JOIN tasks ON \\(\\(metadata\\.task=tasks\\._id\\)\\) WHERE \\(\\(\\(tasks\\.completed=0\\) AND \\(tasks\\.deleted=0\\) AND \\(tasks\\.hideUntil<\\(strftime\\(\\'%s\\',\\'now\\'\\)\\*1000\\)\\)\\) AND \\(metadata\\.key=\\'tags-tag\\'\\) AND \\(metadata\\.value", "SELECT tags.task AS task FROM tags INNER JOIN tasks ON ((tags.task=tasks._id)) WHERE (((tasks.completed=0) AND (tasks.deleted=0) AND (tasks.hideUntil<(strftime('%s','now')*1000))) AND (tags.name") .replaceAll( "SELECT metadata\\.task AS task FROM metadata INNER JOIN tasks ON \\(\\(metadata\\.task=tasks\\._id\\)\\) WHERE \\(\\(\\(tasks\\.completed=0\\) AND \\(tasks\\.deleted=0\\) AND \\(tasks\\.hideUntil<\\(strftime\\(\\'%s\\',\\'now\\'\\)\\*1000\\)\\)\\) AND \\(metadata\\.key=\\'gtasks\\'\\) AND \\(metadata\\.value2", "SELECT google_tasks.task AS task FROM google_tasks INNER JOIN tasks ON ((google_tasks.task=tasks._id)) WHERE (((tasks.completed=0) AND (tasks.deleted=0) AND (tasks.hideUntil<(strftime('%s','now')*1000))) AND (google_tasks.list_id") .replaceAll("AND \\(metadata\\.deleted=0\\)", ""); } private void removeDuplicateTagData(List tagData) { if (tagData.size() > 1) { tagDataDao.delete(tagData.subList(1, tagData.size())); } } private void removeDuplicateTagMetadata(String uuid) { List metadatas = tagDao.getByTagUid(uuid); ImmutableListMultimap metadataByTask = Multimaps.index(metadatas, Tag::getTask); for (Long key : metadataByTask.keySet()) { ImmutableList tags = metadataByTask.get(key); if (tags.size() > 1) { tagDao.delete(tags.subList(1, tags.size())); } } } }