/* * Copyright (c) 2015 Jonas Kalderstam. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.nononsenseapps.notepad.data.local.orgmode; import android.content.Context; import android.util.Pair; import com.nononsenseapps.notepad.data.model.orgmode.RemoteTaskListFile; import com.nononsenseapps.notepad.data.model.orgmode.RemoteTaskNode; import com.nononsenseapps.notepad.data.model.sql.RemoteTask; import com.nononsenseapps.notepad.data.model.sql.RemoteTaskList; import com.nononsenseapps.notepad.data.model.sql.Task; import com.nononsenseapps.notepad.data.model.sql.TaskList; import org.cowboyprogrammer.org.OrgFile; import org.cowboyprogrammer.org.OrgNode; import java.io.IOException; import java.text.ParseException; import java.util.Calendar; import java.util.List; public abstract class Synchronizer extends DBSyncBase implements SynchronizerInterface { public static final int SAVENONE = 0x0; public static final int SAVEDB = 0x01; public static final int SAVEORG = 0x10; public static final String TAG = "OrgSynchronizer"; public Synchronizer(Context context) { super(context); } /** * Performs a full 2-way sync between the DB and the remote source. * * @throws IOException * @throws ParseException */ public void fullSync() throws IOException, ParseException { // For all pairs of files and db entries final List<Pair<OrgFile, Pair<RemoteTaskList, TaskList>>> pairs = getFilesAndDBEntries(); for (Pair<OrgFile, Pair<RemoteTaskList, TaskList>> pair : pairs) { OrgFile file = pair.first; RemoteTaskList dbEntry = pair.second.first; TaskList list = pair.second.second; if (dbEntry == null) { if (file == null) { // NEW CREATE FILE // Create file file = getNewFile(list.title); OrgConverter.toFileFromList(list, file); // Add tasks to File syncTasks(context, list, file); // Save file putRemoteFile(file); // If name was not available, rename list as well if (!file.getFilename().equals(OrgConverter .getTitleAsFilename(list))) { list.title = file.getFilename().substring(0, file.getFilename().length() - 4); list.save(context); } // Create DbEntry dbEntry = new RemoteTaskList(); dbEntry.dbid = list._id; dbEntry.account = getAccountName(); dbEntry.service = getServiceName(); OrgConverter.toRemoteFromFile(dbEntry, file); dbEntry.save(context); } else { // NEW CREATE DB LIST // Create TaskList list = new TaskList(); OrgConverter.toListFromFile(list, file); list.save(context, file.lastModified()); // Create DbEntry dbEntry = new RemoteTaskList(); dbEntry.dbid = list._id; dbEntry.account = getAccountName(); dbEntry.service = getServiceName(); OrgConverter.toRemoteFromFile(dbEntry, file); dbEntry.save(context); // Now do the tasks if (syncTasks(context, list, file)) { // Something changed in the file. putRemoteFile(file); } } } else { if (list == null) { // DELETE FILE DB deleteRemoteFile(file); deleteLocal(list, dbEntry); } else { if (file == null) { // DELETE DB LIST // List and entry deleteLocal(list, dbEntry); } else { // UPDATE EXISTING LIST, IF CHANGED boolean shouldSaveFile = false; if (wasRenamed(list, dbEntry, file)) { final String oldName = file.getFilename(); renameFile(list, dbEntry, file); renameRemoteFile(oldName, file); } // Merge information in database and file final int shouldSave = merge(list, dbEntry, file); if (0 < (shouldSave & SAVEORG)) { // UPDATE FILE DB shouldSaveFile = true; } if (0 < (shouldSave & SAVEDB)) { // UPDATE LIST DB list.save(context); } if (shouldSave != SAVENONE) { OrgConverter.toRemoteFromFile(dbEntry, file); dbEntry.updated = Calendar.getInstance() .getTimeInMillis(); dbEntry.save(context); } // In both cases, sync tasks if (syncTasks(context, list, file) || shouldSaveFile) { // Something changed in the file. putRemoteFile(file); } } } } } } /** * Merge the list and file. Fields considered are the listtype and * listsorting which are stored as comments in the file. * * @param list * @param dbEntry * @param file * @return an integer denoting which should be saved. 0 for none, 0x01 for * task, 0x10 for node. 0x11 for both. */ private int merge(final TaskList list, final RemoteTaskList dbEntry, final OrgFile file) { int shouldSave = SAVENONE; shouldSave |= mergeSorting(list, dbEntry, file); shouldSave |= mergeListType(list, dbEntry, file); return shouldSave; } private int mergeSorting(final TaskList list, final RemoteTaskList dbEntry, final OrgFile file) { final int shouldSave; final String filesorting = OrgConverter.getListSortingFromMeta(file); if (list.sorting == null && RemoteTaskListFile.getSorting(dbEntry) != null || list.sorting != null && !list.sorting.equals(RemoteTaskListFile.getSorting(dbEntry))) { shouldSave = SAVEORG; OrgConverter.setSortingOnFile(list, file); } else if (filesorting == null && RemoteTaskListFile.getSorting(dbEntry) != null || filesorting != null && !filesorting.equals(RemoteTaskListFile.getSorting(dbEntry))) { shouldSave = SAVEORG; list.sorting = filesorting; } else { shouldSave = SAVENONE; } return shouldSave; } private int mergeListType(final TaskList list, final RemoteTaskList dbEntry, final OrgFile file) { final int shouldSave; final String filelisttype = OrgConverter.getListTypeFromMeta(file); if (list.listtype == null && RemoteTaskListFile.getListType(dbEntry) != null || list.listtype != null && !list.listtype.equals(RemoteTaskListFile .getListType(dbEntry))) { shouldSave = SAVEORG; OrgConverter.setListTypeOnFile(list, file); } else if (filelisttype == null && RemoteTaskListFile.getListType(dbEntry) != null || filelisttype != null && !filelisttype .equals(RemoteTaskListFile.getListType(dbEntry))) { shouldSave = SAVEORG; list.listtype = filelisttype; } else { shouldSave = SAVENONE; } return shouldSave; } private boolean syncTasks(final Context context, final TaskList list, final OrgFile file) { final List<Pair<OrgNode, Pair<RemoteTask, Task>>> pairs = getNodesAndDBEntries( file, list); boolean shouldUpdateFile = false; OrgNode prevNode = null; for (Pair<OrgNode, Pair<RemoteTask, Task>> pair : pairs) { OrgNode node = pair.first; RemoteTask dbEntry = pair.second.first; Task task = pair.second.second; if (dbEntry == null) { if (node == null) { // CREATE NODE DB //Log.d(TAG, "CREATE NODE DB"); node = new OrgNode(file.getParser()); node.setLevel(1); node.setParent(file); int idx = -1; if (prevNode != null) { idx = file.getSubNodes().indexOf(prevNode); } file.getSubNodes().add(idx + 1, node); OrgConverter.toNodeFromTask(task, node); dbEntry = new RemoteTask(); dbEntry.dbid = task._id; dbEntry.listdbid = list._id; dbEntry.account = getAccountName(); dbEntry.service = getServiceName(); OrgConverter.toRemoteFromNode(dbEntry, node); dbEntry.save(context); shouldUpdateFile = true; } else { // CREATE TASK DB //Log.d(TAG, "CREATE TASK DB"); task = new Task(); task.dblist = list._id; OrgConverter.toTaskFromNode(task, node); task.save(context); dbEntry = new RemoteTask(); dbEntry.dbid = task._id; dbEntry.listdbid = list._id; dbEntry.account = getAccountName(); dbEntry.service = getServiceName(); shouldUpdateFile = OrgConverter.toRemoteFromNode(dbEntry, node); dbEntry.save(context); replaceNotifications(task, node); } } else { if (task == null) { // DELETE NODE DB //Log.d(TAG, "DELETE NODE DB"); deleteLocal(task, dbEntry); if (node != null) { deleteNode(node); shouldUpdateFile = true; } } else { if (node == null) { // DELETE DB TASK //Log.d(TAG, "DELETE TASK DB"); deleteLocal(task, dbEntry); } else { // TODO need to check notifications also //Log.d(TAG, "MERGE TASKS"); final int shouldSave = merge(task, dbEntry, node); if (0 < (shouldSave & SAVEORG)) { // UPDATE NODE DB OrgConverter.toNodeFromRemote(node, dbEntry); shouldUpdateFile = true; } if (0 < (shouldSave & SAVEDB)) { task.save(context); } if (0 < shouldSave) { // Remember this version for later OrgConverter.toRemoteFromNode(dbEntry, node); dbEntry.save(context); } } } } // Remember the previous next time for positioning if (node != null) { prevNode = node; } } return shouldUpdateFile; } /** * * @param node * to delete from the tree structure. Preserves sub nodes. */ private void deleteNode(final OrgNode node) { final OrgNode parent = node.getParent(); // If no parent, nothing to do if (parent == null) return; // If sub nodes, transfer to root if (!node.getSubNodes().isEmpty()) { final int i = parent.getSubNodes().indexOf(node); parent.getSubNodes().addAll(i, node.getSubNodes()); } // Remove the node parent.getSubNodes().remove(node); } /** * Merges the task and node. The fields considered are title, body, * completed and deadline. * * @param task * @param remote * @param node * @return an integer denoting which should be saved. 0 for none, 0x01 for * task, 0x10 for node. 0x11 for both. */ protected int merge(final Task task, final RemoteTask remote, final OrgNode node) { if (task == null || remote == null || node == null) { throw new NullPointerException( "A merge operation can't have null parties!"); } // 0x01 if task should be saved // 0x10 if node should be saved // 0x11 if both should be saved // 0x00 if nothing needs to be saved int shouldSave = SAVENONE; shouldSave |= mergeTitles(task, remote, node); shouldSave |= mergeBodies(task, remote, node); shouldSave |= mergeTodo(task, remote, node); shouldSave |= mergeTimestamps(task, remote, node); return shouldSave; } private int mergeTodo(final Task task, final RemoteTask remote, final OrgNode node) { final int shouldSave; final String taskTodo; if (task.completed != null) taskTodo = "DONE"; else taskTodo = "TODO"; if (!taskTodo.equals(RemoteTaskNode.getTodo(remote))) { shouldSave = SAVEORG; node.setTodo(taskTodo); } else if (RemoteTaskNode.getTodo(remote) != null && !RemoteTaskNode.getTodo(remote).equals(node.getTodo())) { shouldSave = SAVEDB; if ("DONE".equals(node.getTodo())) { task.completed = Calendar.getInstance().getTimeInMillis(); } else { task.completed = null; } } else { shouldSave = SAVENONE; } return shouldSave; } private int mergeTimestamps(final Task task, final RemoteTask remote, final OrgNode node) { final int shouldSave; Long basedue = null; if (RemoteTaskNode.getDueTime(remote) != null && !RemoteTaskNode.getDueTime(remote).isEmpty()) { basedue = Long.parseLong(RemoteTaskNode.getDueTime(remote)); } final Long nodedue = OrgConverter.getDeadline(node); if (task.due != basedue) { shouldSave = SAVEORG; OrgConverter.setDeadline(node, task.due); } else if (nodedue != basedue) { shouldSave = SAVEDB; task.due = nodedue; } else { shouldSave = SAVENONE; } return shouldSave; } private int mergeBodies(final Task task, final RemoteTask remote, final OrgNode node) { final int shouldSave; boolean taskChanged = !task.note.equals(RemoteTaskNode.getBody(remote)); // Check with trailing newline also if (taskChanged) { taskChanged = !(task.note + "\n").equals(RemoteTaskNode.getBody(remote)); } if (taskChanged) { shouldSave = SAVEORG; node.setBody(task.note); } else if (!node.getBody().equals(RemoteTaskNode.getBody(remote))) { shouldSave = SAVEDB; task.note = node.getBody(); /* * It's not possible to differentiate if the user added a trailing * newline or the sync logic did. I will assume that the sync logic did. */ if (task.note != null && !task.note.isEmpty() && task.note.endsWith("\n")) { task.note = task.note.substring(0, task.note.length() - 1); } } else { shouldSave = SAVENONE; } return shouldSave; } private int mergeTitles(final Task task, final RemoteTask remote, final OrgNode node) { final int shouldSave; if (!task.title.equals(RemoteTaskNode.getTitle(remote))) { shouldSave = SAVEORG; node.setTitle(task.title); } else if (!node.getTitle().equals(RemoteTaskNode.getTitle(remote))) { shouldSave = SAVEDB; task.title = node.getTitle(); } else { shouldSave = SAVENONE; } return shouldSave; } }