From 7cab0a39e51c9cc11137b8ae10465e3feb99719e Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 26 Jan 2019 22:58:53 -0600 Subject: [PATCH] Implement database access (with migrations) --- android/build.gradle | 1 + .../gradle/wrapper/gradle-wrapper.properties | 19 ++ .../org/isoron/habits/AndroidDatabaseTest.kt | 72 +++++++ .../org/isoron/habits/AndroidFilesTest.kt | 46 +++++ .../java/org/isoron/habits/BaseTest.kt | 28 +++ android/src/main/assets | 1 + .../java/org/isoron/habits/AndroidDatabase.kt | 33 ++++ .../java/org/isoron/habits/AndroidFiles.kt | 62 ++++++ .../main/java/org/isoron/habits/AndroidLog.kt | 32 +++ .../main/java/org/isoron/habits/CoreModule.kt | 33 ++-- .../assets/main}/fonts/FontAwesome.ttf | Bin core/assets/main/migrations/001.sql | 0 core/assets/main/migrations/002.sql | 0 core/assets/main/migrations/003.sql | 0 core/assets/main/migrations/004.sql | 0 core/assets/main/migrations/005.sql | 0 core/assets/main/migrations/006.sql | 0 core/assets/main/migrations/007.sql | 0 core/assets/main/migrations/008.sql | 0 core/assets/main/migrations/009.sql | 5 + core/assets/main/migrations/010.sql | 3 + core/assets/main/migrations/011.sql | 1 + core/assets/main/migrations/012.sql | 3 + core/assets/main/migrations/013.sql | 4 + core/assets/main/migrations/014.sql | 14 ++ core/assets/main/migrations/015.sql | 3 + core/assets/main/migrations/016.sql | 2 + core/assets/main/migrations/017.sql | 5 + core/assets/main/migrations/018.sql | 3 + core/assets/main/migrations/019.sql | 1 + core/assets/main/migrations/020.sql | 3 + core/assets/main/migrations/021.sql | 12 ++ core/assets/main/migrations/022.sql | 11 ++ core/assets/test/hello.txt | 2 + core/build.gradle | 18 +- .../kotlin/org/isoron/uhabits/Backend.kt | 47 ++--- .../kotlin/org/isoron/uhabits/Config.kt | 22 +++ .../org/isoron/uhabits/models/HabitList.kt | 57 ++++++ .../org/isoron/uhabits/utils/Database.kt | 88 +++++++++ .../kotlin/org/isoron/uhabits/utils/Files.kt | 60 ++++++ .../kotlin/org/isoron/uhabits/utils/Log.kt | 25 +++ .../org/isoron/uhabits/utils/Strings.kt | 22 +++ .../isoron/uhabits/utils/StringsTest.kt} | 27 +-- .../org/isoron/uhabits/utils/Strings.kt | 30 +++ .../isoron/uhabits/database/JavaDatabase.kt | 87 ++++++++ .../org/isoron/uhabits/utils/JavaFiles.kt | 59 ++++++ .../org/isoron/uhabits/utils/JavaLog.kt | 30 +++ .../org/isoron/uhabits/utils/Strings.kt | 24 +++ .../kotlin/org/isoron/uhabits/BackendTest.kt | 48 +++++ .../kotlin/org/isoron/uhabits/BaseTest.kt | 29 +++ .../uhabits/database/JavaDatabaseTest.kt | 82 ++++++++ .../org/isoron/uhabits/utils/JavaFilesTest.kt | 38 ++++ ios/CoreModule.swift | 49 ----- ios/uhabits.xcodeproj/project.pbxproj | 187 +++++++++++++++++- .../xcshareddata/xcschemes/uhabits.xcscheme | 18 ++ ios/uhabits/AppDelegate.swift | 35 ++-- ios/uhabits/BridgingHeader.h | 19 ++ ios/uhabits/CoreModule.swift | 81 ++++++++ ios/uhabits/CoreModuleBridge.m | 19 ++ ios/uhabits/IosDatabase.swift | 115 +++++++++++ ios/uhabits/IosFiles.swift | 77 ++++++++ ios/uhabits/IosLog.swift | 30 +++ ios/uhabitsTest/Info.plist | 22 +++ ios/uhabitsTest/IosFilesTest.swift | 44 +++++ ios/uhabitsTest/IosSqlDatabaseTest.swift | 73 +++++++ .../src/components/ListHabits/HabitList.js | 1 - .../components/ListHabits/HabitListHeader.js | 4 - 67 files changed, 1828 insertions(+), 138 deletions(-) create mode 100644 android/src/androidTest/java/org/isoron/habits/AndroidDatabaseTest.kt create mode 100644 android/src/androidTest/java/org/isoron/habits/AndroidFilesTest.kt create mode 100644 android/src/androidTest/java/org/isoron/habits/BaseTest.kt create mode 120000 android/src/main/assets create mode 100644 android/src/main/java/org/isoron/habits/AndroidDatabase.kt create mode 100644 android/src/main/java/org/isoron/habits/AndroidFiles.kt create mode 100644 android/src/main/java/org/isoron/habits/AndroidLog.kt rename {android/src/main/assets => core/assets/main}/fonts/FontAwesome.ttf (100%) create mode 100644 core/assets/main/migrations/001.sql create mode 100644 core/assets/main/migrations/002.sql create mode 100644 core/assets/main/migrations/003.sql create mode 100644 core/assets/main/migrations/004.sql create mode 100644 core/assets/main/migrations/005.sql create mode 100644 core/assets/main/migrations/006.sql create mode 100644 core/assets/main/migrations/007.sql create mode 100644 core/assets/main/migrations/008.sql create mode 100644 core/assets/main/migrations/009.sql create mode 100644 core/assets/main/migrations/010.sql create mode 100644 core/assets/main/migrations/011.sql create mode 100644 core/assets/main/migrations/012.sql create mode 100644 core/assets/main/migrations/013.sql create mode 100644 core/assets/main/migrations/014.sql create mode 100644 core/assets/main/migrations/015.sql create mode 100644 core/assets/main/migrations/016.sql create mode 100644 core/assets/main/migrations/017.sql create mode 100644 core/assets/main/migrations/018.sql create mode 100644 core/assets/main/migrations/019.sql create mode 100644 core/assets/main/migrations/020.sql create mode 100644 core/assets/main/migrations/021.sql create mode 100644 core/assets/main/migrations/022.sql create mode 100644 core/assets/test/hello.txt create mode 100644 core/src/commonMain/kotlin/org/isoron/uhabits/Config.kt create mode 100644 core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitList.kt create mode 100644 core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt create mode 100644 core/src/commonMain/kotlin/org/isoron/uhabits/utils/Files.kt create mode 100644 core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt create mode 100644 core/src/commonMain/kotlin/org/isoron/uhabits/utils/Strings.kt rename core/src/commonTest/kotlin/{BackendTest.kt => org/isoron/uhabits/utils/StringsTest.kt} (52%) create mode 100644 core/src/iosMain/kotlin/org/isoron/uhabits/utils/Strings.kt create mode 100644 core/src/jvmMain/kotlin/org/isoron/uhabits/database/JavaDatabase.kt create mode 100644 core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaFiles.kt create mode 100644 core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaLog.kt create mode 100644 core/src/jvmMain/kotlin/org/isoron/uhabits/utils/Strings.kt create mode 100644 core/src/jvmTest/kotlin/org/isoron/uhabits/BackendTest.kt create mode 100644 core/src/jvmTest/kotlin/org/isoron/uhabits/BaseTest.kt create mode 100644 core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt create mode 100644 core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaFilesTest.kt delete mode 100644 ios/CoreModule.swift create mode 100644 ios/uhabits/CoreModule.swift create mode 100644 ios/uhabits/IosDatabase.swift create mode 100644 ios/uhabits/IosFiles.swift create mode 100644 ios/uhabits/IosLog.swift create mode 100644 ios/uhabitsTest/Info.plist create mode 100644 ios/uhabitsTest/IosFilesTest.swift create mode 100644 ios/uhabitsTest/IosSqlDatabaseTest.swift diff --git a/android/build.gradle b/android/build.gradle index 4b971adbd..92071ff9d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -67,6 +67,7 @@ dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' implementation files("../core/build/libs/core-jvm.jar") implementation "com.facebook.react:react-native:0.57.8" + implementation 'org.sqldroid:sqldroid:1.0.3' implementation project(':react-native-svg') testImplementation 'junit:junit:4.12' diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 558870dad..3bc259a2f 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,3 +1,22 @@ +# +# Copyright (C) 2016-2019 Á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 . +# + distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip diff --git a/android/src/androidTest/java/org/isoron/habits/AndroidDatabaseTest.kt b/android/src/androidTest/java/org/isoron/habits/AndroidDatabaseTest.kt new file mode 100644 index 000000000..36248728a --- /dev/null +++ b/android/src/androidTest/java/org/isoron/habits/AndroidDatabaseTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2016-2019 Á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.habits + +import junit.framework.Assert.* +import org.isoron.uhabits.utils.* +import org.junit.* + +class AndroidDatabaseTest : BaseTest() { + @Test + fun testUsage() { + val dbFile = fileOpener.openUserFile("test.sqlite3") + if (dbFile.exists()) dbFile.delete() + val db = databaseOpener.open(dbFile) + + var stmt = db.prepareStatement("drop table if exists demo") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement("begin") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement("create table if not exists demo(key int, value text)") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement("insert into demo(key, value) values (?1, ?2)") + stmt.bindInt(1, 42) + stmt.bindText(2, "Hello World") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement("select * from demo where key > ?1") + stmt.bindInt(1, 10) + var result = stmt.step() + assertEquals(result, StepResult.ROW) + assertEquals(stmt.getInt(0), 42) + assertEquals(stmt.getText(1), "Hello World") + result = stmt.step() + assertEquals(result, StepResult.DONE) + stmt.finalize() + + stmt = db.prepareStatement("drop table demo") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement("commit") + stmt.step() + stmt.finalize() + + db.close() + dbFile.delete() + } +} \ No newline at end of file diff --git a/android/src/androidTest/java/org/isoron/habits/AndroidFilesTest.kt b/android/src/androidTest/java/org/isoron/habits/AndroidFilesTest.kt new file mode 100644 index 000000000..95b2f453a --- /dev/null +++ b/android/src/androidTest/java/org/isoron/habits/AndroidFilesTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016-2019 Á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.habits + +import org.junit.Test + +import org.junit.Assert.* +import java.io.* + +class AndroidFilesTest : BaseTest() { + + @Test + fun testUserFiles() { + val file = File(context.filesDir, "test.txt") + file.writeText("Hello world!") + + val af = fileOpener.openUserFile("test.txt") + assertTrue(af.exists()) + af.delete() + assertFalse(af.exists()) + } + + @Test + fun testResourceFiles() { + val file = fileOpener.openResourceFile("migrations/010.sql") + val lines = file.readLines() + assertEquals("delete from Score", lines[0]) + } +} \ No newline at end of file diff --git a/android/src/androidTest/java/org/isoron/habits/BaseTest.kt b/android/src/androidTest/java/org/isoron/habits/BaseTest.kt new file mode 100644 index 000000000..a6cd8cd59 --- /dev/null +++ b/android/src/androidTest/java/org/isoron/habits/BaseTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2016-2019 Á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.habits + +import android.support.test.* + +open class BaseTest { + val context = InstrumentationRegistry.getTargetContext() + val fileOpener = AndroidFileOpener(context) + val databaseOpener = AndroidDatabaseOpener() +} \ No newline at end of file diff --git a/android/src/main/assets b/android/src/main/assets new file mode 120000 index 000000000..e99c645eb --- /dev/null +++ b/android/src/main/assets @@ -0,0 +1 @@ +../../../core/assets/main/ \ No newline at end of file diff --git a/android/src/main/java/org/isoron/habits/AndroidDatabase.kt b/android/src/main/java/org/isoron/habits/AndroidDatabase.kt new file mode 100644 index 000000000..6240bd749 --- /dev/null +++ b/android/src/main/java/org/isoron/habits/AndroidDatabase.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2016-2019 Á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.habits + +import org.isoron.uhabits.database.* +import org.isoron.uhabits.utils.* +import java.sql.* + +class AndroidDatabaseOpener : DatabaseOpener { + override fun open(file: UserFile): Database { + val platformFile = file as AndroidUserFile + DriverManager.registerDriver(Class.forName("org.sqldroid.SQLDroidDriver").newInstance() as Driver) + val conn = DriverManager.getConnection("jdbc:sqlite:${platformFile.file.absolutePath}") + return JavaDatabase(conn, AndroidLog()) + } +} \ No newline at end of file diff --git a/android/src/main/java/org/isoron/habits/AndroidFiles.kt b/android/src/main/java/org/isoron/habits/AndroidFiles.kt new file mode 100644 index 000000000..b627d578e --- /dev/null +++ b/android/src/main/java/org/isoron/habits/AndroidFiles.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016-2019 Á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.habits + +import android.content.* +import org.isoron.uhabits.utils.* +import java.io.* +import java.util.* + + +class AndroidFileOpener(val context: Context) : FileOpener { + override fun openUserFile(filename: String): UserFile { + return AndroidUserFile(File(context.filesDir, filename)) + } + + override fun openResourceFile(filename: String): ResourceFile { + return AndroidResourceFile(context, filename) + } +} + +class AndroidResourceFile(val context: Context, + val filename: String) : ResourceFile { + + override fun readLines(): List { + val asset = context.assets.open(filename) + val reader = BufferedReader(InputStreamReader(asset)) + val lines = ArrayList() + while (true) { + val line = reader.readLine() ?: break + lines.add(line) + } + return lines + } +} + +class AndroidUserFile(val file: File) : UserFile { + override fun delete() { + file.delete() + } + + override fun exists(): Boolean { + return file.exists() + } + +} diff --git a/android/src/main/java/org/isoron/habits/AndroidLog.kt b/android/src/main/java/org/isoron/habits/AndroidLog.kt new file mode 100644 index 000000000..54b7513f0 --- /dev/null +++ b/android/src/main/java/org/isoron/habits/AndroidLog.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2016-2019 Á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.habits + +import org.isoron.uhabits.utils.* + +class AndroidLog : Log { + override fun debug(msg: String) { + android.util.Log.d("LOOP", msg) + } + + override fun info(msg: String) { + android.util.Log.i("LOOP", msg) + } +} \ No newline at end of file diff --git a/android/src/main/java/org/isoron/habits/CoreModule.kt b/android/src/main/java/org/isoron/habits/CoreModule.kt index f4c7413f1..8afc00178 100644 --- a/android/src/main/java/org/isoron/habits/CoreModule.kt +++ b/android/src/main/java/org/isoron/habits/CoreModule.kt @@ -24,24 +24,17 @@ import com.facebook.react.modules.core.DeviceEventManagerModule.* import org.isoron.uhabits.* -class CoreModule( - private val context: ReactApplicationContext -) : ReactContextBaseJavaModule(context) { +class CoreModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) { + + private var backend = Backend(AndroidDatabaseOpener(), + AndroidFileOpener(context), + AndroidLog()) - private var backend = Backend() private lateinit var emitter: RCTDeviceEventEmitter override fun initialize() { super.initialize() emitter = context.getJSModule(RCTDeviceEventEmitter::class.java) - backend.createHabit("Wake up early") - backend.createHabit("Wash clothes") - backend.createHabit("Exercise") - backend.createHabit("Meditate") - backend.createHabit("Take vitamins") - backend.createHabit("Write 'the quick brown fox jumps over the lazy dog' daily") - backend.createHabit("Write journal") - backend.createHabit("Study French") } override fun getName(): String { @@ -50,15 +43,17 @@ class CoreModule( @ReactMethod fun requestHabitList() { - val params = Arguments.createArray() - for ((id, data) in backend.getHabitList()) { - params.pushMap(Arguments.createMap().apply { - putString("key", id.toString()) - putString("name", data["name"] as String) - putInt("color", data["color"] as Int) + val result = backend.getHabitList() + val data = Arguments.createArray() + for (r in result) { + data.pushMap(Arguments.createMap().apply { + for ((key, value) in r) { + if (value is String) putString(key, value) + else if (value is Int) putInt(key, value) + } }) } - emitter.emit("onHabitList", params) + emitter.emit("onHabitList", data) } @ReactMethod diff --git a/android/src/main/assets/fonts/FontAwesome.ttf b/core/assets/main/fonts/FontAwesome.ttf similarity index 100% rename from android/src/main/assets/fonts/FontAwesome.ttf rename to core/assets/main/fonts/FontAwesome.ttf diff --git a/core/assets/main/migrations/001.sql b/core/assets/main/migrations/001.sql new file mode 100644 index 000000000..e69de29bb diff --git a/core/assets/main/migrations/002.sql b/core/assets/main/migrations/002.sql new file mode 100644 index 000000000..e69de29bb diff --git a/core/assets/main/migrations/003.sql b/core/assets/main/migrations/003.sql new file mode 100644 index 000000000..e69de29bb diff --git a/core/assets/main/migrations/004.sql b/core/assets/main/migrations/004.sql new file mode 100644 index 000000000..e69de29bb diff --git a/core/assets/main/migrations/005.sql b/core/assets/main/migrations/005.sql new file mode 100644 index 000000000..e69de29bb diff --git a/core/assets/main/migrations/006.sql b/core/assets/main/migrations/006.sql new file mode 100644 index 000000000..e69de29bb diff --git a/core/assets/main/migrations/007.sql b/core/assets/main/migrations/007.sql new file mode 100644 index 000000000..e69de29bb diff --git a/core/assets/main/migrations/008.sql b/core/assets/main/migrations/008.sql new file mode 100644 index 000000000..e69de29bb diff --git a/core/assets/main/migrations/009.sql b/core/assets/main/migrations/009.sql new file mode 100644 index 000000000..5a4afd962 --- /dev/null +++ b/core/assets/main/migrations/009.sql @@ -0,0 +1,5 @@ +create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer ) +create table Checkmarks ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer, value integer ) +create table Repetitions ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer ) +create table Streak ( id integer primary key autoincrement, end integer, habit integer references habits(id), length integer, start integer ) +create table Score ( id integer primary key autoincrement, habit integer references habits(id), score integer, timestamp integer ) diff --git a/core/assets/main/migrations/010.sql b/core/assets/main/migrations/010.sql new file mode 100644 index 000000000..afae84e20 --- /dev/null +++ b/core/assets/main/migrations/010.sql @@ -0,0 +1,3 @@ +delete from Score +delete from Streak +delete from Checkmarks \ No newline at end of file diff --git a/core/assets/main/migrations/011.sql b/core/assets/main/migrations/011.sql new file mode 100644 index 000000000..6ea4e2390 --- /dev/null +++ b/core/assets/main/migrations/011.sql @@ -0,0 +1 @@ +alter table Habits add column reminder_days integer not null default 127 \ No newline at end of file diff --git a/core/assets/main/migrations/012.sql b/core/assets/main/migrations/012.sql new file mode 100644 index 000000000..afae84e20 --- /dev/null +++ b/core/assets/main/migrations/012.sql @@ -0,0 +1,3 @@ +delete from Score +delete from Streak +delete from Checkmarks \ No newline at end of file diff --git a/core/assets/main/migrations/013.sql b/core/assets/main/migrations/013.sql new file mode 100644 index 000000000..dbb9b4438 --- /dev/null +++ b/core/assets/main/migrations/013.sql @@ -0,0 +1,4 @@ +create index idx_score_habit_timestamp on Score(habit, timestamp) +create index idx_checkmark_habit_timestamp on Checkmarks(habit, timestamp) +create index idx_repetitions_habit_timestamp on Repetitions(habit, timestamp) +create index idx_streak_habit_end on Streak(habit, end) \ No newline at end of file diff --git a/core/assets/main/migrations/014.sql b/core/assets/main/migrations/014.sql new file mode 100644 index 000000000..6eecfc046 --- /dev/null +++ b/core/assets/main/migrations/014.sql @@ -0,0 +1,14 @@ +update habits set color=0 where color=-2937041 +update habits set color=1 where color=-1684967 +update habits set color=2 where color=-415707 +update habits set color=3 where color=-5262293 +update habits set color=4 where color=-13070788 +update habits set color=5 where color=-16742021 +update habits set color=6 where color=-16732991 +update habits set color=7 where color=-16540699 +update habits set color=8 where color=-10603087 +update habits set color=9 where color=-7461718 +update habits set color=10 where color=-2614432 +update habits set color=11 where color=-13619152 +update habits set color=12 where color=-5592406 +update habits set color=0 where color<0 or color>12 \ No newline at end of file diff --git a/core/assets/main/migrations/015.sql b/core/assets/main/migrations/015.sql new file mode 100644 index 000000000..afae84e20 --- /dev/null +++ b/core/assets/main/migrations/015.sql @@ -0,0 +1,3 @@ +delete from Score +delete from Streak +delete from Checkmarks \ No newline at end of file diff --git a/core/assets/main/migrations/016.sql b/core/assets/main/migrations/016.sql new file mode 100644 index 000000000..059f2016b --- /dev/null +++ b/core/assets/main/migrations/016.sql @@ -0,0 +1,2 @@ +alter table Habits add column type integer not null default 0 +alter table Repetitions add column value integer not null default 2 \ No newline at end of file diff --git a/core/assets/main/migrations/017.sql b/core/assets/main/migrations/017.sql new file mode 100644 index 000000000..15430d771 --- /dev/null +++ b/core/assets/main/migrations/017.sql @@ -0,0 +1,5 @@ +drop table Score +create table Score ( id integer primary key autoincrement, habit integer references habits(id), score real, timestamp integer) +create index idx_score_habit_timestamp on Score(habit, timestamp) +delete from streak +delete from checkmarks \ No newline at end of file diff --git a/core/assets/main/migrations/018.sql b/core/assets/main/migrations/018.sql new file mode 100644 index 000000000..4cdc9cc8d --- /dev/null +++ b/core/assets/main/migrations/018.sql @@ -0,0 +1,3 @@ +alter table Habits add column target_type integer not null default 0 +alter table Habits add column target_value real not null default 0 +alter table Habits add column unit text not null default "" \ No newline at end of file diff --git a/core/assets/main/migrations/019.sql b/core/assets/main/migrations/019.sql new file mode 100644 index 000000000..0569ea531 --- /dev/null +++ b/core/assets/main/migrations/019.sql @@ -0,0 +1 @@ +create table Events ( id integer primary key autoincrement, timestamp integer, message text, server_id integer ) \ No newline at end of file diff --git a/core/assets/main/migrations/020.sql b/core/assets/main/migrations/020.sql new file mode 100644 index 000000000..254bb7b80 --- /dev/null +++ b/core/assets/main/migrations/020.sql @@ -0,0 +1,3 @@ +drop table checkmarks +drop table streak +drop table score diff --git a/core/assets/main/migrations/021.sql b/core/assets/main/migrations/021.sql new file mode 100644 index 000000000..547b6759d --- /dev/null +++ b/core/assets/main/migrations/021.sql @@ -0,0 +1,12 @@ +update habits set color=19 where color=12 +update habits set color=17 where color=11 +update habits set color=15 where color=10 +update habits set color=14 where color=9 +update habits set color=13 where color=8 +update habits set color=10 where color=7 +update habits set color=9 where color=6 +update habits set color=8 where color=5 +update habits set color=7 where color=4 +update habits set color=5 where color=3 +update habits set color=4 where color=2 +update habits set color=0 where color<0 or color>19 \ No newline at end of file diff --git a/core/assets/main/migrations/022.sql b/core/assets/main/migrations/022.sql new file mode 100644 index 000000000..b9ca2ba5a --- /dev/null +++ b/core/assets/main/migrations/022.sql @@ -0,0 +1,11 @@ +delete from repetitions where habit not in (select id from habits) +delete from repetitions where timestamp is null +delete from repetitions where habit is null +delete from repetitions where rowid not in ( select min(rowid) from repetitions group by habit, timestamp ) +alter table Repetitions rename to RepetitionsBak +create table Repetitions ( id integer primary key autoincrement, habit integer not null references habits(id), timestamp integer not null, value integer not null) +drop index if exists idx_repetitions_habit_timestamp +create unique index idx_repetitions_habit_timestamp on Repetitions( habit, timestamp) +insert into Repetitions select * from RepetitionsBak +drop table RepetitionsBak +pragma foreign_keys=ON \ No newline at end of file diff --git a/core/assets/test/hello.txt b/core/assets/test/hello.txt new file mode 100644 index 000000000..e432ffe94 --- /dev/null +++ b/core/assets/test/hello.txt @@ -0,0 +1,2 @@ +Hello World! +This is a resource. \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index 03869821b..b4157290f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -48,8 +48,8 @@ kotlin { compilations.main.outputKinds('FRAMEWORK') } - // Replace the target above with the following to produce a framework - // which can be installed on a real iPhone. + // Replace the target above by the following one to produce a framework + // which can be installed on a real iPhone: // fromPreset(presets.iosArm64, 'iOS') { // compilations.main.outputKinds('FRAMEWORK') // } @@ -76,6 +76,20 @@ kotlin { dependencies { implementation 'org.jetbrains.kotlin:kotlin-test' implementation 'org.jetbrains.kotlin:kotlin-test-junit' + implementation 'org.xerial:sqlite-jdbc:3.25.2' + } + } + } + + task iosTest { + dependsOn 'linkTestDebugExecutableIOS' + group = JavaBasePlugin.VERIFICATION_GROUP + description = "Runs tests for target 'ios' on an iOS simulator" + + doLast { + def binary = kotlin.targets.iOS.compilations.test.getBinary('EXECUTABLE', 'DEBUG') + exec { + commandLine 'xcrun', 'simctl', 'spawn', "iPhone 8", binary.absolutePath } } } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt index 422d84a71..13defaee5 100644 --- a/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/Backend.kt @@ -19,37 +19,38 @@ package org.isoron.uhabits -import org.isoron.uhabits.models.Color -import org.isoron.uhabits.models.Frequency -import org.isoron.uhabits.models.Habit -import org.isoron.uhabits.models.HabitType -import kotlin.random.Random - -class Backend { - var nextId = 1 - var habits = mutableListOf() - - fun getHabitList(): Map> { - return habits.map { h -> - h.id to mapOf("name" to h.name, - "color" to h.color.paletteIndex) - }.toMap() +import org.isoron.uhabits.models.HabitList +import org.isoron.uhabits.utils.* + +class Backend(var databaseOpener: DatabaseOpener, + var fileOpener: FileOpener, + var log: Log) { + + var db: Database + + var habits: HabitList + + init { + val dbFile = fileOpener.openUserFile("uhabits.sqlite3") + db = databaseOpener.open(dbFile) + db.migrateTo(LOOP_DATABASE_VERSION, fileOpener, log) + habits = HabitList(db) + } + + fun getHabitList(): List> { + return habits.getActive().map { h -> + mapOf("key" to h.id.toString(), + "name" to h.name, + "color" to h.color.paletteIndex) + } } fun createHabit(name: String) { - val c = (nextId / 4) % 5 - habits.add(Habit(nextId, name, "", Frequency(1, 1), Color(c), - false, habits.size, "", 0, HabitType.YES_NO_HABIT)) - nextId += 1 } fun deleteHabit(id: Int) { - val h = habits.find { h -> h.id == id } - if (h != null) habits.remove(h) } fun updateHabit(id: Int, name: String) { - val h = habits.find { h -> h.id == id } - h?.name = name } } diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/Config.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/Config.kt new file mode 100644 index 000000000..38568d2da --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/Config.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016-2019 Á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 + +const val LOOP_DATABASE_VERSION = 22 \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitList.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitList.kt new file mode 100644 index 000000000..427b1b452 --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/models/HabitList.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016-2019 Á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.models + +import org.isoron.uhabits.utils.Database +import org.isoron.uhabits.utils.StepResult + +class HabitList(var db: Database) { + var habits = mutableListOf() + + init { + loadHabitsFromDatabase() + } + + fun getActive(): List { + return habits.filter { h -> !h.isArchived } + } + + private fun loadHabitsFromDatabase() { + val stmt = db.prepareStatement( + "select id, name, description, freq_num, freq_den, color, " + + "archived, position, unit, target_value, type " + + "from habits") + + while (stmt.step() == StepResult.ROW) { + habits.add(Habit(id = stmt.getInt(0), + name = stmt.getText(1), + description = stmt.getText(2), + frequency = Frequency(stmt.getInt(3), + stmt.getInt(4)), + color = Color(stmt.getInt(5)), + isArchived = (stmt.getInt(6) != 0), + position = stmt.getInt(7), + unit = stmt.getText(8), + target = 0, + type = HabitType.YES_NO_HABIT)) + } + stmt.finalize() + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt new file mode 100644 index 000000000..e4f827aca --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Database.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2016-2019 Á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.utils + +enum class StepResult { + ROW, + DONE +} + +interface PreparedStatement { + fun step(): StepResult + fun finalize() + fun getInt(index: Int): Int + fun getText(index: Int): String + fun bindInt(index: Int, value: Int) + fun bindText(index: Int, value: String) + fun reset() +} + +interface Database { + fun prepareStatement(sql: String): PreparedStatement + fun close() +} + +interface DatabaseOpener { + fun open(file: UserFile): Database +} + +fun Database.execute(sql: String) { + val stmt = prepareStatement(sql) + stmt.step() + stmt.finalize() +} + +fun Database.queryInt(sql: String): Int { + val stmt = prepareStatement(sql) + stmt.step() + val result = stmt.getInt(0) + stmt.finalize() + return result +} + +fun Database.begin() = execute("begin") +fun Database.commit() = execute("commit") +fun Database.rollback() = execute("rollback") +fun Database.getVersion() = queryInt("pragma user_version") +fun Database.setVersion(v: Int) = execute("pragma user_version = $v") + +fun Database.migrateTo(newVersion: Int, fileOpener: FileOpener, log: Log) { + val currentVersion = getVersion() + log.debug("Current database version: $currentVersion") + + if (currentVersion == newVersion) return + log.debug("Upgrading to version: $newVersion") + + if (currentVersion > newVersion) + throw RuntimeException("database produced by future version of the application") + + begin() + for (v in (currentVersion + 1)..newVersion) { + log.debug("Running migration $v") + val filename = sprintf("migrations/%03d.sql", v) + val migrationFile = fileOpener.openResourceFile(filename) + for (line in migrationFile.readLines()) { + if (line.isEmpty()) continue + execute(line) + } + setVersion(v) + } + commit() +} diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Files.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Files.kt new file mode 100644 index 000000000..ee72de9e8 --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Files.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016-2019 Á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.utils + +/** + * Represents a file that was shipped with the application, such as migration + * files or translations. These files cannot be deleted. + */ +interface ResourceFile { + fun readLines(): List +} + +/** + * Represents a file that was created after the application was installed, as a + * result of some user action, such as databases and logs. These files can be + * deleted. + */ +interface UserFile { + fun delete() + fun exists(): Boolean +} + +interface FileOpener { + /** + * Opens a file which was shipped bundled with the application, such as a + * migration file. + * + * The path is relative to the assets folder. For example, to open + * assets/main/migrations/09.sql you should provide migrations/09.sql + * as the filename. + */ + fun openResourceFile(filename: String): ResourceFile + + /** + * Opens a file which was not shipped with the application, such as + * databases and logs. + * + * The path is relative to the user folder. For example, if the application + * stores the user data at /home/user/.loop/ and you wish to open the file + * /home/user/.loop/crash.log, you should provide crash.log as the filename. + */ + fun openUserFile(filename: String): UserFile +} diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt new file mode 100644 index 000000000..d359b3911 --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Log.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016-2019 Á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.utils + +interface Log { + fun info(msg: String) + fun debug(msg: String) +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Strings.kt b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Strings.kt new file mode 100644 index 000000000..9139da30c --- /dev/null +++ b/core/src/commonMain/kotlin/org/isoron/uhabits/utils/Strings.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2016-2019 Á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.utils + +expect fun sprintf(format: String, vararg args: Any?): String \ No newline at end of file diff --git a/core/src/commonTest/kotlin/BackendTest.kt b/core/src/commonTest/kotlin/org/isoron/uhabits/utils/StringsTest.kt similarity index 52% rename from core/src/commonTest/kotlin/BackendTest.kt rename to core/src/commonTest/kotlin/org/isoron/uhabits/utils/StringsTest.kt index 4f614583f..5b0cb3ce4 100644 --- a/core/src/commonTest/kotlin/BackendTest.kt +++ b/core/src/commonTest/kotlin/org/isoron/uhabits/utils/StringsTest.kt @@ -17,33 +17,14 @@ * with this program. If not, see . */ -package org.isoron.uhabits +package org.isoron.uhabits.utils import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue -class BackendTest { +class StringsTest { @Test - fun testBackend() { - val backend = Backend() - assertEquals(backend.getHabitList().size, 0) - - backend.createHabit("Brush teeth") - backend.createHabit("Wake up early") - - var result = backend.getHabitList() - assertEquals(result.size, 2) - assertEquals(result[1]!!["name"], "Brush teeth") - assertEquals(result[2]!!["name"], "Wake up early") - - backend.deleteHabit(1) - result = backend.getHabitList() - assertEquals(result.size, 1) - assertTrue(2 in result.keys) - - backend.updateHabit(2, "Wake up late") - result = backend.getHabitList() - assertEquals(result[2]!!["name"], "Wake up late") + fun testSprintf() { + assertEquals("Number 004", sprintf("Number %03d", 4)) } } \ No newline at end of file diff --git a/core/src/iosMain/kotlin/org/isoron/uhabits/utils/Strings.kt b/core/src/iosMain/kotlin/org/isoron/uhabits/utils/Strings.kt new file mode 100644 index 000000000..00126c451 --- /dev/null +++ b/core/src/iosMain/kotlin/org/isoron/uhabits/utils/Strings.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016-2019 Á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.utils + +import kotlinx.cinterop.* + +actual fun sprintf(format: String, vararg args: Any?): String { + val buffer = ByteArray(1000) + buffer.usePinned { p -> + platform.posix.sprintf(p.addressOf(0), format, *args) + } + return buffer.stringFromUtf8() +} \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/database/JavaDatabase.kt b/core/src/jvmMain/kotlin/org/isoron/uhabits/database/JavaDatabase.kt new file mode 100644 index 000000000..2b4004c0f --- /dev/null +++ b/core/src/jvmMain/kotlin/org/isoron/uhabits/database/JavaDatabase.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016-2019 Á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.database + +import org.isoron.uhabits.utils.* +import java.sql.Connection +import java.sql.DriverManager +import java.sql.PreparedStatement +import java.sql.ResultSet + +class JavaPreparedStatement(private var stmt : PreparedStatement) : org.isoron.uhabits.utils.PreparedStatement { + + private var rs: ResultSet? = null + private var hasExecuted = false + + override fun step(): StepResult { + if (!hasExecuted) { + hasExecuted = true + val hasResult = stmt.execute() + if (hasResult) rs = stmt.resultSet + } + + if (rs == null || !rs!!.next()) return StepResult.DONE + return StepResult.ROW + } + + override fun finalize() { + stmt.close() + } + + override fun getInt(index: Int): Int { + return rs!!.getInt(index + 1) + } + + override fun getText(index: Int): String { + return rs!!.getString(index + 1) + } + + override fun bindInt(index: Int, value: Int) { + stmt.setInt(index, value) + } + + override fun bindText(index: Int, value: String) { + stmt.setString(index, value) + } + + override fun reset() { + stmt.clearParameters() + } +} + +class JavaDatabase(private var conn: Connection, + private val log: Log) : Database { + + override fun prepareStatement(sql: String): org.isoron.uhabits.utils.PreparedStatement { + log.debug("Running SQL: ${sql}") + return JavaPreparedStatement(conn.prepareStatement(sql)) + } + override fun close() { + conn.close() + } +} + +class JavaDatabaseOpener(private val log: Log) : DatabaseOpener { + override fun open(file: UserFile): Database { + val platformFile = file as JavaUserFile + val conn = DriverManager.getConnection("jdbc:sqlite:${platformFile.path}") + return JavaDatabase(conn, log) + } +} \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaFiles.kt b/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaFiles.kt new file mode 100644 index 000000000..fc6732914 --- /dev/null +++ b/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaFiles.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016-2019 Á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.utils + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +class JavaResourceFile(private val path: Path) : ResourceFile { + override fun readLines(): List { + return Files.readAllLines(path) + } +} + +class JavaUserFile(val path: Path) : UserFile { + override fun exists(): Boolean { + return Files.exists(path) + } + + override fun delete() { + Files.delete(path) + } +} + +class JavaFileOpener : FileOpener { + override fun openUserFile(filename: String): UserFile { + val path = Paths.get("/tmp/$filename") + return JavaUserFile(path) + } + + override fun openResourceFile(filename: String): ResourceFile { + val rootFolders = listOf("assets/main", + "assets/test") + for (root in rootFolders) { + val path = Paths.get("$root/$filename") + if (Files.exists(path) && Files.isReadable(path)) { + return JavaResourceFile(path) + } + } + throw RuntimeException("file not found") + } +} diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaLog.kt b/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaLog.kt new file mode 100644 index 000000000..32d5ca248 --- /dev/null +++ b/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/JavaLog.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016-2019 Á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.utils + +class JavaLog : Log { + override fun info(msg: String) { + println("[I] $msg") + } + + override fun debug(msg: String) { + println("[D] $msg") + } +} \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/Strings.kt b/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/Strings.kt new file mode 100644 index 000000000..8bc26773c --- /dev/null +++ b/core/src/jvmMain/kotlin/org/isoron/uhabits/utils/Strings.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016-2019 Á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.utils + +actual fun sprintf(format: String, vararg args: Any?): String { + return String.format(format, *args) +} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/BackendTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/BackendTest.kt new file mode 100644 index 000000000..d4eb10f19 --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/BackendTest.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016-2019 Á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 + +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertTrue +import org.junit.Test + +class BackendTest : BaseTest() { + @Test + fun testBackend() { +// val backend = Backend(databaseOpener, fileOpener) +// assertEquals(backend.getHabitList().size, 0) +// +// backend.createHabit("Brush teeth") +// backend.createHabit("Wake up early") + +// var result = backend.getHabitList() +// assertEquals(2, result.size) +// assertEquals(result[0]["name"], "Brush teeth") +// assertEquals(result[0]["name"], "Wake up early") +// +// backend.deleteHabit(1) +// result = backend.getHabitList() +// assertEquals(result.size, 1) +// +// backend.updateHabit(2, "Wake up late") +// result = backend.getHabitList() +// assertEquals(result[2]["name"], "Wake up late") + } +} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/BaseTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/BaseTest.kt new file mode 100644 index 000000000..2b6ec7ded --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/BaseTest.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2016-2019 Á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 + +import org.isoron.uhabits.utils.JavaFileOpener +import org.isoron.uhabits.database.JavaDatabaseOpener +import org.isoron.uhabits.utils.JavaLog + +open class BaseTest { + val fileOpener = JavaFileOpener() + val databaseOpener = JavaDatabaseOpener(JavaLog()) +} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt new file mode 100644 index 000000000..4746c41a2 --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/database/JavaDatabaseTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2016-2019 Á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.database + +import org.isoron.uhabits.BaseTest +import org.isoron.uhabits.utils.* +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals + +class JavaDatabaseTest : BaseTest() { + private lateinit var db: Database + + @Before + fun setup() { + val dbFile = fileOpener.openUserFile("test.sqlite3") + if (dbFile.exists()) dbFile.delete() + db = databaseOpener.open(dbFile) + } + + @Test + fun testUsage() { + db.setVersion(0) + assertEquals(db.getVersion(), 0) + + db.setVersion(23) + assertEquals(db.getVersion(), 23) + + var stmt = db.prepareStatement("drop table if exists demo") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement("create table if not exists demo(key int, value text)") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement("insert into demo(key, value) values (?1, ?2)") + stmt.bindInt(1, 42) + stmt.bindText(2, "Hello World") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement("select * from demo where key > ?1") + stmt.bindInt(1, 10) + + var result = stmt.step() + assertEquals(result, StepResult.ROW) + assertEquals(stmt.getInt(0), 42) + assertEquals(stmt.getText(1), "Hello World") + + result = stmt.step() + assertEquals(result, StepResult.DONE) + + stmt.finalize() + db.close() + } + + @Test + fun testMigrateTo() { + assertEquals(0, db.getVersion()) + db.migrateTo(22, fileOpener) + assertEquals(22, db.getVersion()) + db.execute("select * from habits") + } +} \ No newline at end of file diff --git a/core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaFilesTest.kt b/core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaFilesTest.kt new file mode 100644 index 000000000..e0b712a90 --- /dev/null +++ b/core/src/jvmTest/kotlin/org/isoron/uhabits/utils/JavaFilesTest.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016-2019 Á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.utils + +import org.isoron.uhabits.BaseTest +import org.junit.Test +import kotlin.test.assertEquals + +class JavaFilesTest : BaseTest() { + @Test + fun testReadLines() { + val hello = fileOpener.openResourceFile("hello.txt") + var lines = hello.readLines() + assertEquals("Hello World!", lines[0]) + assertEquals("This is a resource.", lines[1]) + + val migration = fileOpener.openResourceFile("migrations/012.sql") + lines = migration.readLines() + assertEquals("delete from Score", lines[0]) + } +} \ No newline at end of file diff --git a/ios/CoreModule.swift b/ios/CoreModule.swift deleted file mode 100644 index 50c0b5022..000000000 --- a/ios/CoreModule.swift +++ /dev/null @@ -1,49 +0,0 @@ -import Foundation - -@objc(CoreModule) -class CoreModule: RCTEventEmitter { - - @objc - open override func supportedEvents() -> [String] { - return ["onHabitList"] - } - - @objc - func requestHabitList() { - DispatchQueue.main.async { - let habits = AppDelegate.backend.getHabitList() - let result = habits.map { - ["key": String($0.key.intValue), - "name": $0.value["name"], - "color": $0.value["color"]] - } - self.sendEvent(withName: "onHabitList", body: result) - } - } - - @objc - func createHabit(_ name: String) { - DispatchQueue.main.async { - AppDelegate.backend.createHabit(name: name) - } - } - - @objc - func deleteHabit(_ id: Int32) { - DispatchQueue.main.async { - AppDelegate.backend.deleteHabit(id: id) - } - } - - @objc - func updateHabit(_ id: Int32, _ name: String) { - DispatchQueue.main.async { - AppDelegate.backend.updateHabit(id: id, name: name) - } - } - - @objc - override static func requiresMainQueueSetup() -> Bool { - return true - } -} diff --git a/ios/uhabits.xcodeproj/project.pbxproj b/ios/uhabits.xcodeproj/project.pbxproj index fb56bb101..79b22d8d6 100644 --- a/ios/uhabits.xcodeproj/project.pbxproj +++ b/ios/uhabits.xcodeproj/project.pbxproj @@ -15,6 +15,13 @@ 000BCDF521F69E1400F4DA11 /* FontAwesome Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 000BCDF421F69E1400F4DA11 /* FontAwesome Regular.ttf */; }; 000BCE0521F6CB1100F4DA11 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 000BCDFE21F6CAFF00F4DA11 /* libRCTWebSocket.a */; }; 000C283821F51C9B00C5EC6D /* libRNSVG.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 000C283521F51C4E00C5EC6D /* libRNSVG.a */; }; + 0021019C21F8AA3E00F9283D /* IosDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0021019B21F8AA3E00F9283D /* IosDatabase.swift */; }; + 002101A421F936A300F9283D /* IosSqlDatabaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */; }; + 002101AC21F9428C00F9283D /* core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0008A5C021F16D25000DB3E7 /* core.framework */; }; + 0091878521FD70B5001BDE6B /* IosLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0091878421FD70B5001BDE6B /* IosLog.swift */; }; + 00B2AC3D21FCA9D900CBEC8E /* IosFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */; }; + 00B2AC6521FD1A4500CBEC8E /* migrations in Resources */ = {isa = PBXBuildFile; fileRef = 00B2AC6421FD1A4500CBEC8E /* migrations */; }; + 00B2AC6821FD1DA700CBEC8E /* IosFilesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */; }; 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; @@ -72,6 +79,13 @@ remoteGlobalIDString = 94DDAC5C1F3D024300EED511; remoteInfo = "RNSVG-tvOS"; }; + 002101A621F936A300F9283D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = uhabits; + }; 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; @@ -316,11 +330,19 @@ 0008A5C021F16D25000DB3E7 /* core.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = core.framework; path = ../core/build/bin/iOS/main/debug/framework/core.framework; sourceTree = ""; }; 0008A5F521F17513000DB3E7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = uhabits/AppDelegate.swift; sourceTree = ""; }; 0008A5F721F17531000DB3E7 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BridgingHeader.h; path = uhabits/BridgingHeader.h; sourceTree = ""; }; - 0008A62921F2B728000DB3E7 /* CoreModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreModule.swift; sourceTree = ""; }; + 0008A62921F2B728000DB3E7 /* CoreModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CoreModule.swift; path = uhabits/CoreModule.swift; sourceTree = ""; }; 0008A62B21F2B755000DB3E7 /* CoreModuleBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = CoreModuleBridge.m; path = uhabits/CoreModuleBridge.m; sourceTree = ""; }; 000BCDF421F69E1400F4DA11 /* FontAwesome Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "FontAwesome Regular.ttf"; path = "../react-native/res/fonts/FontAwesome Regular.ttf"; sourceTree = ""; }; 000BCDF621F6CAFF00F4DA11 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../react-native/node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; 000C280A21F51C4E00C5EC6D /* RNSVG.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNSVG.xcodeproj; path = "../react-native/node_modules/react-native-svg/ios/RNSVG.xcodeproj"; sourceTree = ""; }; + 0021019B21F8AA3E00F9283D /* IosDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosDatabase.swift; path = uhabits/IosDatabase.swift; sourceTree = ""; }; + 002101A121F936A300F9283D /* uhabitsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = uhabitsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosSqlDatabaseTest.swift; sourceTree = ""; }; + 002101A521F936A300F9283D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0091878421FD70B5001BDE6B /* IosLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosLog.swift; path = uhabits/IosLog.swift; sourceTree = ""; }; + 00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosFiles.swift; path = uhabits/IosFiles.swift; sourceTree = ""; }; + 00B2AC6421FD1A4500CBEC8E /* migrations */ = {isa = PBXFileReference; lastKnownFileType = folder; name = migrations; path = ../core/assets/main/migrations; sourceTree = ""; }; + 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosFilesTest.swift; sourceTree = ""; }; 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; }; 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = "../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj"; sourceTree = ""; }; 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = ""; }; @@ -339,6 +361,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 0021019E21F936A300F9283D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 002101AC21F9428C00F9283D /* core.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -365,6 +395,7 @@ 000BCDCC21F69E0000F4DA11 /* Assets */ = { isa = PBXGroup; children = ( + 00B2AC6421FD1A4500CBEC8E /* migrations */, 000BCDF421F69E1400F4DA11 /* FontAwesome Regular.ttf */, ); name = Assets; @@ -390,6 +421,17 @@ name = Products; sourceTree = ""; }; + 002101A221F936A300F9283D /* Unit Tests */ = { + isa = PBXGroup; + children = ( + 002101A521F936A300F9283D /* Info.plist */, + 002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */, + 00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */, + ); + name = "Unit Tests"; + path = uhabitsTest; + sourceTree = ""; + }; 00C302A81ABCB8CE00DB3ED1 /* Products */ = { isa = PBXGroup; children = ( @@ -437,12 +479,15 @@ isa = PBXGroup; children = ( 13B07FB61A68108700A75B9A /* Info.plist */, - 0008A5F521F17513000DB3E7 /* AppDelegate.swift */, - 0008A5F721F17531000DB3E7 /* BridgingHeader.h */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, - 0008A62921F2B728000DB3E7 /* CoreModule.swift */, + 0008A5F721F17531000DB3E7 /* BridgingHeader.h */, 0008A62B21F2B755000DB3E7 /* CoreModuleBridge.m */, + 0008A5F521F17513000DB3E7 /* AppDelegate.swift */, + 0008A62921F2B728000DB3E7 /* CoreModule.swift */, + 0021019B21F8AA3E00F9283D /* IosDatabase.swift */, + 00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */, + 0091878421FD70B5001BDE6B /* IosLog.swift */, ); name = Application; sourceTree = ""; @@ -530,6 +575,7 @@ children = ( 000BCDCC21F69E0000F4DA11 /* Assets */, 13B07FAE1A68108700A75B9A /* Application */, + 002101A221F936A300F9283D /* Unit Tests */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 83CBBA001A601CBA00E9B192 /* Products */, 2D16E6871FA4F8E400B85C8A /* Frameworks */, @@ -543,6 +589,7 @@ isa = PBXGroup; children = ( 13B07F961A680F5B00A75B9A /* uhabits.app */, + 002101A121F936A300F9283D /* uhabitsTests.xctest */, ); name = Products; sourceTree = ""; @@ -559,6 +606,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 002101A021F936A300F9283D /* uhabitsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 002101A821F936A300F9283D /* Build configuration list for PBXNativeTarget "uhabitsTests" */; + buildPhases = ( + 0021019D21F936A300F9283D /* Sources */, + 0021019E21F936A300F9283D /* Frameworks */, + 0021019F21F936A300F9283D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 002101A721F936A300F9283D /* PBXTargetDependency */, + ); + name = uhabitsTests; + productName = uhabitsTests; + productReference = 002101A121F936A300F9283D /* uhabitsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 13B07F861A680F5B00A75B9A /* uhabits */ = { isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "uhabits" */; @@ -584,9 +649,16 @@ 83CBB9F71A601CBA00E9B192 /* Project object */ = { isa = PBXProject; attributes = { + LastSwiftUpdateCheck = 1010; LastUpgradeCheck = 0940; - ORGANIZATIONNAME = Facebook; + ORGANIZATIONNAME = "Loop Habit Tracker"; TargetAttributes = { + 002101A021F936A300F9283D = { + CreatedOnToolsVersion = 10.1; + DevelopmentTeam = R5YTHGE3PS; + ProvisioningStyle = Automatic; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; 13B07F861A680F5B00A75B9A = { DevelopmentTeam = R5YTHGE3PS; LastSwiftMigration = 1010; @@ -657,6 +729,7 @@ projectRoot = ""; targets = ( 13B07F861A680F5B00A75B9A /* uhabits */, + 002101A021F936A300F9283D /* uhabitsTests */, ); }; /* End PBXProject section */ @@ -931,6 +1004,13 @@ /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ + 0021019F21F936A300F9283D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F8E1A680F5B00A75B9A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -938,6 +1018,7 @@ 000BCDF521F69E1400F4DA11 /* FontAwesome Regular.ttf in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, + 00B2AC6521FD1A4500CBEC8E /* migrations in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -956,16 +1037,28 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh"; + shellScript = "export NODE_BINARY=node\n../node_modules/react-native/scripts/react-native-xcode.sh\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 0021019D21F936A300F9283D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 002101A421F936A300F9283D /* IosSqlDatabaseTest.swift in Sources */, + 00B2AC6821FD1DA700CBEC8E /* IosFilesTest.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 13B07F871A680F5B00A75B9A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 00B2AC3D21FCA9D900CBEC8E /* IosFiles.swift in Sources */, 0008A62C21F2B755000DB3E7 /* CoreModuleBridge.m in Sources */, + 0091878521FD70B5001BDE6B /* IosLog.swift in Sources */, + 0021019C21F8AA3E00F9283D /* IosDatabase.swift in Sources */, 0008A62A21F2B728000DB3E7 /* CoreModule.swift in Sources */, 0008A5F621F17513000DB3E7 /* AppDelegate.swift in Sources */, ); @@ -973,6 +1066,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 002101A721F936A300F9283D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* uhabits */; + targetProxy = 002101A621F936A300F9283D /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { isa = PBXVariantGroup; @@ -986,6 +1087,71 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 002101A921F936A300F9283D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = R5YTHGE3PS; + FRAMEWORK_SEARCH_PATHS = "${PROJECT_DIR}/../core/build/bin/iOS/main/debug/framework/**"; + GCC_C_LANGUAGE_STANDARD = gnu11; + HEADER_SEARCH_PATHS = "${PROJECT_DIR}/../core/build/bin/iOS/main/debug/framework/core.framework/**"; + INFOPLIST_FILE = "$(PROJECT_DIR)/uhabitsTest/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.isoron.uhabitsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "${PROJECT_DIR}/uhabits/BridgingHeader.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/uhabits.app/uhabits"; + }; + name = Debug; + }; + 002101AA21F936A300F9283D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = R5YTHGE3PS; + FRAMEWORK_SEARCH_PATHS = "${PROJECT_DIR}/../core/build/bin/iOS/main/debug/framework/**"; + GCC_C_LANGUAGE_STANDARD = gnu11; + HEADER_SEARCH_PATHS = "${PROJECT_DIR}/../core/build/bin/iOS/main/debug/framework/core.framework/**"; + INFOPLIST_FILE = "$(PROJECT_DIR)/uhabitsTest/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = org.isoron.uhabitsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "${PROJECT_DIR}/uhabits/BridgingHeader.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/uhabits.app/uhabits"; + }; + name = Release; + }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1140,6 +1306,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 002101A821F936A300F9283D /* Build configuration list for PBXNativeTarget "uhabitsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 002101A921F936A300F9283D /* Debug */, + 002101AA21F936A300F9283D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "uhabits" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/ios/uhabits.xcodeproj/xcshareddata/xcschemes/uhabits.xcscheme b/ios/uhabits.xcodeproj/xcshareddata/xcschemes/uhabits.xcscheme index 41becf511..3307cea4a 100644 --- a/ios/uhabits.xcodeproj/xcshareddata/xcschemes/uhabits.xcscheme +++ b/ios/uhabits.xcodeproj/xcshareddata/xcschemes/uhabits.xcscheme @@ -40,8 +40,19 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + codeCoverageEnabled = "YES" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + + + + diff --git a/ios/uhabits/AppDelegate.swift b/ios/uhabits/AppDelegate.swift index 9cd807a41..3e78dfcb0 100644 --- a/ios/uhabits/AppDelegate.swift +++ b/ios/uhabits/AppDelegate.swift @@ -1,22 +1,35 @@ +/* + * Copyright (C) 2016-2019 Á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 . + */ + import Foundation +import SQLite3 @UIApplicationMain - class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? var bridge: RCTBridge! - static var backend = Backend() + + static var backend = Backend(databaseOpener: IosDatabaseOpener(), + fileOpener: IosFileOpener(), + log: IosLog()) func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - AppDelegate.backend.createHabit(name: "Wake up early") - AppDelegate.backend.createHabit(name: "Wash clothes") - AppDelegate.backend.createHabit(name: "Exercise") - AppDelegate.backend.createHabit(name: "Meditate") - AppDelegate.backend.createHabit(name: "Take vitamins") - AppDelegate.backend.createHabit(name: "Write 'the quick brown fox jumps over the lazy dog' daily") - AppDelegate.backend.createHabit(name: "Write journal") - AppDelegate.backend.createHabit(name: "Study French") - let jsCodeLocation = RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index.ios", fallbackResource: nil) let rootView = RCTRootView(bundleURL: jsCodeLocation, moduleName: "LoopHabitTracker", initialProperties: nil, launchOptions: launchOptions) rootView?.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) diff --git a/ios/uhabits/BridgingHeader.h b/ios/uhabits/BridgingHeader.h index de3d2446f..51e0608dc 100644 --- a/ios/uhabits/BridgingHeader.h +++ b/ios/uhabits/BridgingHeader.h @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2016-2019 Á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 . + */ + #import #import #import diff --git a/ios/uhabits/CoreModule.swift b/ios/uhabits/CoreModule.swift new file mode 100644 index 000000000..411dfe424 --- /dev/null +++ b/ios/uhabits/CoreModule.swift @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2016-2019 Á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 . + */ + +import Foundation + +extension String: Error {} + +@objc(CoreModule) +class CoreModule: RCTEventEmitter { + + func convert(_ obj: Any?) -> Any? { + if obj is KotlinInt { + return (obj as! KotlinInt).intValue + } + if obj is NSString { + return obj + } + if obj is Dictionary { + return (obj as! Dictionary).mapValues{ convert($0) } + } + if obj is Array { + return (obj as! Array).map { convert($0) } + } + return nil + } + + @objc + open override func supportedEvents() -> [String] { + return ["onHabitList"] + } + + @objc + func requestHabitList() { + DispatchQueue.main.async { + let result = AppDelegate.backend.getHabitList() + self.sendEvent(withName: "onHabitList", body: self.convert(result)) + } + } + + @objc + func createHabit(_ name: String) { + DispatchQueue.main.async { + AppDelegate.backend.createHabit(name: name) + } + } + + @objc + func deleteHabit(_ id: Int32) { + DispatchQueue.main.async { + AppDelegate.backend.deleteHabit(id: id) + } + } + + @objc + func updateHabit(_ id: Int32, _ name: String) { + DispatchQueue.main.async { + AppDelegate.backend.updateHabit(id: id, name: name) + } + } + + @objc + override static func requiresMainQueueSetup() -> Bool { + return true + } +} diff --git a/ios/uhabits/CoreModuleBridge.m b/ios/uhabits/CoreModuleBridge.m index 236cdba5f..ebbbc1078 100644 --- a/ios/uhabits/CoreModuleBridge.m +++ b/ios/uhabits/CoreModuleBridge.m @@ -1,3 +1,22 @@ +/* + * Copyright (C) 2016-2019 Á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 . + */ + #import #import #import diff --git a/ios/uhabits/IosDatabase.swift b/ios/uhabits/IosDatabase.swift new file mode 100644 index 000000000..6a9062877 --- /dev/null +++ b/ios/uhabits/IosDatabase.swift @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2016-2019 Á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 . + */ + +import Foundation +import SQLite3 + +internal let SQLITE_STATIC = unsafeBitCast(0, to: sqlite3_destructor_type.self) +internal let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self) + +class IosPreparedStatement : NSObject, PreparedStatement { + + var db: OpaquePointer + var statement: OpaquePointer + + init(withStatement statement: OpaquePointer, withDb db: OpaquePointer) { + self.statement = statement + self.db = db + } + + func step() -> StepResult { + let result = sqlite3_step(statement) + if result == SQLITE_ROW { + return StepResult.row + } else if result == SQLITE_DONE { + return StepResult.done + } else { + let errMsg = String(cString: sqlite3_errmsg(db)!) + fatalError("Database error: \(errMsg) (\(result))") + } + } + + func getInt(index: Int32) -> Int32 { + return sqlite3_column_int(statement, index) + } + + func getText(index: Int32) -> String { + return String(cString: sqlite3_column_text(statement, index)) + } + + func bindInt(index: Int32, value: Int32) { + sqlite3_bind_int(statement, index, value) + } + + func bindText(index: Int32, value: String) { + sqlite3_bind_text(statement, index, value, -1, SQLITE_TRANSIENT) + } + + func reset() { + sqlite3_reset(statement) + } + + override func finalize() { + sqlite3_finalize(statement) + } +} + +class IosDatabase : NSObject, Database { + var db: OpaquePointer + + init(withDb db: OpaquePointer) { + self.db = db + } + + func prepareStatement(sql: String) -> PreparedStatement { + if sql.isEmpty { + fatalError("Provided SQL query is empty") + } + print("Running SQL: \(sql)") + var statement : OpaquePointer? + let result = sqlite3_prepare_v2(db, sql, -1, &statement, nil) + if result == SQLITE_OK { + return IosPreparedStatement(withStatement: statement!, withDb: db) + } else { + let errMsg = String(cString: sqlite3_errmsg(db)!) + fatalError("Database error: \(errMsg)") + } + } + + func close() { + sqlite3_close(db) + } +} + +class IosDatabaseOpener : NSObject, DatabaseOpener { + func open(file: UserFile) -> Database { + let dbPath = (file as! IosUserFile).path + + let version = String(cString: sqlite3_libversion()) + print("SQLite \(version)") + print("Opening database: \(dbPath)") + var db: OpaquePointer? + let result = sqlite3_open(dbPath, &db) + if result == SQLITE_OK { + return IosDatabase(withDb: db!) + } else { + fatalError("Error opening database (code \(result))") + } + } +} diff --git a/ios/uhabits/IosFiles.swift b/ios/uhabits/IosFiles.swift new file mode 100644 index 000000000..5ed33e78b --- /dev/null +++ b/ios/uhabits/IosFiles.swift @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016-2019 Á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 . + */ + +import Foundation + +class IosResourceFile : NSObject, ResourceFile { + + var path: String + + var fileManager = FileManager.default + + init(forPath path: String) { + self.path = path + } + + func readLines() -> [String] { + do { + let contents = try String(contentsOfFile: self.path, encoding: .utf8) + return contents.components(separatedBy: CharacterSet.newlines) + } catch { + return [""] + } + } +} + +class IosUserFile : NSObject, UserFile { + + var path: String + + init(forPath path: String) { + self.path = path + } + + func delete() { + do { + try FileManager.default.removeItem(atPath: path) + } catch { + + } + } + + func exists() -> Bool { + return FileManager.default.fileExists(atPath: path) + } +} + +class IosFileOpener : NSObject, FileOpener { + func openResourceFile(filename: String) -> ResourceFile { + let path = "\(Bundle.main.resourcePath!)/\(filename)" + return IosResourceFile(forPath: path) + } + + func openUserFile(filename: String) -> UserFile { + do { + let root = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path + return IosUserFile(forPath: "\(root)/\(filename)") + } catch { + return IosUserFile(forPath: "invalid") + } + } +} diff --git a/ios/uhabits/IosLog.swift b/ios/uhabits/IosLog.swift new file mode 100644 index 000000000..94f7fea0d --- /dev/null +++ b/ios/uhabits/IosLog.swift @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2016-2019 Á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 . + */ + +import Foundation + +class IosLog : NSObject, Log { + func info(msg: String) { + print("[I] \(msg)") + } + + func debug(msg: String) { + print("[D] \(msg)") + } +} diff --git a/ios/uhabitsTest/Info.plist b/ios/uhabitsTest/Info.plist new file mode 100644 index 000000000..6c40a6cd0 --- /dev/null +++ b/ios/uhabitsTest/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/ios/uhabitsTest/IosFilesTest.swift b/ios/uhabitsTest/IosFilesTest.swift new file mode 100644 index 000000000..55b9a3fa4 --- /dev/null +++ b/ios/uhabitsTest/IosFilesTest.swift @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016-2019 Á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 . + */ + +import XCTest +@testable import uhabits + +class IosFilesTest: XCTestCase { + func testResourceFiles() { + let fileOpener = IosFileOpener() + let file = fileOpener.openResourceFile(filename: "migrations/010.sql") + let lines = file.readLines() + XCTAssertEqual(lines[0], "delete from Score") + } + + func testUserFiles() throws { + let fm = FileManager.default + let root = try fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path + let path = "\(root)/test.txt" + fm.createFile(atPath: path, contents: "Hello world\nThis is line 2".data(using: .utf8), attributes: nil) + + let fileOpener = IosFileOpener() + let file = fileOpener.openUserFile(filename: "test.txt") + XCTAssertTrue(file.exists()) + + file.delete() + XCTAssertFalse(file.exists()) + } +} diff --git a/ios/uhabitsTest/IosSqlDatabaseTest.swift b/ios/uhabitsTest/IosSqlDatabaseTest.swift new file mode 100644 index 000000000..0093dfeda --- /dev/null +++ b/ios/uhabitsTest/IosSqlDatabaseTest.swift @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2016-2019 Á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 . + */ + +import XCTest +@testable import uhabits + +class IosDatabaseTest: XCTestCase { + func testUsage() { + let databaseOpener = IosDatabaseOpener() + let fileOpener = IosFileOpener() + + let dbFile = fileOpener.openUserFile(filename: "test.sqlite3") + if dbFile.exists() { + dbFile.delete() + } + let db = databaseOpener.open(file: dbFile) + + var stmt = db.prepareStatement(sql: "drop table if exists demo") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement(sql: "begin") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement(sql: "create table if not exists demo(key int, value text)") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement(sql: "insert into demo(key, value) values (?1, ?2)") + stmt.bindInt(index: 1, value: 42) + stmt.bindText(index: 2, value: "Hello World") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement(sql: "select * from demo where key > ?1") + stmt.bindInt(index: 1, value: 10) + var result = stmt.step() + XCTAssertEqual(result, StepResult.row) + XCTAssertEqual(stmt.getInt(index: 0), 42) + XCTAssertEqual(stmt.getText(index: 1), "Hello World") + result = stmt.step() + XCTAssertEqual(result, StepResult.done) + stmt.finalize() + + stmt = db.prepareStatement(sql: "drop table demo") + stmt.step() + stmt.finalize() + + stmt = db.prepareStatement(sql: "commit") + stmt.step() + stmt.finalize() + + db.close() + dbFile.delete() + } +} diff --git a/react-native/src/components/ListHabits/HabitList.js b/react-native/src/components/ListHabits/HabitList.js index 81316a22a..78b8a569a 100644 --- a/react-native/src/components/ListHabits/HabitList.js +++ b/react-native/src/components/ListHabits/HabitList.js @@ -98,7 +98,6 @@ export default class HabitList extends React.Component { - )} /> diff --git a/react-native/src/components/ListHabits/HabitListHeader.js b/react-native/src/components/ListHabits/HabitListHeader.js index 3144e8956..39b680d9a 100644 --- a/react-native/src/components/ListHabits/HabitListHeader.js +++ b/react-native/src/components/ListHabits/HabitListHeader.js @@ -69,10 +69,6 @@ export default class HabitListHeader extends React.Component { static renderColumns() { return [ - { - dayName: 'Sun', - dayNumber: '6', - }, { dayName: 'Sat', dayNumber: '5',