From 79b134164a9ef61b39d89724423b239ff42c56f6 Mon Sep 17 00:00:00 2001 From: "Alinson S. Xavier" Date: Wed, 30 Dec 2020 16:36:27 -0600 Subject: [PATCH] Convert remaining core.io classes to Kotlin --- .../uhabits/core/database/DatabaseOpener.kt | 2 +- .../uhabits/core/io/AbstractImporter.java | 58 ---- .../uhabits/core/io/AbstractImporter.kt | 26 ++ .../isoron/uhabits/core/io/GenericImporter.kt | 4 +- .../uhabits/core/io/HabitBullCSVImporter.kt | 4 +- .../uhabits/core/io/HabitsCSVExporter.java | 293 ------------------ .../uhabits/core/io/HabitsCSVExporter.kt | 231 ++++++++++++++ .../isoron/uhabits/core/io/LoopDBImporter.kt | 5 +- .../uhabits/core/io/RewireDBImporter.java | 216 ------------- .../uhabits/core/io/RewireDBImporter.kt | 171 ++++++++++ .../uhabits/core/io/TickmateDBImporter.java | 145 --------- .../uhabits/core/io/TickmateDBImporter.kt | 111 +++++++ .../uhabits/core/tasks/ExportCSVTask.java | 2 +- .../uhabits/core/utils/FileExtensions.kt | 31 ++ .../isoron/uhabits/core/io/ImportTest.java | 4 +- 15 files changed, 580 insertions(+), 723 deletions(-) delete mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/AbstractImporter.java create mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/AbstractImporter.kt delete mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java create mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt delete mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/RewireDBImporter.java create mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/RewireDBImporter.kt delete mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/TickmateDBImporter.java create mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/TickmateDBImporter.kt create mode 100644 android/uhabits-core/src/main/java/org/isoron/uhabits/core/utils/FileExtensions.kt diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/DatabaseOpener.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/DatabaseOpener.kt index efecfec02..b6237a958 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/DatabaseOpener.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/database/DatabaseOpener.kt @@ -21,5 +21,5 @@ package org.isoron.uhabits.core.database import java.io.File interface DatabaseOpener { - fun open(file: File): Database? + fun open(file: File): Database } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/AbstractImporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/AbstractImporter.java deleted file mode 100644 index e9f5416d0..000000000 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/AbstractImporter.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (C) 2017 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker 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. - * - * Loop Habit Tracker 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 . - */ - -package org.isoron.uhabits.core.io; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.models.*; - -import java.io.*; -import java.util.*; - -/** - * AbstractImporter is the base class for all classes that import data from - * files into the app. - */ -public abstract class AbstractImporter -{ - protected final HabitList habitList; - - public AbstractImporter(HabitList habitList) - { - this.habitList = habitList; - } - - public abstract boolean canHandle(@NonNull File file) throws IOException; - - public abstract void importHabitsFromFile(@NonNull File file) throws IOException; - - public static boolean isSQLite3File(@NonNull File file) throws IOException - { - FileInputStream fis = new FileInputStream(file); - - byte[] sqliteHeader = "SQLite format 3".getBytes(); - byte[] buffer = new byte[sqliteHeader.length]; - - int count = fis.read(buffer); - if(count < sqliteHeader.length) return false; - - return Arrays.equals(buffer, sqliteHeader); - } -} \ No newline at end of file diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/AbstractImporter.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/AbstractImporter.kt new file mode 100644 index 000000000..15910f5e8 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/AbstractImporter.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2017 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker 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. + * + * Loop Habit Tracker 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 . + */ +package org.isoron.uhabits.core.io + +import java.io.File + +abstract class AbstractImporter { + abstract fun canHandle(file: File): Boolean + abstract fun importHabitsFromFile(file: File) +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/GenericImporter.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/GenericImporter.kt index 47c97f09c..95a8c8530 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/GenericImporter.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/GenericImporter.kt @@ -18,7 +18,6 @@ */ package org.isoron.uhabits.core.io -import org.isoron.uhabits.core.models.HabitList import java.io.File import javax.inject.Inject @@ -28,12 +27,11 @@ import javax.inject.Inject */ class GenericImporter @Inject constructor( - habits: HabitList, loopDBImporter: LoopDBImporter, rewireDBImporter: RewireDBImporter, tickmateDBImporter: TickmateDBImporter, habitBullCSVImporter: HabitBullCSVImporter, -) : AbstractImporter(habits) { +) : AbstractImporter() { var importers: List = listOf( loopDBImporter, diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitBullCSVImporter.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitBullCSVImporter.kt index d14b71485..1ba79de9c 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitBullCSVImporter.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitBullCSVImporter.kt @@ -37,9 +37,9 @@ import javax.inject.Inject */ class HabitBullCSVImporter @Inject constructor( - habits: HabitList, + private val habitList: HabitList, private val modelFactory: ModelFactory, -) : AbstractImporter(habits) { +) : AbstractImporter() { override fun canHandle(file: File): Boolean { val reader = BufferedReader(FileReader(file)) diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java deleted file mode 100644 index 5f809bd1d..000000000 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.java +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker 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. - * - * Loop Habit Tracker 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 . - */ - -package org.isoron.uhabits.core.io; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; - -import java.io.*; -import java.text.*; -import java.util.*; -import java.util.zip.*; - -/** - * Class that exports the application data to CSV files. - */ -public class HabitsCSVExporter -{ - private List selectedHabits; - - private List generateDirs; - - private List generateFilenames; - - private String exportDirName; - /** - * Delimiter used in a CSV file. - */ - private final String DELIMITER = ","; - - @NonNull - private final HabitList allHabits; - - public HabitsCSVExporter(@NonNull HabitList allHabits, - @NonNull List selectedHabits, - @NonNull File dir) - { - this.allHabits = allHabits; - this.selectedHabits = selectedHabits; - this.exportDirName = dir.getAbsolutePath() + "/"; - - generateDirs = new LinkedList<>(); - generateFilenames = new LinkedList<>(); - } - - public String writeArchive() throws IOException - { - String zipFilename; - - writeHabits(); - zipFilename = writeZipFile(); - cleanup(); - - return zipFilename; - } - - private void addFileToZip(ZipOutputStream zos, String filename) - throws IOException - { - FileInputStream fis = - new FileInputStream(new File(exportDirName + filename)); - ZipEntry ze = new ZipEntry(filename); - zos.putNextEntry(ze); - - int length; - byte bytes[] = new byte[1024]; - while ((length = fis.read(bytes)) >= 0) zos.write(bytes, 0, length); - - zos.closeEntry(); - fis.close(); - } - - private void cleanup() - { - for (String filename : generateFilenames) - new File(exportDirName + filename).delete(); - - for (String filename : generateDirs) - new File(exportDirName + filename).delete(); - - new File(exportDirName).delete(); - } - - @NonNull - private String sanitizeFilename(String name) - { - String s = name.replaceAll("[^ a-zA-Z0-9\\._-]+", ""); - return s.substring(0, Math.min(s.length(), 100)); - } - - private void writeHabits() throws IOException - { - String filename = "Habits.csv"; - new File(exportDirName).mkdirs(); - FileWriter out = new FileWriter(exportDirName + filename); - generateFilenames.add(filename); - allHabits.writeCSV(out); - out.close(); - - for (Habit h : selectedHabits) - { - String sane = sanitizeFilename(h.getName()); - String habitDirName = - String.format(Locale.US, "%03d %s", allHabits.indexOf(h) + 1, sane); - habitDirName = habitDirName.trim() + "/"; - - new File(exportDirName + habitDirName).mkdirs(); - generateDirs.add(habitDirName); - - writeScores(habitDirName, h); - writeEntries(habitDirName, h.getComputedEntries()); - } - - writeMultipleHabits(); - } - - private void writeScores(String habitDirName, Habit habit) - throws IOException - { - String path = habitDirName + "Scores.csv"; - FileWriter out = new FileWriter(exportDirName + path); - generateFilenames.add(path); - SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); - - Timestamp today = DateUtils.getTodayWithOffset(); - Timestamp oldest = today; - List known = habit.getComputedEntries().getKnown(); - if(!known.isEmpty()) oldest = known.get(known.size() - 1).getTimestamp(); - - for (Score s : habit.getScores().getByInterval(oldest, today)) - { - String timestamp = dateFormat.format(s.getTimestamp().getUnixTime()); - String score = String.format(Locale.US, "%.4f", s.getValue()); - out.write(String.format("%s,%s\n", timestamp, score)); - } - out.close(); - } - - private void writeEntries(String habitDirName, EntryList entries) - throws IOException - { - String filename = habitDirName + "Checkmarks.csv"; - FileWriter out = new FileWriter(exportDirName + filename); - generateFilenames.add(filename); - - SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); - for (Entry e : entries.getKnown()) - { - String date = dateFormat.format(e.getTimestamp().toJavaDate()); - out.write(String.format(Locale.US, "%s,%d\n", date, e.getValue())); - } - - out.close(); - } - - /** - * Writes a scores file and a checkmarks file containing scores and checkmarks of every habit. - * The first column corresponds to the date. Subsequent columns correspond to a habit. - * Habits are taken from the list of selected habits. - * Dates are determined from the oldest repetition date to the newest repetition date found in - * the list of habits. - * - * @throws IOException if there was problem writing the files - */ - private void writeMultipleHabits() throws IOException - { - String scoresFileName = "Scores.csv"; - String checksFileName = "Checkmarks.csv"; - generateFilenames.add(scoresFileName); - generateFilenames.add(checksFileName); - FileWriter scoresWriter = new FileWriter(exportDirName + scoresFileName); - FileWriter checksWriter = new FileWriter(exportDirName + checksFileName); - - writeMultipleHabitsHeader(scoresWriter); - writeMultipleHabitsHeader(checksWriter); - - Timestamp[] timeframe = getTimeframe(); - Timestamp oldest = timeframe[0]; - Timestamp newest = DateUtils.getToday(); - - List checkmarks = new ArrayList<>(); - List> scores = new ArrayList<>(); - for (Habit h : selectedHabits) - { - checkmarks.add(h.getComputedEntries().getValues(oldest, newest)); - scores.add(new ArrayList<>(h.getScores().getByInterval(oldest, newest))); - } - - int days = oldest.daysUntil(newest); - SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); - for (int i = 0; i <= days; i++) - { - Date day = newest.minus(i).toJavaDate(); - - String date = dateFormat.format(day); - StringBuilder sb = new StringBuilder(); - sb.append(date).append(DELIMITER); - checksWriter.write(sb.toString()); - scoresWriter.write(sb.toString()); - - for(int j = 0; j < selectedHabits.size(); j++) - { - checksWriter.write(String.valueOf(checkmarks.get(j)[i])); - checksWriter.write(DELIMITER); - String score = String.format(Locale.US, "%.4f", scores.get(j).get(i).getValue()); - scoresWriter.write(score); - scoresWriter.write(DELIMITER); - } - checksWriter.write("\n"); - scoresWriter.write("\n"); - } - scoresWriter.close(); - checksWriter.close(); - } - - /** - * Writes the first row, containing header information, using the given writer. - * This consists of the date title and the names of the selected habits. - * - * @param out the writer to use - * @throws IOException if there was a problem writing - */ - private void writeMultipleHabitsHeader(Writer out) throws IOException - { - out.write("Date" + DELIMITER); - for (Habit h : selectedHabits) { - out.write(h.getName()); - out.write(DELIMITER); - } - out.write("\n"); - } - - /** - * Gets the overall timeframe of the selected habits. - * The timeframe is an array containing the oldest timestamp among the habits and the - * newest timestamp among the habits. - * Both timestamps are in milliseconds. - * - * @return the timeframe containing the oldest timestamp and the newest timestamp - */ - private Timestamp[] getTimeframe() - { - Timestamp oldest = Timestamp.ZERO.plus(1000000); - Timestamp newest = Timestamp.ZERO; - for (Habit h : selectedHabits) - { - List entries = h.getOriginalEntries().getKnown(); - if (entries.isEmpty()) continue; - Timestamp currNew = entries.get(0).getTimestamp(); - Timestamp currOld = entries.get(entries.size() - 1).getTimestamp(); - oldest = currOld.isOlderThan(oldest) ? currOld : oldest; - newest = currNew.isNewerThan(newest) ? currNew : newest; - } - return new Timestamp[]{oldest, newest}; - } - - private String writeZipFile() throws IOException - { - SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); - String date = dateFormat.format(DateUtils.getStartOfToday()); - String zipFilename = - String.format("%s/Loop Habits CSV %s.zip", exportDirName, date); - - FileOutputStream fos = new FileOutputStream(zipFilename); - ZipOutputStream zos = new ZipOutputStream(fos); - - for (String filename : generateFilenames) - addFileToZip(zos, filename); - - zos.close(); - fos.close(); - - return zipFilename; - } -} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt new file mode 100644 index 000000000..2cd7dbcd4 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/HabitsCSVExporter.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker 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. + * + * Loop Habit Tracker 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 . + */ +package org.isoron.uhabits.core.io + +import org.isoron.uhabits.core.models.EntryList +import org.isoron.uhabits.core.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.Score +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.utils.DateFormats +import org.isoron.uhabits.core.utils.DateUtils +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.FileWriter +import java.io.IOException +import java.io.Writer +import java.util.ArrayList +import java.util.LinkedList +import java.util.Locale +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +/** + * Class that exports the application data to CSV files. + */ +class HabitsCSVExporter( + private val allHabits: HabitList, + private val selectedHabits: List, + dir: File +) { + private val generatedDirs = LinkedList() + private val generatedFilenames = LinkedList() + private val exportDirName: String = dir.absolutePath + "/" + private val delimiter = "," + + fun writeArchive(): String { + writeHabits() + val zipFilename = writeZipFile() + cleanup() + return zipFilename + } + + private fun addFileToZip(zos: ZipOutputStream, filename: String) { + val fis = FileInputStream(File(exportDirName + filename)) + val ze = ZipEntry(filename) + zos.putNextEntry(ze) + var length: Int + val bytes = ByteArray(1024) + while (fis.read(bytes).also { length = it } >= 0) zos.write(bytes, 0, length) + zos.closeEntry() + fis.close() + } + + private fun cleanup() { + for (filename in generatedFilenames) File(exportDirName + filename).delete() + for (filename in generatedDirs) File(exportDirName + filename).delete() + File(exportDirName).delete() + } + + private fun sanitizeFilename(name: String): String { + val s = name.replace("[^ a-zA-Z0-9\\._-]+".toRegex(), "") + return s.substring(0, Math.min(s.length, 100)) + } + + private fun writeHabits() { + val filename = "Habits.csv" + File(exportDirName).mkdirs() + val out = FileWriter(exportDirName + filename) + generatedFilenames.add(filename) + allHabits.writeCSV(out) + out.close() + for (h in selectedHabits) { + val sane = sanitizeFilename(h.name) + var habitDirName = String.format(Locale.US, "%03d %s", allHabits.indexOf(h) + 1, sane) + habitDirName = habitDirName.trim() + "/" + File(exportDirName + habitDirName).mkdirs() + generatedDirs.add(habitDirName) + writeScores(habitDirName, h) + writeEntries(habitDirName, h.computedEntries) + } + writeMultipleHabits() + } + + private fun writeScores(habitDirName: String, habit: Habit) { + val path = habitDirName + "Scores.csv" + val out = FileWriter(exportDirName + path) + generatedFilenames.add(path) + val dateFormat = DateFormats.getCSVDateFormat() + val today = DateUtils.getTodayWithOffset() + var oldest = today + val known = habit.computedEntries.getKnown() + if (known.isNotEmpty()) oldest = known[known.size - 1].timestamp + for ((timestamp1, value) in habit.scores.getByInterval(oldest, today)) { + val timestamp = dateFormat.format(timestamp1.unixTime) + val score = String.format(Locale.US, "%.4f", value) + out.write(String.format("%s,%s\n", timestamp, score)) + } + out.close() + } + + private fun writeEntries(habitDirName: String, entries: EntryList) { + val filename = habitDirName + "Checkmarks.csv" + val out = FileWriter(exportDirName + filename) + generatedFilenames.add(filename) + val dateFormat = DateFormats.getCSVDateFormat() + for ((timestamp, value) in entries.getKnown()) { + val date = dateFormat.format(timestamp.toJavaDate()) + out.write(String.format(Locale.US, "%s,%d\n", date, value)) + } + out.close() + } + + /** + * Writes a scores file and a checkmarks file containing scores and checkmarks of every habit. + * The first column corresponds to the date. Subsequent columns correspond to a habit. + * Habits are taken from the list of selected habits. + * Dates are determined from the oldest repetition date to the newest repetition date found in + * the list of habits. + */ + private fun writeMultipleHabits() { + val scoresFileName = "Scores.csv" + val checksFileName = "Checkmarks.csv" + generatedFilenames.add(scoresFileName) + generatedFilenames.add(checksFileName) + + val scoresWriter = FileWriter(exportDirName + scoresFileName) + val checksWriter = FileWriter(exportDirName + checksFileName) + writeMultipleHabitsHeader(scoresWriter) + writeMultipleHabitsHeader(checksWriter) + + val timeframe = getTimeframe() + val oldest = timeframe[0] + val newest = DateUtils.getToday() + val checkmarks: MutableList = ArrayList() + val scores: MutableList> = ArrayList() + for (habit in selectedHabits) { + checkmarks.add(habit.computedEntries.getValues(oldest, newest)) + scores.add(ArrayList(habit.scores.getByInterval(oldest, newest))) + } + + val days = oldest.daysUntil(newest) + val dateFormat = DateFormats.getCSVDateFormat() + for (i in 0..days) { + val day = newest.minus(i).toJavaDate() + val date = dateFormat.format(day) + val sb = StringBuilder() + sb.append(date).append(delimiter) + checksWriter.write(sb.toString()) + scoresWriter.write(sb.toString()) + for (j in selectedHabits.indices) { + checksWriter.write(checkmarks[j][i].toString()) + checksWriter.write(delimiter) + val score = String.format(Locale.US, "%.4f", scores[j][i].value) + scoresWriter.write(score) + scoresWriter.write(delimiter) + } + checksWriter.write("\n") + scoresWriter.write("\n") + } + scoresWriter.close() + checksWriter.close() + } + + /** + * Writes the first row, containing header information, using the given writer. + * This consists of the date title and the names of the selected habits. + * + * @param out the writer to use + * @throws IOException if there was a problem writing + */ + @Throws(IOException::class) + private fun writeMultipleHabitsHeader(out: Writer) { + out.write("Date$delimiter") + for (habit in selectedHabits) { + out.write(habit.name) + out.write(delimiter) + } + out.write("\n") + } + + /** + * Gets the overall timeframe of the selected habits. + * The timeframe is an array containing the oldest timestamp among the habits and the + * newest timestamp among the habits. + * Both timestamps are in milliseconds. + * + * @return the timeframe containing the oldest timestamp and the newest timestamp + */ + private fun getTimeframe(): Array { + var oldest = Timestamp.ZERO.plus(1000000) + var newest = Timestamp.ZERO + for (habit in selectedHabits) { + val entries = habit.originalEntries.getKnown() + if (entries.isEmpty()) continue + val currNew = entries[0].timestamp + val currOld = entries[entries.size - 1].timestamp + oldest = if (currOld.isOlderThan(oldest)) currOld else oldest + newest = if (currNew.isNewerThan(newest)) currNew else newest + } + return arrayOf(oldest, newest) + } + + private fun writeZipFile(): String { + val dateFormat = DateFormats.getCSVDateFormat() + val date = dateFormat.format(DateUtils.getStartOfToday()) + val zipFilename = String.format("%s/Loop Habits CSV %s.zip", exportDirName, date) + val fos = FileOutputStream(zipFilename) + val zos = ZipOutputStream(fos) + for (filename in generatedFilenames) addFileToZip(zos, filename) + zos.close() + fos.close() + return zipFilename + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.kt index e5c53c087..0d1baa298 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.kt +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/LoopDBImporter.kt @@ -33,6 +33,7 @@ import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.sqlite.records.EntryRecord import org.isoron.uhabits.core.models.sqlite.records.HabitRecord +import org.isoron.uhabits.core.utils.isSQLite3File import java.io.File import javax.inject.Inject @@ -46,12 +47,12 @@ class LoopDBImporter @AppScope val opener: DatabaseOpener, @AppScope val runner: CommandRunner, @AppScope logging: Logging, -) : AbstractImporter(habitList) { +) : AbstractImporter() { private val logger = logging.getLogger("LoopDBImporter") override fun canHandle(file: File): Boolean { - if (!isSQLite3File(file)) return false + if (!file.isSQLite3File()) return false val db = opener.open(file)!! var canHandle = true val c = db.query("select count(*) from SQLITE_MASTER where name='Habits' or name='Repetitions'") diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/RewireDBImporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/RewireDBImporter.java deleted file mode 100644 index 22f8f3ac4..000000000 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/RewireDBImporter.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (C) 2017 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker 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. - * - * Loop Habit Tracker 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 . - */ - -package org.isoron.uhabits.core.io; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.database.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; - -import java.io.*; -import java.util.*; - -import javax.inject.*; - -import static org.isoron.uhabits.core.models.Entry.*; - -/** - * Class that imports database files exported by Rewire. - */ -public class RewireDBImporter extends AbstractImporter -{ - private ModelFactory modelFactory; - - @NonNull - private final DatabaseOpener opener; - - @Inject - public RewireDBImporter(@NonNull HabitList habits, - @NonNull ModelFactory modelFactory, - @NonNull DatabaseOpener opener) - { - super(habits); - this.modelFactory = modelFactory; - this.opener = opener; - } - - @Override - public boolean canHandle(@NonNull File file) throws IOException - { - if (!isSQLite3File(file)) return false; - - Database db = opener.open(file); - Cursor c = db.query("select count(*) from SQLITE_MASTER " + - "where name='CHECKINS' or name='UNIT'"); - - boolean result = (c.moveToNext() && c.getInt(0) == 2); - - c.close(); - db.close(); - return result; - } - - @Override - public void importHabitsFromFile(@NonNull File file) throws IOException - { - Database db = opener.open(file); - db.beginTransaction(); - createHabits(db); - db.setTransactionSuccessful(); - db.endTransaction(); - db.close(); - } - - private void createHabits(Database db) - { - Cursor c = null; - - try - { - c = db.query("select _id, name, description, schedule, " + - "active_days, repeating_count, days, period " + - "from habits"); - if (!c.moveToNext()) return; - - do - { - int id = c.getInt(0); - String name = c.getString(1); - String description = c.getString(2); - int schedule = c.getInt(3); - String activeDays = c.getString(4); - int repeatingCount = c.getInt(5); - int days = c.getInt(6); - int periodIndex = c.getInt(7); - - Habit habit = modelFactory.buildHabit(); - habit.setName(name); - habit.setDescription(description == null ? "" : description); - - int periods[] = { 7, 31, 365 }; - int numerator, denominator; - - switch (schedule) - { - case 0: - numerator = activeDays.split(",").length; - denominator = 7; - break; - - case 1: - numerator = days; - denominator = (periods[periodIndex]); - break; - - case 2: - numerator = 1; - denominator = repeatingCount; - break; - - default: - throw new IllegalStateException(); - } - - habit.setFrequency(new Frequency(numerator, denominator)); - habitList.add(habit); - - createReminder(db, habit, id); - createCheckmarks(db, habit, id); - - } while (c.moveToNext()); - } - finally - { - if (c != null) c.close(); - } - } - - private void createCheckmarks(@NonNull Database db, - @NonNull Habit habit, - int rewireHabitId) - { - Cursor c = null; - - try - { - String[] params = { Integer.toString(rewireHabitId) }; - c = db.query( - "select distinct date from checkins where habit_id=? and type=2", - params); - if (!c.moveToNext()) return; - - do - { - String date = c.getString(0); - int year = Integer.parseInt(date.substring(0, 4)); - int month = Integer.parseInt(date.substring(4, 6)); - int day = Integer.parseInt(date.substring(6, 8)); - - GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); - cal.set(year, month - 1, day); - - habit.getOriginalEntries().add(new Entry(new Timestamp(cal), YES_MANUAL)); - } while (c.moveToNext()); - } - finally - { - if (c != null) c.close(); - } - } - - private void createReminder(Database db, Habit habit, int rewireHabitId) - { - String[] params = { Integer.toString(rewireHabitId) }; - Cursor c = null; - - try - { - c = db.query( - "select time, active_days from reminders where habit_id=? limit 1", - params); - - if (!c.moveToNext()) return; - int rewireReminder = Integer.parseInt(c.getString(0)); - if (rewireReminder <= 0 || rewireReminder >= 1440) return; - - boolean reminderDays[] = new boolean[7]; - - String activeDays[] = c.getString(1).split(","); - for (String d : activeDays) - { - int idx = (Integer.parseInt(d) + 1) % 7; - reminderDays[idx] = true; - } - - int hour = rewireReminder / 60; - int minute = rewireReminder % 60; - WeekdayList days = new WeekdayList(reminderDays); - - Reminder reminder = new Reminder(hour, minute, days); - habit.setReminder(reminder); - habitList.update(habit); - } - finally - { - if (c != null) c.close(); - } - } -} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/RewireDBImporter.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/RewireDBImporter.kt new file mode 100644 index 000000000..0c37e2731 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/RewireDBImporter.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2017 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker 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. + * + * Loop Habit Tracker 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 . + */ +package org.isoron.uhabits.core.io + +import org.isoron.uhabits.core.database.Cursor +import org.isoron.uhabits.core.database.Database +import org.isoron.uhabits.core.database.DatabaseOpener +import org.isoron.uhabits.core.models.Entry +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.Reminder +import org.isoron.uhabits.core.models.Timestamp +import org.isoron.uhabits.core.models.WeekdayList +import org.isoron.uhabits.core.utils.DateUtils +import org.isoron.uhabits.core.utils.isSQLite3File +import java.io.File +import javax.inject.Inject + +/** + * Class that imports database files exported by Rewire. + */ +class RewireDBImporter +@Inject constructor( + private val habitList: HabitList, + private val modelFactory: ModelFactory, + private val opener: DatabaseOpener +) : AbstractImporter() { + + override fun canHandle(file: File): Boolean { + if (!file.isSQLite3File()) return false + val db = opener.open(file) + val c = db.query( + "select count(*) from SQLITE_MASTER " + + "where name='CHECKINS' or name='UNIT'" + ) + val result = c.moveToNext() && c.getInt(0) == 2 + c.close() + db.close() + return result + } + + override fun importHabitsFromFile(file: File) { + val db = opener.open(file) + db.beginTransaction() + createHabits(db) + db.setTransactionSuccessful() + db.endTransaction() + db.close() + } + + private fun createHabits(db: Database) { + var c: Cursor? = null + try { + c = db.query( + "select _id, name, description, schedule, " + + "active_days, repeating_count, days, period " + + "from habits" + ) + if (!c.moveToNext()) return + do { + val id = c.getInt(0)!! + val name = c.getString(1) + val description = c.getString(2) + val schedule = c.getInt(3)!! + val activeDays = c.getString(4) + val repeatingCount = c.getInt(5)!! + val days = c.getInt(6)!! + val periodIndex = c.getInt(7)!! + + val habit = modelFactory.buildHabit() + habit.name = name!! + habit.description = description ?: "" + val periods = intArrayOf(7, 31, 365) + var numerator: Int + var denominator: Int + when (schedule) { + 0 -> { + numerator = activeDays!!.split(",").toTypedArray().size + denominator = 7 + } + 1 -> { + numerator = days + denominator = periods[periodIndex] + } + 2 -> { + numerator = 1 + denominator = repeatingCount + } + else -> throw IllegalStateException() + } + habit.frequency = Frequency(numerator, denominator) + habitList.add(habit) + createReminder(db, habit, id) + createCheckmarks(db, habit, id) + } while (c.moveToNext()) + } finally { + c?.close() + } + } + + private fun createCheckmarks( + db: Database, + habit: Habit, + rewireHabitId: Int + ) { + var c: Cursor? = null + try { + c = db.query( + "select distinct date from checkins where habit_id=? and type=2", + rewireHabitId.toString(), + ) + if (!c.moveToNext()) return + do { + val date = c.getString(0) + val year = date!!.substring(0, 4).toInt() + val month = date.substring(4, 6).toInt() + val day = date.substring(6, 8).toInt() + val cal = DateUtils.getStartOfTodayCalendar() + cal[year, month - 1] = day + habit.originalEntries.add(Entry(Timestamp(cal), Entry.YES_MANUAL)) + } while (c.moveToNext()) + } finally { + c?.close() + } + } + + private fun createReminder(db: Database, habit: Habit, rewireHabitId: Int) { + var c: Cursor? = null + try { + c = db.query( + "select time, active_days from reminders where habit_id=? limit 1", + rewireHabitId.toString(), + ) + if (!c.moveToNext()) return + val rewireReminder = c.getInt(0)!! + if (rewireReminder <= 0 || rewireReminder >= 1440) return + val reminderDays = BooleanArray(7) + val activeDays = c.getString(1)!!.split(",").toTypedArray() + for (d in activeDays) { + val idx = (d.toInt() + 1) % 7 + reminderDays[idx] = true + } + val hour = rewireReminder / 60 + val minute = rewireReminder % 60 + val days = WeekdayList(reminderDays) + val reminder = Reminder(hour, minute, days) + habit.reminder = reminder + habitList.update(habit) + } finally { + c?.close() + } + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/TickmateDBImporter.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/TickmateDBImporter.java deleted file mode 100644 index e198a0a9f..000000000 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/TickmateDBImporter.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2017 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker 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. - * - * Loop Habit Tracker 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 . - */ - -package org.isoron.uhabits.core.io; - -import androidx.annotation.*; - -import org.isoron.uhabits.core.database.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.utils.*; - -import java.io.*; -import java.util.*; - -import javax.inject.*; - -import static org.isoron.uhabits.core.models.Entry.*; - -/** - * Class that imports data from database files exported by Tickmate. - */ -public class TickmateDBImporter extends AbstractImporter -{ - private ModelFactory modelFactory; - - @NonNull - private final DatabaseOpener opener; - - @Inject - public TickmateDBImporter(@NonNull HabitList habits, - @NonNull ModelFactory modelFactory, - @NonNull DatabaseOpener opener) - { - super(habits); - this.modelFactory = modelFactory; - this.opener = opener; - } - - @Override - public boolean canHandle(@NonNull File file) throws IOException - { - if (!isSQLite3File(file)) return false; - - Database db = opener.open(file); - Cursor c = db.query("select count(*) from SQLITE_MASTER " + - "where name='tracks' or name='track2groups'"); - - boolean result = (c.moveToNext() && c.getInt(0) == 2); - - c.close(); - db.close(); - return result; - } - - @Override - public void importHabitsFromFile(@NonNull File file) throws IOException - { - final Database db = opener.open(file); - db.beginTransaction(); - createHabits(db); - db.setTransactionSuccessful(); - db.endTransaction(); - db.close(); - } - - private void createCheckmarks(@NonNull Database db, - @NonNull Habit habit, - int tickmateTrackId) - { - Cursor c = null; - - try - { - String[] params = {Integer.toString(tickmateTrackId)}; - c = db.query( - "select distinct year, month, day from ticks where _track_id=?", - params); - if (!c.moveToNext()) return; - - do - { - int year = c.getInt(0); - int month = c.getInt(1); - int day = c.getInt(2); - - GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); - cal.set(year, month, day); - - habit.getOriginalEntries().add(new Entry(new Timestamp(cal), YES_MANUAL)); - } while (c.moveToNext()); - } - finally - { - if (c != null) c.close(); - } - } - - private void createHabits(Database db) - { - Cursor c = null; - - try - { - c = db.query("select _id, name, description from tracks", - new String[0]); - if (!c.moveToNext()) return; - - do - { - int id = c.getInt(0); - String name = c.getString(1); - String description = c.getString(2); - - Habit habit = modelFactory.buildHabit(); - habit.setName(name); - habit.setDescription(description == null ? "" : description); - habit.setFrequency(Frequency.DAILY); - habitList.add(habit); - - createCheckmarks(db, habit, id); - - } while (c.moveToNext()); - } - finally - { - if (c != null) c.close(); - } - } -} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/TickmateDBImporter.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/TickmateDBImporter.kt new file mode 100644 index 000000000..8885167c1 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/io/TickmateDBImporter.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2017 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker 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. + * + * Loop Habit Tracker 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 . + */ +package org.isoron.uhabits.core.io + +import org.isoron.uhabits.core.database.Cursor +import org.isoron.uhabits.core.database.Database +import org.isoron.uhabits.core.database.DatabaseOpener +import org.isoron.uhabits.core.models.Entry +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.DateUtils +import org.isoron.uhabits.core.utils.isSQLite3File +import java.io.File +import javax.inject.Inject + +/** + * Class that imports data from database files exported by Tickmate. + */ +class TickmateDBImporter @Inject constructor( + private val habitList: HabitList, + private val modelFactory: ModelFactory, + private val opener: DatabaseOpener +) : AbstractImporter() { + + override fun canHandle(file: File): Boolean { + if (!file.isSQLite3File()) return false + val db = opener.open(file) + val c = db.query( + "select count(*) from SQLITE_MASTER " + + "where name='tracks' or name='track2groups'" + ) + val result = c.moveToNext() && c.getInt(0) == 2 + c.close() + db.close() + return result + } + + override fun importHabitsFromFile(file: File) { + val db = opener.open(file) + db.beginTransaction() + createHabits(db) + db.setTransactionSuccessful() + db.endTransaction() + db.close() + } + + private fun createCheckmarks( + db: Database, + habit: Habit, + tickmateTrackId: Int + ) { + var c: Cursor? = null + try { + c = db.query( + "select distinct year, month, day from ticks where _track_id=?", + tickmateTrackId.toString(), + ) + if (!c.moveToNext()) return + do { + val year = c.getInt(0)!! + val month = c.getInt(1)!! + val day = c.getInt(2)!! + val cal = DateUtils.getStartOfTodayCalendar() + cal[year, month] = day + habit.originalEntries.add(Entry(Timestamp(cal), Entry.YES_MANUAL)) + } while (c.moveToNext()) + } finally { + c?.close() + } + } + + private fun createHabits(db: Database) { + var c: Cursor? = null + try { + c = db.query("select _id, name, description from tracks") + if (!c.moveToNext()) return + do { + val id = c.getInt(0)!! + val name = c.getString(1) + val description = c.getString(2) + val habit = modelFactory.buildHabit() + habit.name = name!! + habit.description = description ?: "" + habit.frequency = Frequency.DAILY + habitList.add(habit) + createCheckmarks(db, habit, id) + } while (c.moveToNext()) + } finally { + c?.close() + } + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/tasks/ExportCSVTask.java b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/tasks/ExportCSVTask.java index 9bc392d99..27e26b8fe 100644 --- a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/tasks/ExportCSVTask.java +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/tasks/ExportCSVTask.java @@ -65,7 +65,7 @@ public class ExportCSVTask implements Task exporter = new HabitsCSVExporter(habitList, selectedHabits, outputDir); archiveFilename = exporter.writeArchive(); } - catch (IOException e) + catch (Exception e) { e.printStackTrace(); } diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/utils/FileExtensions.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/utils/FileExtensions.kt new file mode 100644 index 000000000..70b2b89cb --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/utils/FileExtensions.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016-2020 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker 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. + * + * Loop Habit Tracker 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 . + */ + +package org.isoron.uhabits.core.utils + +import java.io.File +import java.io.FileInputStream + +fun File.isSQLite3File(): Boolean { + val fis = FileInputStream(this) + val sqliteHeader = "SQLite format 3".toByteArray() + val buffer = ByteArray(sqliteHeader.size) + val count = fis.read(buffer) + return if (count < sqliteHeader.size) false else buffer.contentEquals(sqliteHeader) +} 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 f76ce25f4..0f35ec0f1 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 @@ -133,8 +133,8 @@ public class ImportTest extends BaseUnitTest assertTrue(file.exists()); assertTrue(file.canRead()); - GenericImporter importer = new GenericImporter(habitList, - new LoopDBImporter(habitList, modelFactory, databaseOpener, commandRunner, new StandardLogging()), + GenericImporter importer = new GenericImporter( + new LoopDBImporter(habitList, modelFactory, databaseOpener, commandRunner, new StandardLogging()), new RewireDBImporter(habitList, modelFactory, databaseOpener), new TickmateDBImporter(habitList, modelFactory, databaseOpener), new HabitBullCSVImporter(habitList, modelFactory));