diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/GenericImporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/GenericImporter.java index 48b7b61cb..332f24cc2 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/GenericImporter.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/GenericImporter.java @@ -41,7 +41,8 @@ public class GenericImporter extends AbstractImporter @NonNull LoopDBImporter loopDBImporter, @NonNull RewireDBImporter rewireDBImporter, @NonNull TickmateDBImporter tickmateDBImporter, - @NonNull HabitBullCSVImporter habitBullCSVImporter) + @NonNull HabitBullCSVImporter habitBullCSVImporter, + @NonNull HabitsCSVImporter habitsCSVImporter) { super(habits); importers = new LinkedList<>(); @@ -49,6 +50,7 @@ public class GenericImporter extends AbstractImporter importers.add(rewireDBImporter); importers.add(tickmateDBImporter); importers.add(habitBullCSVImporter); + importers.add(habitsCSVImporter); } @Override diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVImporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVImporter.java new file mode 100644 index 000000000..644a27f1f --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVImporter.java @@ -0,0 +1,162 @@ +package org.isoron.uhabits.core.io; + +import androidx.annotation.NonNull; + +import com.opencsv.CSVReader; + +import org.isoron.uhabits.core.models.Checkmark; +import org.isoron.uhabits.core.models.Frequency; +import org.isoron.uhabits.core.models.Habit; +import org.isoron.uhabits.core.models.HabitList; +import org.isoron.uhabits.core.models.ModelFactory; +import org.isoron.uhabits.core.models.Timestamp; +import org.isoron.uhabits.core.utils.ColorConstants; +import org.isoron.uhabits.core.utils.DateFormats; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.text.ParseException; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import javax.inject.Inject; + +public class HabitsCSVImporter extends AbstractImporter { + private ModelFactory modelFactory; + + @Inject + public HabitsCSVImporter(@NonNull HabitList habits, + @NonNull ModelFactory modelFactory) { + super(habits); + this.modelFactory = modelFactory; + } + + @Override + public boolean canHandle(@NonNull File file) { + try { + ZipFile zipFile = new ZipFile(file); + ZipEntry entry = zipFile.getEntry("Habits.csv"); + return entry != null; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public void importHabitsFromFile(@NonNull File file) throws IOException { + ZipFile zipFile = new ZipFile(file); + CSVReader habitsCsv = new CSVReader(new InputStreamReader(zipFile.getInputStream(zipFile.getEntry("Habits.csv")))); + try { + HashMap map = new HashMap<>(); + + boolean hasQuestion = false; + for (String line[] : habitsCsv) { + if ("Position".equals(line[0])) { + // older csv files did not have a question column + if ("Question".equals(line[2])) hasQuestion = true; + continue; + } + + int idx = 0; + String position = line[idx++]; + String name = line[idx++]; + String question = hasQuestion ? line[idx++] : null; + String desc = line[idx++]; + int reps = Integer.parseInt(line[idx++]); + int intv = Integer.parseInt(line[idx++]); + String colr = line[idx++]; + + Habit habit = findExisting(name); + if (habit == null) { + habit = modelFactory.buildHabit(); + habit.setName(name); + if (hasQuestion) { + habit.setDescription(desc); + habit.setQuestion(question); + } else { + habit.setDescription(""); + habit.setQuestion(desc); + } + habit.setFrequency(new Frequency(reps, intv)); + habit.setColor(findColor(colr)); + habitList.add(habit); + } + + parseCheckmarks(findCheckmarks(position, zipFile), habit); + } + } finally { + try { + zipFile.close(); + habitsCsv.close(); + } catch (IOException e) { + // ignore + } + } + } + + Timestamp parse(String ts) throws ParseException { + Date date = DateFormats.getCSVDateFormat().parse(ts); + Timestamp timestamp = new Timestamp(date.getTime()); + return timestamp; + } + + void parseCheckmarks(CSVReader checkmarks, Habit habit) { + if (checkmarks == null) return; + try { + for (String line[] : checkmarks) { + try { + Timestamp ts = parse(line[0]); + int value = Integer.parseInt(line[1]); + // only add positive changes ;) + if (value > Checkmark.UNCHECKED) { + habit.getRepetitions().toggle(ts, value); + } + } catch (Exception e) { + // skip + } + } + } finally { + try { + checkmarks.close(); + } catch (IOException e) { + // ignore + } + } + } + + CSVReader findCheckmarks(String position, ZipFile zipFile) throws IOException { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + // look for 00x */Checkmarks.csv + if (!entry.isDirectory() && entry.getName().startsWith(position) && entry.getName().endsWith("/Checkmarks.csv")) { + return new CSVReader(new InputStreamReader(zipFile.getInputStream(entry))); + } + } + return null; + } + + private Habit findExisting(String name) { + Iterator iterator = habitList.iterator(); + while (iterator.hasNext()) { + Habit habit = iterator.next(); + if (habit.getName().equals(name)) return habit; + } + return null; + } + + private int findColor(String hex) { + for (int i = 0; i < ColorConstants.CSV_PALETTE.length; i++) { + if (ColorConstants.CSV_PALETTE[i].equalsIgnoreCase(hex)) { + return i; + } + } + return 0; + } +} diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/HabitsCSVExporterTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/HabitsCSVExporterAndImporterTest.java similarity index 62% rename from android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/HabitsCSVExporterTest.java rename to android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/HabitsCSVExporterAndImporterTest.java index 758a70220..df3bbfa7a 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/HabitsCSVExporterTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/HabitsCSVExporterAndImporterTest.java @@ -19,20 +19,33 @@ package org.isoron.uhabits.core.io; -import org.apache.commons.io.*; -import org.isoron.uhabits.core.*; -import org.isoron.uhabits.core.models.*; -import org.junit.*; - -import java.io.*; -import java.nio.file.*; -import java.util.*; -import java.util.zip.*; - +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.isoron.uhabits.core.BaseUnitTest; +import org.isoron.uhabits.core.models.Habit; +import org.isoron.uhabits.core.models.HabitList; +import org.isoron.uhabits.core.models.memory.MemoryModelFactory; +import org.jetbrains.annotations.NotNull; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; -public class HabitsCSVExporterTest extends BaseUnitTest +public class HabitsCSVExporterAndImporterTest extends BaseUnitTest { private File baseDir; @@ -77,6 +90,39 @@ public class HabitsCSVExporterTest extends BaseUnitTest assertPathExists("Scores.csv"); } + @Test + public void testImportCSV() throws IOException { + List selected = new LinkedList<>(); + for (Habit h : habitList) selected.add(h); + + HabitsCSVExporter exporter = + new HabitsCSVExporter(habitList, selected, baseDir); + File file = new File(exporter.writeArchive()); + + HabitList oldHabits = habitList; + + // reset model before importing + modelFactory = new MemoryModelFactory(); + habitList = spy(modelFactory.buildHabitList()); + + HabitsCSVImporter importer = new HabitsCSVImporter(habitList, modelFactory); + assertTrue(importer.canHandle(file)); + + importer.importHabitsFromFile(file); + + String fixed = stringRepresentation(oldHabits); + String updated = stringRepresentation(habitList); + + // equals methods are too strict for this purpose + assertEquals(fixed, updated); + } + + @NotNull + private String stringRepresentation(HabitList oldHabits) { + assertEquals(2, oldHabits.size()); + return oldHabits.getByPosition(0).toString() + oldHabits.getByPosition(1).toString(); + } + private void unzip(File file) throws IOException { ZipFile zip = new ZipFile(file); diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/ImportTest.java b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/ImportTest.java index 73671d7e7..1a3729094 100644 --- a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/ImportTest.java +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/io/ImportTest.java @@ -135,7 +135,8 @@ public class ImportTest extends BaseUnitTest new LoopDBImporter(habitList, modelFactory, databaseOpener), new RewireDBImporter(habitList, modelFactory, databaseOpener), new TickmateDBImporter(habitList, modelFactory, databaseOpener), - new HabitBullCSVImporter(habitList, modelFactory)); + new HabitBullCSVImporter(habitList, modelFactory), + new HabitsCSVImporter(habitList, modelFactory)); assertTrue(importer.canHandle(file)); importer.importHabitsFromFile(file);