Implement database access (with migrations)

pull/498/head
Alinson S. Xavier 7 years ago
parent e19339d808
commit 7cab0a39e5

@ -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'

@ -1,3 +1,22 @@
#
# Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip

@ -0,0 +1,72 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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()
}
}

@ -0,0 +1,46 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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])
}
}

@ -0,0 +1,28 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.habits
import android.support.test.*
open class BaseTest {
val context = InstrumentationRegistry.getTargetContext()
val fileOpener = AndroidFileOpener(context)
val databaseOpener = AndroidDatabaseOpener()
}

@ -0,0 +1 @@
../../../core/assets/main/

@ -0,0 +1,33 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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())
}
}

@ -0,0 +1,62 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String> {
val asset = context.assets.open(filename)
val reader = BufferedReader(InputStreamReader(asset))
val lines = ArrayList<String>()
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()
}
}

@ -0,0 +1,32 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

@ -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

@ -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 )

@ -0,0 +1,3 @@
delete from Score
delete from Streak
delete from Checkmarks

@ -0,0 +1 @@
alter table Habits add column reminder_days integer not null default 127

@ -0,0 +1,3 @@
delete from Score
delete from Streak
delete from Checkmarks

@ -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)

@ -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

@ -0,0 +1,3 @@
delete from Score
delete from Streak
delete from Checkmarks

@ -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

@ -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

@ -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 ""

@ -0,0 +1 @@
create table Events ( id integer primary key autoincrement, timestamp integer, message text, server_id integer )

@ -0,0 +1,3 @@
drop table checkmarks
drop table streak
drop table score

@ -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

@ -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

@ -0,0 +1,2 @@
Hello World!
This is a resource.

@ -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
}
}
}

@ -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<Habit>()
fun getHabitList(): Map<Int, Map<String, *>> {
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<Map<String, *>> {
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
}
}

@ -0,0 +1,22 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits
const val LOOP_DATABASE_VERSION = 22

@ -0,0 +1,57 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<Habit>()
init {
loadHabitsFromDatabase()
}
fun getActive(): List<Habit> {
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()
}
}

@ -0,0 +1,88 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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()
}

@ -0,0 +1,60 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String>
}
/**
* 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
}

@ -0,0 +1,25 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
interface Log {
fun info(msg: String)
fun debug(msg: String)
}

@ -0,0 +1,22 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
expect fun sprintf(format: String, vararg args: Any?): String

@ -17,33 +17,14 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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))
}
}

@ -0,0 +1,30 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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()
}

@ -0,0 +1,87 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

@ -0,0 +1,59 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String> {
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")
}
}

@ -0,0 +1,30 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
class JavaLog : Log {
override fun info(msg: String) {
println("[I] $msg")
}
override fun debug(msg: String) {
println("[D] $msg")
}
}

@ -0,0 +1,24 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.utils
actual fun sprintf(format: String, vararg args: Any?): String {
return String.format(format, *args)
}

@ -0,0 +1,48 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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")
}
}

@ -0,0 +1,29 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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())
}

@ -0,0 +1,82 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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")
}
}

@ -0,0 +1,38 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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])
}
}

@ -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
}
}

@ -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 = "<group>"; };
0008A5F521F17513000DB3E7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = uhabits/AppDelegate.swift; sourceTree = "<group>"; };
0008A5F721F17531000DB3E7 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BridgingHeader.h; path = uhabits/BridgingHeader.h; sourceTree = "<group>"; };
0008A62921F2B728000DB3E7 /* CoreModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreModule.swift; sourceTree = "<group>"; };
0008A62921F2B728000DB3E7 /* CoreModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CoreModule.swift; path = uhabits/CoreModule.swift; sourceTree = "<group>"; };
0008A62B21F2B755000DB3E7 /* CoreModuleBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = CoreModuleBridge.m; path = uhabits/CoreModuleBridge.m; sourceTree = "<group>"; };
000BCDF421F69E1400F4DA11 /* FontAwesome Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "FontAwesome Regular.ttf"; path = "../react-native/res/fonts/FontAwesome Regular.ttf"; sourceTree = "<group>"; };
000BCDF621F6CAFF00F4DA11 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../react-native/node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = "<group>"; };
000C280A21F51C4E00C5EC6D /* RNSVG.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNSVG.xcodeproj; path = "../react-native/node_modules/react-native-svg/ios/RNSVG.xcodeproj"; sourceTree = "<group>"; };
0021019B21F8AA3E00F9283D /* IosDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosDatabase.swift; path = uhabits/IosDatabase.swift; sourceTree = "<group>"; };
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 = "<group>"; };
002101A521F936A300F9283D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0091878421FD70B5001BDE6B /* IosLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosLog.swift; path = uhabits/IosLog.swift; sourceTree = "<group>"; };
00B2AC3C21FCA9D900CBEC8E /* IosFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = IosFiles.swift; path = uhabits/IosFiles.swift; sourceTree = "<group>"; };
00B2AC6421FD1A4500CBEC8E /* migrations */ = {isa = PBXFileReference; lastKnownFileType = folder; name = migrations; path = ../core/assets/main/migrations; sourceTree = "<group>"; };
00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosFilesTest.swift; sourceTree = "<group>"; };
00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = "<group>"; };
00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = "../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj"; sourceTree = "<group>"; };
00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = "<group>"; };
@ -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 = "<group>";
};
002101A221F936A300F9283D /* Unit Tests */ = {
isa = PBXGroup;
children = (
002101A521F936A300F9283D /* Info.plist */,
002101A321F936A300F9283D /* IosSqlDatabaseTest.swift */,
00B2AC6621FD1CEF00CBEC8E /* IosFilesTest.swift */,
);
name = "Unit Tests";
path = uhabitsTest;
sourceTree = "<group>";
};
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 = "<group>";
@ -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 = "<group>";
@ -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 = (

@ -40,8 +40,19 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
codeCoverageEnabled = "YES"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "002101A021F936A300F9283D"
BuildableName = "uhabitsTests.xctest"
BlueprintName = "uhabitsTests"
ReferencedContainer = "container:uhabits.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
@ -75,6 +86,13 @@
ReferencedContainer = "container:uhabits.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<EnvironmentVariables>
<EnvironmentVariable
key = "OS_ACTIVITY_MODE"
value = "disable"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>

@ -1,22 +1,35 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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)

@ -1,3 +1,22 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTBridgeModule.h>

@ -0,0 +1,81 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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<String, Any> {
return (obj as! Dictionary<String, Any>).mapValues{ convert($0) }
}
if obj is Array<Any> {
return (obj as! Array<Any>).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
}
}

@ -1,3 +1,22 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@ -0,0 +1,115 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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))")
}
}
}

@ -0,0 +1,77 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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")
}
}
}

@ -0,0 +1,30 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
import Foundation
class IosLog : NSObject, Log {
func info(msg: String) {
print("[I] \(msg)")
}
func debug(msg: String) {
print("[D] \(msg)")
}
}

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

@ -0,0 +1,44 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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())
}
}

@ -0,0 +1,73 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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()
}
}

@ -98,7 +98,6 @@ export default class HabitList extends React.Component {
<CheckmarkButton color={Colors[item.color]} />
<CheckmarkButton color={Colors[item.color]} />
<CheckmarkButton color={Colors[item.color]} />
<CheckmarkButton color={Colors[item.color]} />
</View>
)}
/>

@ -69,10 +69,6 @@ export default class HabitListHeader extends React.Component {
static renderColumns() {
return [
{
dayName: 'Sun',
dayNumber: '6',
},
{
dayName: 'Sat',
dayNumber: '5',

Loading…
Cancel
Save