Move uhabits-core to top level; all Java files to uhabits-core:jvmMain/jvmTest

This commit is contained in:
2021-01-03 13:21:02 -06:00
parent 1137088e20
commit 9fd36d8d53
225 changed files with 444 additions and 116 deletions

View File

@@ -10,7 +10,7 @@ KOTLIN_VERSION = 1.4.10
KX_COROUTINES_VERSION = 1.4.2
SUPPORT_LIBRARY_VERSION = 28.0.0
AUTO_FACTORY_VERSION = 1.0-beta6
BUILD_TOOLS_VERSION = 4.1.0
BUILD_TOOLS_VERSION = 4.0.0
KTOR_VERSION=1.4.2
ESPRESSO_VERSION=3.3.0

View File

@@ -1 +1 @@
include ':uhabits-android', ':uhabits-core-legacy', ':android-pickers', ':uhabits-core'
include ':uhabits-android', ':android-pickers'

View File

@@ -90,8 +90,6 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation project(':uhabits-core-legacy')
androidTestImplementation project(':uhabits-core')
compileOnly "javax.annotation:jsr250-api:1.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation "com.github.paolorotolo:appintro:3.4.0"
@@ -111,9 +109,8 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.zxing:core:3.4.1'
implementation 'com.opencsv:opencsv:3.10'
implementation project(':uhabits-core-legacy')
implementation project(':uhabits-core')
implementation project(":android-pickers")
implementation files("../../uhabits-core/build/libs/uhabits-core-jvm.jar")
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
kaptAndroidTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
testImplementation "com.google.dagger:dagger:$DAGGER_VERSION"

View File

@@ -1 +0,0 @@
/build

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 424 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,32 +0,0 @@
apply plugin: 'idea'
apply plugin: 'java'
apply plugin: 'kotlin'
dependencies {
annotationProcessor "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
annotationProcessor "com.google.dagger:dagger:$DAGGER_VERSION"
compileOnly "com.google.auto.factory:auto-factory:$AUTO_FACTORY_VERSION"
compileOnly "com.google.dagger:dagger:$DAGGER_VERSION"
compileOnly 'javax.annotation:jsr250-api:1.0'
compileOnly 'org.jetbrains:annotations:18.0.0'
implementation "com.google.guava:guava:30.0-jre"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$KOTLIN_VERSION"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:$KX_COROUTINES_VERSION"
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation 'commons-codec:commons-codec:1.15'
implementation 'org.apache.commons:commons-lang3:3.5'
implementation 'com.opencsv:opencsv:3.10'
implementation project(':uhabits-core')
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
testImplementation "org.jetbrains.kotlin:kotlin-test:$KOTLIN_VERSION"
testImplementation 'junit:junit:4.12'
testImplementation 'nl.jqno.equalsverifier:equalsverifier:2.4.8'
testImplementation 'org.apache.commons:commons-io:1.3.2'
testImplementation 'org.hamcrest:hamcrest-library:1.4-atlassian-1'
testImplementation 'org.mockito:mockito-core:2.28.2'
testImplementation 'org.xerial:sqlite-jdbc:3.18.0'
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"

View File

@@ -1,24 +0,0 @@
/*
* Copyright (C) 2016 Á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.core
import javax.inject.Scope
@Scope
annotation class AppScope

View File

@@ -1,23 +0,0 @@
/*
* Copyright (C) 2017 Á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.core
const val DATABASE_FILENAME = "uhabits.db"
const val DATABASE_VERSION = 24

View File

@@ -1,32 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.commands
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
data class ArchiveHabitsCommand(
val habitList: HabitList,
val selected: List<Habit>,
) : Command {
override fun run() {
for (h in selected) h.isArchived = true
habitList.update(selected)
}
}

View File

@@ -1,34 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.commands
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.PaletteColor
data class ChangeHabitColorCommand(
val habitList: HabitList,
val selected: List<Habit>,
val newColor: PaletteColor,
) : Command {
override fun run() {
for (h in selected) h.color = newColor
habitList.update(selected)
}
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.commands
interface Command {
fun run()
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.commands
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.tasks.Task
import org.isoron.uhabits.core.tasks.TaskRunner
import java.util.LinkedList
import javax.inject.Inject
@AppScope
open class CommandRunner
@Inject constructor(
private val taskRunner: TaskRunner,
) {
private val listeners: LinkedList<Listener> = LinkedList()
open fun run(command: Command) {
taskRunner.execute(
object : Task {
override fun doInBackground() {
command.run()
}
override fun onPostExecute() {
notifyListeners(command)
}
}
)
}
fun addListener(l: Listener) {
listeners.add(l)
}
fun notifyListeners(command: Command) {
for (l in listeners) l.onCommandFinished(command)
}
fun removeListener(l: Listener) {
listeners.remove(l)
}
interface Listener {
fun onCommandFinished(command: Command)
}
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.commands
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.ModelFactory
data class CreateHabitCommand(
val modelFactory: ModelFactory,
val habitList: HabitList,
val model: Habit,
) : Command {
override fun run() {
val habit = modelFactory.buildHabit()
habit.copyFrom(model)
habitList.add(habit)
}
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.commands
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.Timestamp
data class CreateRepetitionCommand(
val habitList: HabitList,
val habit: Habit,
val timestamp: Timestamp,
val value: Int,
) : Command {
override fun run() {
val entries = habit.originalEntries
entries.add(Entry(timestamp, value))
habit.recompute()
habitList.resort()
}
}

View File

@@ -1,31 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.commands
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
data class DeleteHabitsCommand(
val habitList: HabitList,
val selected: List<Habit>,
) : Command {
override fun run() {
for (h in selected) habitList.remove(h)
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.commands
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitNotFoundException
data class EditHabitCommand(
val habitList: HabitList,
val habitId: Long,
val modified: Habit,
) : Command {
override fun run() {
val habit = habitList.getById(habitId) ?: throw HabitNotFoundException()
habit.copyFrom(modified)
habitList.update(habit)
habit.observable.notifyListeners()
habit.recompute()
}
}

View File

@@ -1,32 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.commands
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
data class UnarchiveHabitsCommand(
val habitList: HabitList,
val selected: List<Habit>,
) : Command {
override fun run() {
for (h in selected) h.isArchived = false
habitList.update(selected)
}
}

View File

@@ -1,22 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
@Retention(AnnotationRetention.RUNTIME)
annotation class Column(val name: String = "")

View File

@@ -1,62 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
import java.io.Closeable
interface Cursor : Closeable {
override fun close()
/**
* Moves the cursor forward one row from its current position. Returns
* true if the current position is valid, or false if the cursor is already
* past the last row. The cursor start at position -1, so this method must
* be called first.
*/
fun moveToNext(): Boolean
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as an Integer. If the value is null, returns null. The first
* column has index zero.
*/
fun getInt(index: Int): Int?
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as a Long. If the value is null, returns null. The first
* column has index zero.
*/
fun getLong(index: Int): Long?
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as a Double. If the value is null, returns null. The first
* column has index zero.
*/
fun getDouble(index: Int): Double?
/**
* Retrieves the value of the designated column in the current row of this
* Cursor as a String. If the value is null, returns null. The first
* column has index zero.
*/
fun getString(index: Int): String?
}

View File

@@ -1,62 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
import java.io.File
interface Database {
fun query(q: String, vararg params: String): Cursor
fun query(q: String, callback: ProcessCallback) {
query(q).use { c ->
c.moveToNext()
callback.process(c)
}
}
fun update(
tableName: String,
values: Map<String, Any?>,
where: String,
vararg params: String,
): Int
fun insert(tableName: String, values: Map<String, Any?>): Long?
fun delete(tableName: String, where: String, vararg params: String)
fun execute(query: String, vararg params: Any)
fun beginTransaction()
fun setTransactionSuccessful()
fun endTransaction()
fun close()
val version: Int
val file: File?
interface ProcessCallback {
fun process(cursor: Cursor)
}
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
import java.io.File
interface DatabaseOpener {
fun open(file: File): Database
}

View File

@@ -1,78 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
import java.sql.ResultSet
import java.sql.SQLException
class JdbcCursor(private val resultSet: ResultSet) : Cursor {
override fun close() {
try {
resultSet.close()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun moveToNext(): Boolean {
return try {
resultSet.next()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun getInt(index: Int): Int? {
return try {
val value = resultSet.getInt(index + 1)
if (resultSet.wasNull()) null else value
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun getLong(index: Int): Long? {
return try {
val value = resultSet.getLong(index + 1)
if (resultSet.wasNull()) null else value
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun getDouble(index: Int): Double? {
return try {
val value = resultSet.getDouble(index + 1)
if (resultSet.wasNull()) null else value
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun getString(index: Int): String? {
return try {
val value = resultSet.getString(index + 1)
if (resultSet.wasNull()) null else value
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
}

View File

@@ -1,166 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
import org.apache.commons.lang3.StringUtils
import java.io.File
import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.SQLException
import java.sql.Types
import java.util.ArrayList
class JdbcDatabase(private val connection: Connection) : Database {
private var transactionSuccessful = false
override fun query(q: String, vararg params: String): Cursor {
return try {
val st = buildStatement(q, params)
JdbcCursor(st.executeQuery())
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun update(
tableName: String,
values: Map<String, Any?>,
where: String,
vararg params: String,
): Int {
return try {
val fields = ArrayList<String?>()
val valuesStr = ArrayList<String>()
for ((key, value) in values) {
fields.add("$key=?")
valuesStr.add(value.toString())
}
valuesStr.addAll(listOf(*params))
val query = String.format(
"update %s set %s where %s",
tableName,
StringUtils.join(fields, ", "),
where
)
val st = buildStatement(query, valuesStr.toTypedArray())
st.executeUpdate()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun insert(tableName: String, values: Map<String, Any?>): Long? {
return try {
val fields = ArrayList<String?>()
val params = ArrayList<Any?>()
val questionMarks = ArrayList<String?>()
for ((key, value) in values) {
fields.add(key)
params.add(value)
questionMarks.add("?")
}
val query = String.format(
"insert into %s(%s) values(%s)",
tableName,
StringUtils.join(fields, ", "),
StringUtils.join(questionMarks, ", ")
)
val st = buildStatement(query, params.toTypedArray())
st.execute()
var id: Long? = null
val keys = st.generatedKeys
if (keys.next()) id = keys.getLong(1)
id
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun delete(tableName: String, where: String, vararg params: String) {
val query = String.format("delete from %s where %s", tableName, where)
execute(query, *params)
}
override fun execute(query: String, vararg params: Any) {
try {
buildStatement(query, params).execute()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
private fun buildStatement(query: String, params: Array<out Any?>): PreparedStatement {
val st = connection.prepareStatement(query)
var index = 1
for (param in params) {
when (param) {
null -> st.setNull(index++, Types.INTEGER)
is Int -> st.setInt(index++, param)
is Double -> st.setDouble(index++, param)
is String -> st.setString(index++, param)
is Long -> st.setLong(index++, param)
else -> throw IllegalArgumentException()
}
}
return st
}
@Synchronized
override fun beginTransaction() {
try {
connection.autoCommit = false
transactionSuccessful = false
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
@Synchronized
override fun setTransactionSuccessful() {
transactionSuccessful = true
}
@Synchronized
override fun endTransaction() {
try {
if (transactionSuccessful) connection.commit() else connection.rollback()
connection.autoCommit = true
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override fun close() {
try {
connection.close()
} catch (e: SQLException) {
throw RuntimeException(e)
}
}
override val version: Int
get() {
query("PRAGMA user_version").use { c ->
c.moveToNext()
return c.getInt(0)!!
}
}
override val file: File?
get() = null
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.util.Locale
class MigrationHelper(
private val db: Database,
) {
fun migrateTo(newVersion: Int) {
try {
for (v in db.version + 1..newVersion) {
val fname = String.format(Locale.US, "/migrations/%02d.sql", v)
for (command in SQLParser.parse(open(fname))) db.execute(command)
}
} catch (e: Exception) {
throw RuntimeException(e)
}
}
private fun open(fname: String): InputStream {
val resource = javaClass.getResourceAsStream(fname)
if (resource != null) return resource
// Workaround for bug in Android Studio / IntelliJ. Removing this
// causes unit tests to fail when run from within the IDE, although
// everything works fine from the command line.
val file = File("uhabits-core/src/main/resources/$fname")
if (file.exists()) return FileInputStream(file)
throw RuntimeException("resource not found: $fname")
}
}

View File

@@ -1,255 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
import org.apache.commons.lang3.StringUtils
import org.apache.commons.lang3.tuple.ImmutablePair
import org.apache.commons.lang3.tuple.Pair
import java.lang.reflect.Field
import java.util.ArrayList
import java.util.HashMap
import java.util.LinkedList
class Repository<T>(
private val klass: Class<T>,
private val db: Database,
) {
/**
* Returns the record that has the id provided. If no record is found, returns null.
*/
fun find(id: Long): T? {
return findFirst(String.format("where %s=?", getIdName()), id.toString())
}
/**
* Returns all records matching the given SQL query.
*
* The query should only contain the "where" part of the SQL query, and optionally the "order
* by" part. "Group by" is not allowed. If no matching records are found, returns an empty list.
*/
fun findAll(query: String, vararg params: String): List<T> {
db.query(buildSelectQuery() + query, *params).use { c -> return cursorToMultipleRecords(c) }
}
/**
* Returns the first record matching the given SQL query. See findAll for more details about
* the parameters.
*/
fun findFirst(query: String, vararg params: String): T? {
db.query(buildSelectQuery() + query, *params).use { c ->
return if (!c.moveToNext()) null else cursorToSingleRecord(c)
}
}
/**
* Executes the given SQL query on the repository.
*
* The query can be of any kind. For example, complex deletes and updates are allowed. The
* repository does not perform any checks to guarantee that the query is valid, however the
* underlying database might.
*/
fun execSQL(query: String, vararg params: Any) {
db.execute(query, *params)
}
/**
* Executes the given callback inside a database transaction.
*
* If the callback terminates without throwing any exceptions, the transaction is considered
* successful. If any exceptions are thrown, the transaction is aborted. Nesting transactions
* is not allowed.
*/
fun executeAsTransaction(callback: Runnable) {
db.beginTransaction()
try {
callback.run()
db.setTransactionSuccessful()
} catch (e: Exception) {
throw RuntimeException(e)
} finally {
db.endTransaction()
}
}
/**
* Saves the record on the database.
*
* If the id of the given record is null, it is assumed that the record has not been inserted
* in the repository yet. The record will be inserted, a new id will be automatically generated,
* and the id of the given record will be updated.
*
* If the given record has a non-null id, then an update will be performed instead. That is,
* the previous record will be overwritten by the one provided.
*/
fun save(record: T) {
try {
val fields = getFields()
val columns = getColumnNames()
val values: MutableMap<String, Any?> = HashMap()
for (i in fields.indices) values[columns[i]] = fields[i][record]
var id = getIdField()[record] as Long?
var affectedRows = 0
if (id != null) {
affectedRows = db.update(getTableName(), values, "${getIdName()}=?", id.toString())
}
if (id == null || affectedRows == 0) {
id = db.insert(getTableName(), values)
getIdField()[record] = id
}
} catch (e: Exception) {
throw RuntimeException(e)
}
}
/**
* Removes the given record from the repository. The id of the given record is also set to null.
*/
fun remove(record: T) {
try {
val id = getIdField()[record] as Long?
db.delete(getTableName(), "${getIdName()}=?", id.toString())
getIdField()[record] = null
} catch (e: Exception) {
throw RuntimeException(e)
}
}
private fun cursorToMultipleRecords(c: Cursor): List<T> {
val records: MutableList<T> = LinkedList()
while (c.moveToNext()) records.add(cursorToSingleRecord(c))
return records
}
@Suppress("UNCHECKED_CAST")
private fun cursorToSingleRecord(cursor: Cursor): T {
return try {
val constructor = klass.declaredConstructors[0]
constructor.isAccessible = true
val record = constructor.newInstance() as T
var index = 0
for (field in getFields()) copyFieldFromCursor(record, field, cursor, index++)
record
} catch (e: Exception) {
throw RuntimeException(e)
}
}
private fun copyFieldFromCursor(record: T, field: Field, c: Cursor, index: Int) {
when {
field.type.isAssignableFrom(java.lang.Integer::class.java) -> field[record] = c.getInt(index)
field.type.isAssignableFrom(java.lang.Long::class.java) -> field[record] = c.getLong(index)
field.type.isAssignableFrom(java.lang.Double::class.java) -> field[record] = c.getDouble(index)
field.type.isAssignableFrom(java.lang.String::class.java) -> field[record] = c.getString(index)
else -> throw RuntimeException("Type not supported: ${field.type.name} ${field.name}")
}
}
private fun buildSelectQuery(): String {
return String.format("select %s from %s ", StringUtils.join(getColumnNames(), ", "), getTableName())
}
private val fieldColumnPairs: List<Pair<Field, Column>>
get() {
val fields: MutableList<Pair<Field, Column>> = ArrayList()
for (f in klass.declaredFields) {
for (annotation in f.annotations) {
if (annotation !is Column) continue
fields.add(ImmutablePair(f, annotation))
}
}
return fields
}
private var cacheFields: Array<Field>? = null
private fun getFields(): Array<Field> {
if (cacheFields == null) {
val fields: MutableList<Field> = ArrayList()
val columns = fieldColumnPairs
for (pair in columns) fields.add(pair.left)
cacheFields = fields.toTypedArray()
}
return cacheFields!!
}
private var cacheColumnNames: Array<String>? = null
private fun getColumnNames(): Array<String> {
if (cacheColumnNames == null) {
val names: MutableList<String> = ArrayList()
val columns = fieldColumnPairs
for (pair in columns) {
var cname = pair.right.name
if (cname.isEmpty()) cname = pair.left.name
if (names.contains(cname)) throw RuntimeException("duplicated column : $cname")
names.add(cname)
}
cacheColumnNames = names.toTypedArray()
}
return cacheColumnNames!!
}
private var cacheTableName: String? = null
private fun getTableName(): String {
if (cacheTableName == null) {
val name = getTableAnnotation().name
if (name.isEmpty()) throw RuntimeException("Table name is empty")
cacheTableName = name
}
return cacheTableName!!
}
private var cacheIdName: String? = null
private fun getIdName(): String {
if (cacheIdName == null) {
val id = getTableAnnotation().id
if (id.isEmpty()) throw RuntimeException("Table id is empty")
cacheIdName = id
}
return cacheIdName!!
}
private var cacheIdField: Field? = null
private fun getIdField(): Field {
if (cacheIdField == null) {
val fields = getFields()
val idName = getIdName()
for (f in fields) if (f.name == idName) {
cacheIdField = f
break
}
if (cacheIdField == null) throw RuntimeException("Field not found: $idName")
}
return cacheIdField!!
}
private fun getTableAnnotation(): Table {
var t: Table? = null
for (annotation in klass.annotations) {
if (annotation !is Table) continue
t = annotation
break
}
if (t == null) throw RuntimeException("Table annotation not found")
return t
}
}

View File

@@ -1,131 +0,0 @@
/*
* Copyright (C) 2014 Markus Pfeiffer
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.isoron.uhabits.core.database
import java.io.BufferedInputStream
import java.io.InputStream
import java.util.ArrayList
internal class Tokenizer(
private val mStream: InputStream,
) {
private var mIsNext = false
private var mCurrent = 0
operator fun hasNext(): Boolean {
if (!mIsNext) {
mIsNext = true
mCurrent = mStream.read()
}
return mCurrent != -1
}
operator fun next(): Int {
if (!mIsNext) {
mCurrent = mStream.read()
}
mIsNext = false
return mCurrent
}
fun skip(s: String?): Boolean {
if (s == null || s.isEmpty()) {
return false
}
if (s[0].toInt() != mCurrent) {
return false
}
val len = s.length
mStream.mark(len - 1)
for (n in 1 until len) {
val value = mStream.read()
if (value != s[n].toInt()) {
mStream.reset()
return false
}
}
return true
}
}
object SQLParser {
private const val STATE_NONE = 0
private const val STATE_STRING = 1
private const val STATE_COMMENT = 2
private const val STATE_COMMENT_BLOCK = 3
fun parse(stream: InputStream): List<String> {
val buffer = BufferedInputStream(stream)
val commands: MutableList<String> = ArrayList()
val sb = StringBuffer()
try {
val tokenizer = Tokenizer(buffer)
var state = STATE_NONE
while (tokenizer.hasNext()) {
val c = tokenizer.next().toChar()
if (state == STATE_COMMENT_BLOCK) {
if (tokenizer.skip("*/")) {
state = STATE_NONE
}
continue
} else if (state == STATE_COMMENT) {
if (isNewLine(c)) {
state = STATE_NONE
}
continue
} else if (state == STATE_NONE && tokenizer.skip("/*")) {
state = STATE_COMMENT_BLOCK
continue
} else if (state == STATE_NONE && tokenizer.skip("--")) {
state = STATE_COMMENT
continue
} else if (state == STATE_NONE && c == ';') {
val command = sb.toString().trim { it <= ' ' }
commands.add(command)
sb.setLength(0)
continue
} else if (state == STATE_NONE && c == '\'') {
state = STATE_STRING
} else if (state == STATE_STRING && c == '\'') {
state = STATE_NONE
}
if (state == STATE_NONE || state == STATE_STRING) {
if (state == STATE_NONE && isWhitespace(c)) {
if (sb.length > 0 && sb[sb.length - 1] != ' ') {
sb.append(' ')
}
} else {
sb.append(c)
}
}
}
} finally {
buffer.close()
}
if (sb.isNotEmpty()) {
commands.add(sb.toString().trim { it <= ' ' })
}
return commands
}
private fun isNewLine(c: Char): Boolean {
return c == '\r' || c == '\n'
}
private fun isWhitespace(c: Char): Boolean {
return c == '\r' || c == '\n' || c == '\t' || c == ' '
}
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Table(val name: String, val id: String = "id")

View File

@@ -1,21 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.database
class UnsupportedDatabaseVersionException : RuntimeException()

View File

@@ -1,26 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.io
import java.io.File
abstract class AbstractImporter {
abstract fun canHandle(file: File): Boolean
abstract fun importHabitsFromFile(file: File)
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.io
import java.io.File
import javax.inject.Inject
/**
* A GenericImporter decides which implementation of AbstractImporter is able to
* handle a given file and delegates to it the task of importing the data.
*/
class GenericImporter
@Inject constructor(
loopDBImporter: LoopDBImporter,
rewireDBImporter: RewireDBImporter,
tickmateDBImporter: TickmateDBImporter,
habitBullCSVImporter: HabitBullCSVImporter,
) : AbstractImporter() {
var importers: List<AbstractImporter> = listOf(
loopDBImporter,
rewireDBImporter,
tickmateDBImporter,
habitBullCSVImporter,
)
override fun canHandle(file: File): Boolean {
for (importer in importers) {
if (importer.canHandle(file)) {
return true
}
}
return false
}
override fun importHabitsFromFile(file: File) {
for (importer in importers) {
if (importer.canHandle(file)) {
importer.importHabitsFromFile(file)
}
}
}
}

View File

@@ -1,78 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.io
import com.opencsv.CSVReader
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.utils.DateUtils
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
import java.util.HashMap
import javax.inject.Inject
/**
* Class that imports data from HabitBull CSV files.
*/
class HabitBullCSVImporter
@Inject constructor(
private val habitList: HabitList,
private val modelFactory: ModelFactory,
) : AbstractImporter() {
override fun canHandle(file: File): Boolean {
val reader = BufferedReader(FileReader(file))
val line = reader.readLine()
return line.startsWith("HabitName,HabitDescription,HabitCategory")
}
override fun importHabitsFromFile(file: File) {
val reader = CSVReader(FileReader(file))
val map = HashMap<String, Habit>()
for (line in reader) {
val name = line[0]
if (name == "HabitName") continue
val description = line[1]
val dateString = line[3].split("-").toTypedArray()
val year = dateString[0].toInt()
val month = dateString[1].toInt()
val day = dateString[2].toInt()
val date = DateUtils.getStartOfTodayCalendar()
date[year, month - 1] = day
val timestamp = Timestamp(date.timeInMillis)
val value = line[4].toInt()
if (value != 1) continue
var h = map[name]
if (h == null) {
h = modelFactory.buildHabit()
h.name = name
h.description = description ?: ""
h.frequency = Frequency.DAILY
habitList.add(h)
map[name] = h
}
h.originalEntries.add(Entry(timestamp, Entry.YES_MANUAL))
}
}
}

View File

@@ -1,232 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.io
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.Score
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.utils.DateFormats
import org.isoron.uhabits.core.utils.DateUtils
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.FileWriter
import java.io.IOException
import java.io.Writer
import java.util.ArrayList
import java.util.LinkedList
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/**
* Class that exports the application data to CSV files.
*/
class HabitsCSVExporter(
private val allHabits: HabitList,
private val selectedHabits: List<Habit>,
dir: File
) {
private val generatedDirs = LinkedList<String>()
private val generatedFilenames = LinkedList<String>()
private val exportDirName: String = dir.absolutePath + "/"
private val delimiter = ","
fun writeArchive(): String {
writeHabits()
val zipFilename = writeZipFile()
cleanup()
return zipFilename
}
private fun addFileToZip(zos: ZipOutputStream, filename: String) {
val fis = FileInputStream(File(exportDirName + filename))
val ze = ZipEntry(filename)
zos.putNextEntry(ze)
var length: Int
val bytes = ByteArray(1024)
while (fis.read(bytes).also { length = it } >= 0) zos.write(bytes, 0, length)
zos.closeEntry()
fis.close()
}
private fun cleanup() {
for (filename in generatedFilenames) File(exportDirName + filename).delete()
for (filename in generatedDirs) File(exportDirName + filename).delete()
File(exportDirName).delete()
}
private fun sanitizeFilename(name: String): String {
val s = name.replace("[^ a-zA-Z0-9\\._-]+".toRegex(), "")
return s.substring(0, Math.min(s.length, 100))
}
private fun writeHabits() {
val filename = "Habits.csv"
File(exportDirName).mkdirs()
val out = FileWriter(exportDirName + filename)
generatedFilenames.add(filename)
allHabits.writeCSV(out)
out.close()
for (h in selectedHabits) {
val sane = sanitizeFilename(h.name)
var habitDirName = String.format(Locale.US, "%03d %s", allHabits.indexOf(h) + 1, sane)
habitDirName = habitDirName.trim() + "/"
File(exportDirName + habitDirName).mkdirs()
generatedDirs.add(habitDirName)
writeScores(habitDirName, h)
writeEntries(habitDirName, h.computedEntries)
}
writeMultipleHabits()
}
private fun writeScores(habitDirName: String, habit: Habit) {
val path = habitDirName + "Scores.csv"
val out = FileWriter(exportDirName + path)
generatedFilenames.add(path)
val dateFormat = DateFormats.getCSVDateFormat()
val today = DateUtils.getTodayWithOffset()
var oldest = today
val known = habit.computedEntries.getKnown()
if (known.isNotEmpty()) oldest = known[known.size - 1].timestamp
for ((timestamp1, value) in habit.scores.getByInterval(oldest, today)) {
val timestamp = dateFormat.format(timestamp1.unixTime)
val score = String.format(Locale.US, "%.4f", value)
out.write(String.format("%s,%s\n", timestamp, score))
}
out.close()
}
private fun writeEntries(habitDirName: String, entries: EntryList) {
val filename = habitDirName + "Checkmarks.csv"
val out = FileWriter(exportDirName + filename)
generatedFilenames.add(filename)
val dateFormat = DateFormats.getCSVDateFormat()
for ((timestamp, value) in entries.getKnown()) {
val date = dateFormat.format(timestamp.toJavaDate())
out.write(String.format(Locale.US, "%s,%d\n", date, value))
}
out.close()
}
/**
* Writes a scores file and a checkmarks file containing scores and checkmarks of every habit.
* The first column corresponds to the date. Subsequent columns correspond to a habit.
* Habits are taken from the list of selected habits.
* Dates are determined from the oldest repetition date to the newest repetition date found in
* the list of habits.
*/
private fun writeMultipleHabits() {
val scoresFileName = "Scores.csv"
val checksFileName = "Checkmarks.csv"
generatedFilenames.add(scoresFileName)
generatedFilenames.add(checksFileName)
val scoresWriter = FileWriter(exportDirName + scoresFileName)
val checksWriter = FileWriter(exportDirName + checksFileName)
writeMultipleHabitsHeader(scoresWriter)
writeMultipleHabitsHeader(checksWriter)
val timeframe = getTimeframe()
val oldest = timeframe[0]
val newest = DateUtils.getToday()
val checkmarks: MutableList<ArrayList<Entry>> = ArrayList()
val scores: MutableList<ArrayList<Score>> = ArrayList()
for (habit in selectedHabits) {
checkmarks.add(ArrayList(habit.computedEntries.getByInterval(oldest, newest)))
scores.add(ArrayList(habit.scores.getByInterval(oldest, newest)))
}
val days = oldest.daysUntil(newest)
val dateFormat = DateFormats.getCSVDateFormat()
for (i in 0..days) {
val day = newest.minus(i).toJavaDate()
val date = dateFormat.format(day)
val sb = StringBuilder()
sb.append(date).append(delimiter)
checksWriter.write(sb.toString())
scoresWriter.write(sb.toString())
for (j in selectedHabits.indices) {
checksWriter.write(checkmarks[j][i].toString())
checksWriter.write(delimiter)
val score = String.format(Locale.US, "%.4f", scores[j][i].value)
scoresWriter.write(score)
scoresWriter.write(delimiter)
}
checksWriter.write("\n")
scoresWriter.write("\n")
}
scoresWriter.close()
checksWriter.close()
}
/**
* Writes the first row, containing header information, using the given writer.
* This consists of the date title and the names of the selected habits.
*
* @param out the writer to use
* @throws IOException if there was a problem writing
*/
@Throws(IOException::class)
private fun writeMultipleHabitsHeader(out: Writer) {
out.write("Date$delimiter")
for (habit in selectedHabits) {
out.write(habit.name)
out.write(delimiter)
}
out.write("\n")
}
/**
* Gets the overall timeframe of the selected habits.
* The timeframe is an array containing the oldest timestamp among the habits and the
* newest timestamp among the habits.
* Both timestamps are in milliseconds.
*
* @return the timeframe containing the oldest timestamp and the newest timestamp
*/
private fun getTimeframe(): Array<Timestamp> {
var oldest = Timestamp.ZERO.plus(1000000)
var newest = Timestamp.ZERO
for (habit in selectedHabits) {
val entries = habit.originalEntries.getKnown()
if (entries.isEmpty()) continue
val currNew = entries[0].timestamp
val currOld = entries[entries.size - 1].timestamp
oldest = if (currOld.isOlderThan(oldest)) currOld else oldest
newest = if (currNew.isNewerThan(newest)) currNew else newest
}
return arrayOf(oldest, newest)
}
private fun writeZipFile(): String {
val dateFormat = DateFormats.getCSVDateFormat()
val date = dateFormat.format(DateUtils.getStartOfToday())
val zipFilename = String.format("%s/Loop Habits CSV %s.zip", exportDirName, date)
val fos = FileOutputStream(zipFilename)
val zos = ZipOutputStream(fos)
for (filename in generatedFilenames) addFileToZip(zos, filename)
zos.close()
fos.close()
return zipFilename
}
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright (C) 2016-2020 Á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.core.io
interface Logging {
fun getLogger(name: String): Logger
}
interface Logger {
fun info(msg: String)
fun debug(msg: String)
fun error(msg: String)
fun error(exception: Exception)
}
class StandardLogging : Logging {
override fun getLogger(name: String): Logger {
return StandardLogger(name)
}
}
class StandardLogger(val name: String) : Logger {
override fun info(msg: String) {
println("[$name] $msg")
}
override fun debug(msg: String) {
println("[$name] $msg")
}
override fun error(msg: String) {
println("[$name] $msg")
}
override fun error(exception: Exception) {
exception.printStackTrace()
}
}

View File

@@ -1,113 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.io
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.DATABASE_VERSION
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.commands.CreateHabitCommand
import org.isoron.uhabits.core.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.commands.EditHabitCommand
import org.isoron.uhabits.core.database.DatabaseOpener
import org.isoron.uhabits.core.database.MigrationHelper
import org.isoron.uhabits.core.database.Repository
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.models.sqlite.records.EntryRecord
import org.isoron.uhabits.core.models.sqlite.records.HabitRecord
import org.isoron.uhabits.core.utils.isSQLite3File
import java.io.File
import javax.inject.Inject
/**
* Class that imports data from database files exported by Loop Habit Tracker.
*/
class LoopDBImporter
@Inject constructor(
@AppScope val habitList: HabitList,
@AppScope val modelFactory: ModelFactory,
@AppScope val opener: DatabaseOpener,
@AppScope val runner: CommandRunner,
@AppScope logging: Logging,
) : AbstractImporter() {
private val logger = logging.getLogger("LoopDBImporter")
override fun canHandle(file: File): Boolean {
if (!file.isSQLite3File()) return false
val db = opener.open(file)!!
var canHandle = true
val c = db.query("select count(*) from SQLITE_MASTER where name='Habits' or name='Repetitions'")
if (!c.moveToNext() || c.getInt(0) != 2) {
logger.error("Cannot handle file: tables not found")
canHandle = false
}
if (db.version > DATABASE_VERSION) {
logger.error("Cannot handle file: incompatible version: ${db.version} > $DATABASE_VERSION")
canHandle = false
}
c.close()
db.close()
return canHandle
}
override fun importHabitsFromFile(file: File) {
val db = opener.open(file)!!
val helper = MigrationHelper(db)
helper.migrateTo(DATABASE_VERSION)
val habitsRepository = Repository(HabitRecord::class.java, db)
val entryRepository = Repository(EntryRecord::class.java, db)
for (habitRecord in habitsRepository.findAll("order by position")) {
var habit = habitList.getByUUID(habitRecord.uuid)
val entryRecords = entryRepository.findAll("where habit = ?", habitRecord.id.toString())
var command: Command
if (habit == null) {
habit = modelFactory.buildHabit()
habitRecord.id = null
habitRecord.copyTo(habit)
command = CreateHabitCommand(modelFactory, habitList, habit)
command.run()
} else {
val modified = modelFactory.buildHabit()
habitRecord.id = habit.id
habitRecord.copyTo(modified)
command = EditHabitCommand(habitList, habit.id!!, modified)
command.run()
}
// Reload saved version of the habit
habit = habitList.getByUUID(habitRecord.uuid)
for (r in entryRecords) {
val t = Timestamp(r.timestamp)
val (_, value) = habit!!.originalEntries.get(t)
if (value != r.value) CreateRepetitionCommand(habitList, habit, t, r.value).run()
}
runner.notifyListeners(command)
}
db.close()
}
}

View File

@@ -1,171 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.io
import org.isoron.uhabits.core.database.Cursor
import org.isoron.uhabits.core.database.Database
import org.isoron.uhabits.core.database.DatabaseOpener
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.models.WeekdayList
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.core.utils.isSQLite3File
import java.io.File
import javax.inject.Inject
/**
* Class that imports database files exported by Rewire.
*/
class RewireDBImporter
@Inject constructor(
private val habitList: HabitList,
private val modelFactory: ModelFactory,
private val opener: DatabaseOpener
) : AbstractImporter() {
override fun canHandle(file: File): Boolean {
if (!file.isSQLite3File()) return false
val db = opener.open(file)
val c = db.query(
"select count(*) from SQLITE_MASTER " +
"where name='CHECKINS' or name='UNIT'"
)
val result = c.moveToNext() && c.getInt(0) == 2
c.close()
db.close()
return result
}
override fun importHabitsFromFile(file: File) {
val db = opener.open(file)
db.beginTransaction()
createHabits(db)
db.setTransactionSuccessful()
db.endTransaction()
db.close()
}
private fun createHabits(db: Database) {
var c: Cursor? = null
try {
c = db.query(
"select _id, name, description, schedule, " +
"active_days, repeating_count, days, period " +
"from habits"
)
if (!c.moveToNext()) return
do {
val id = c.getInt(0)!!
val name = c.getString(1)
val description = c.getString(2)
val schedule = c.getInt(3)!!
val activeDays = c.getString(4)
val repeatingCount = c.getInt(5)!!
val days = c.getInt(6)!!
val periodIndex = c.getInt(7)!!
val habit = modelFactory.buildHabit()
habit.name = name!!
habit.description = description ?: ""
val periods = intArrayOf(7, 31, 365)
var numerator: Int
var denominator: Int
when (schedule) {
0 -> {
numerator = activeDays!!.split(",").toTypedArray().size
denominator = 7
}
1 -> {
numerator = days
denominator = periods[periodIndex]
}
2 -> {
numerator = 1
denominator = repeatingCount
}
else -> throw IllegalStateException()
}
habit.frequency = Frequency(numerator, denominator)
habitList.add(habit)
createReminder(db, habit, id)
createCheckmarks(db, habit, id)
} while (c.moveToNext())
} finally {
c?.close()
}
}
private fun createCheckmarks(
db: Database,
habit: Habit,
rewireHabitId: Int
) {
var c: Cursor? = null
try {
c = db.query(
"select distinct date from checkins where habit_id=? and type=2",
rewireHabitId.toString(),
)
if (!c.moveToNext()) return
do {
val date = c.getString(0)
val year = date!!.substring(0, 4).toInt()
val month = date.substring(4, 6).toInt()
val day = date.substring(6, 8).toInt()
val cal = DateUtils.getStartOfTodayCalendar()
cal[year, month - 1] = day
habit.originalEntries.add(Entry(Timestamp(cal), Entry.YES_MANUAL))
} while (c.moveToNext())
} finally {
c?.close()
}
}
private fun createReminder(db: Database, habit: Habit, rewireHabitId: Int) {
var c: Cursor? = null
try {
c = db.query(
"select time, active_days from reminders where habit_id=? limit 1",
rewireHabitId.toString(),
)
if (!c.moveToNext()) return
val rewireReminder = c.getInt(0)!!
if (rewireReminder <= 0 || rewireReminder >= 1440) return
val reminderDays = BooleanArray(7)
val activeDays = c.getString(1)!!.split(",").toTypedArray()
for (d in activeDays) {
val idx = (d.toInt() + 1) % 7
reminderDays[idx] = true
}
val hour = rewireReminder / 60
val minute = rewireReminder % 60
val days = WeekdayList(reminderDays)
val reminder = Reminder(hour, minute, days)
habit.reminder = reminder
habitList.update(habit)
} finally {
c?.close()
}
}
}

View File

@@ -1,111 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.io
import org.isoron.uhabits.core.database.Cursor
import org.isoron.uhabits.core.database.Database
import org.isoron.uhabits.core.database.DatabaseOpener
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.utils.DateUtils
import org.isoron.uhabits.core.utils.isSQLite3File
import java.io.File
import javax.inject.Inject
/**
* Class that imports data from database files exported by Tickmate.
*/
class TickmateDBImporter @Inject constructor(
private val habitList: HabitList,
private val modelFactory: ModelFactory,
private val opener: DatabaseOpener
) : AbstractImporter() {
override fun canHandle(file: File): Boolean {
if (!file.isSQLite3File()) return false
val db = opener.open(file)
val c = db.query(
"select count(*) from SQLITE_MASTER " +
"where name='tracks' or name='track2groups'"
)
val result = c.moveToNext() && c.getInt(0) == 2
c.close()
db.close()
return result
}
override fun importHabitsFromFile(file: File) {
val db = opener.open(file)
db.beginTransaction()
createHabits(db)
db.setTransactionSuccessful()
db.endTransaction()
db.close()
}
private fun createCheckmarks(
db: Database,
habit: Habit,
tickmateTrackId: Int
) {
var c: Cursor? = null
try {
c = db.query(
"select distinct year, month, day from ticks where _track_id=?",
tickmateTrackId.toString(),
)
if (!c.moveToNext()) return
do {
val year = c.getInt(0)!!
val month = c.getInt(1)!!
val day = c.getInt(2)!!
val cal = DateUtils.getStartOfTodayCalendar()
cal[year, month] = day
habit.originalEntries.add(Entry(Timestamp(cal), Entry.YES_MANUAL))
} while (c.moveToNext())
} finally {
c?.close()
}
}
private fun createHabits(db: Database) {
var c: Cursor? = null
try {
c = db.query("select _id, name, description from tracks")
if (!c.moveToNext()) return
do {
val id = c.getInt(0)!!
val name = c.getString(1)
val description = c.getString(2)
val habit = modelFactory.buildHabit()
habit.name = name!!
habit.description = description ?: ""
habit.frequency = Frequency.DAILY
habitList.add(habit)
createCheckmarks(db, habit, id)
} while (c.moveToNext())
} finally {
c?.close()
}
}
}

View File

@@ -1,69 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
data class Entry(
val timestamp: Timestamp,
val value: Int,
) {
companion object {
/**
* Value indicating that the habit is not applicable for this timestamp.
*/
const val SKIP = 3
/**
* Value indicating that the user has performed the habit at this timestamp.
*/
const val YES_MANUAL = 2
/**
* Value indicating that the user did not perform the habit, but they were not
* expected to, because of the frequency of the habit.
*/
const val YES_AUTO = 1
/**
* Value indicating that the user did not perform the habit, even though they were
* expected to perform it.
*/
const val NO = 0
/**
* Value indicating that no data is available for the given timestamp.
*/
const val UNKNOWN = -1
fun nextToggleValueWithSkip(value: Int): Int {
return when (value) {
NO, UNKNOWN, YES_AUTO -> YES_MANUAL
YES_MANUAL -> SKIP
SKIP -> NO
else -> NO
}
}
fun nextToggleValueWithoutSkip(value: Int): Int {
return when (value) {
NO, UNKNOWN, YES_AUTO -> YES_MANUAL
else -> NO
}
}
}
}

View File

@@ -1,297 +0,0 @@
/*
* Copyright (C) 2016-2020 Á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.core.models
import org.isoron.uhabits.core.models.Entry.Companion.SKIP
import org.isoron.uhabits.core.models.Entry.Companion.UNKNOWN
import org.isoron.uhabits.core.models.Entry.Companion.YES_AUTO
import org.isoron.uhabits.core.models.Entry.Companion.YES_MANUAL
import org.isoron.uhabits.core.utils.DateUtils
import java.util.ArrayList
import java.util.Calendar
import javax.annotation.concurrent.ThreadSafe
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
@ThreadSafe
open class EntryList {
private val entriesByTimestamp: HashMap<Timestamp, Entry> = HashMap()
/**
* Returns the entry corresponding to the given timestamp. If no entry with such timestamp
* has been previously added, returns Entry(timestamp, UNKNOWN).
*/
@Synchronized
open fun get(timestamp: Timestamp): Entry {
return entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN)
}
/**
* Returns one entry for each day in the given interval. The first element corresponds to the
* newest entry, and the last element corresponds to the oldest. The interval endpoints are
* included.
*/
@Synchronized
open fun getByInterval(from: Timestamp, to: Timestamp): List<Entry> {
val result = mutableListOf<Entry>()
if (from.isNewerThan(to)) return result
var current = to
while (current >= from) {
result.add(get(current))
current = current.minus(1)
}
return result
}
/**
* Adds the given entry to the list. If another entry with the same timestamp already exists,
* replaces it.
*/
@Synchronized
open fun add(entry: Entry) {
entriesByTimestamp[entry.timestamp] = entry
}
/**
* Returns all entries whose values are known, sorted by timestamp. The first element
* corresponds to the newest entry, and the last element corresponds to the oldest.
*/
@Synchronized
open fun getKnown(): List<Entry> {
return entriesByTimestamp.values.sortedBy { it.timestamp }.reversed()
}
/**
* Replaces all entries in this list by entries computed automatically from another list.
*
* For boolean habits, this function creates additional entries (with value YES_AUTO) according
* to the frequency of the habit. For numerical habits, this function simply copies all entries.
*/
@Synchronized
open fun recomputeFrom(
originalEntries: EntryList,
frequency: Frequency,
isNumerical: Boolean,
) {
clear()
val original = originalEntries.getKnown()
if (isNumerical) {
original.forEach { add(it) }
} else {
val intervals = buildIntervals(frequency, original)
snapIntervalsTogether(intervals)
val computed = buildEntriesFromInterval(original, intervals)
computed.filter { it.value != UNKNOWN }.forEach { add(it) }
}
}
/**
* Removes all known entries.
*/
@Synchronized
open fun clear() {
entriesByTimestamp.clear()
}
/**
* Returns the total number of successful entries for each month, grouped by day of week.
* <p>
* The checkmarks are returned in a HashMap. The key is the timestamp for
* the first day of the month, at midnight (00:00). The value is an integer
* array with 7 entries. The first entry contains the total number of
* successful checkmarks during the specified month that occurred on a Saturday. The
* second entry corresponds to Sunday, and so on. If there are no
* successful checkmarks during a certain month, the value is null.
*
* @return total number of checkmarks by month versus day of week
*/
@Synchronized
fun computeWeekdayFrequency(isNumerical: Boolean): HashMap<Timestamp, Array<Int>> {
val entries = getKnown()
val map = hashMapOf<Timestamp, Array<Int>>()
for ((originalTimestamp, value) in entries) {
val weekday = originalTimestamp.weekday
val truncatedTimestamp = Timestamp(
originalTimestamp.toCalendar().apply {
set(Calendar.DAY_OF_MONTH, 1)
}.timeInMillis
)
var list = map[truncatedTimestamp]
if (list == null) {
list = arrayOf(0, 0, 0, 0, 0, 0, 0)
map[truncatedTimestamp] = list
}
if (isNumerical) {
list[weekday] += value
} else if (value == YES_MANUAL) {
list[weekday] += 1
}
}
return map
}
data class Interval(val begin: Timestamp, val center: Timestamp, val end: Timestamp) {
val length: Int
get() = begin.daysUntil(end) + 1
}
/**
* Converts a list of intervals into a list of entries. Entries that fall outside of any
* interval receive value UNKNOWN. Entries that fall within an interval but do not appear
* in [original] receive value YES_AUTO. Entries provided in [original] are copied over.
*
* The intervals should be sorted by timestamp. The first element in the list should
* correspond to the newest interval.
*/
companion object {
fun buildEntriesFromInterval(
original: List<Entry>,
intervals: List<Interval>,
): List<Entry> {
val result = arrayListOf<Entry>()
if (original.isEmpty()) return result
var from = original[0].timestamp
var to = original[0].timestamp
for (e in original) {
if (e.timestamp < from) from = e.timestamp
if (e.timestamp > to) to = e.timestamp
}
for (interval in intervals) {
if (interval.begin < from) from = interval.begin
if (interval.end > to) to = interval.end
}
// Create unknown entries
var current = to
while (current >= from) {
result.add(Entry(current, UNKNOWN))
current = current.minus(1)
}
// Create YES_AUTO entries
intervals.forEach { interval ->
current = interval.end
while (current >= interval.begin) {
val offset = current.daysUntil(to)
result[offset] = Entry(current, YES_AUTO)
current = current.minus(1)
}
}
// Copy original entries
original.forEach { entry ->
val offset = entry.timestamp.daysUntil(to)
if (result[offset].value == UNKNOWN || entry.value == SKIP || entry.value == YES_MANUAL) {
result[offset] = entry
}
}
return result
}
/**
* Starting from the second newest interval, this function tries to slide the
* intervals backwards into the past, so that gaps are eliminated and
* streaks are maximized.
*
* The intervals should be sorted by timestamp. The first element in the list should
* correspond to the newest interval.
*/
fun snapIntervalsTogether(intervals: ArrayList<Interval>) {
for (i in 1 until intervals.size) {
val curr = intervals[i]
val next = intervals[i - 1]
val gapNextToCurrent = next.begin.daysUntil(curr.end)
val gapCenterToEnd = curr.center.daysUntil(curr.end)
if (gapNextToCurrent >= 0) {
val shift = min(gapCenterToEnd, gapNextToCurrent + 1)
intervals[i] = Interval(
curr.begin.minus(shift),
curr.center,
curr.end.minus(shift)
)
}
}
}
fun buildIntervals(
freq: Frequency,
entries: List<Entry>,
): ArrayList<Interval> {
val filtered = entries.filter { it.value == YES_MANUAL }
val num = freq.numerator
val den = freq.denominator
val intervals = arrayListOf<Interval>()
for (i in num - 1 until filtered.size) {
val (begin, _) = filtered[i]
val (center, _) = filtered[i - num + 1]
if (begin.daysUntil(center) < den) {
val end = begin.plus(den - 1)
intervals.add(Interval(begin, center, end))
}
}
return intervals
}
}
}
/**
* Given a list of entries, truncates the timestamp of each entry (according to the field given),
* groups the entries according to this truncated timestamp, then creates a new entry (t,v) for
* each group, where t is the truncated timestamp and v is the sum of the values of all entries in
* the group.
*
* For numerical habits, non-positive entry values are converted to zero. For boolean habits, each
* YES_MANUAL value is converted to 1000 and all other values are converted to zero.
*
* The returned list is sorted by timestamp, with the newest entry coming first and the oldest entry
* coming last. If the original list has gaps in it (for example, weeks or months without any
* entries), then the list produced by this method will also have gaps.
*
* The argument [firstWeekday] is only relevant when truncating by week.
*/
fun List<Entry>.groupedSum(
truncateField: DateUtils.TruncateField,
firstWeekday: Int = Calendar.SATURDAY,
isNumerical: Boolean,
): List<Entry> {
return this.map { (timestamp, value) ->
if (isNumerical) {
Entry(timestamp, max(0, value))
} else {
Entry(timestamp, if (value == YES_MANUAL) 1000 else 0)
}
}.groupBy { entry ->
entry.timestamp.truncate(
truncateField,
firstWeekday,
)
}.entries.map { (timestamp, entries) ->
Entry(timestamp, entries.sumOf { it.value })
}.sortedBy { (timestamp, _) ->
- timestamp.unixTime
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
data class Frequency(
var numerator: Int,
var denominator: Int,
) {
init {
if (numerator == denominator) {
denominator = 1
numerator = 1
}
}
fun toDouble(): Double {
return numerator.toDouble() / denominator
}
companion object {
@JvmField
val DAILY = Frequency(1, 1)
@JvmField
val THREE_TIMES_PER_WEEK = Frequency(3, 7)
@JvmField
val TWO_TIMES_PER_WEEK = Frequency(2, 7)
@JvmField
val WEEKLY = Frequency(1, 7)
}
}

View File

@@ -1,163 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
import org.isoron.uhabits.core.utils.DateUtils
import java.util.UUID
data class Habit(
var color: PaletteColor = PaletteColor(8),
var description: String = "",
var frequency: Frequency = Frequency.DAILY,
var id: Long? = null,
var isArchived: Boolean = false,
var name: String = "",
var position: Int = 0,
var question: String = "",
var reminder: Reminder? = null,
var targetType: Int = AT_LEAST,
var targetValue: Double = 0.0,
var type: Int = YES_NO_HABIT,
var unit: String = "",
var uuid: String? = null,
val computedEntries: EntryList,
val originalEntries: EntryList,
val scores: ScoreList,
val streaks: StreakList,
) {
init {
if (uuid == null) this.uuid = UUID.randomUUID().toString().replace("-", "")
}
var observable = ModelObservable()
val isNumerical: Boolean
get() = type == NUMBER_HABIT
val uriString: String
get() = "content://org.isoron.uhabits/habit/$id"
fun hasReminder(): Boolean = reminder != null
fun isCompletedToday(): Boolean {
val today = DateUtils.getTodayWithOffset()
val value = computedEntries.get(today).value
return if (isNumerical) {
if (targetType == AT_LEAST) {
value / 1000.0 >= targetValue
} else {
value / 1000.0 <= targetValue
}
} else {
value != Entry.NO && value != Entry.UNKNOWN
}
}
fun recompute() {
computedEntries.recomputeFrom(
originalEntries = originalEntries,
frequency = frequency,
isNumerical = isNumerical,
)
val to = DateUtils.getTodayWithOffset().plus(30)
val entries = computedEntries.getKnown()
var from = entries.lastOrNull()?.timestamp ?: to
if (from.isNewerThan(to)) from = to
scores.recompute(
frequency = frequency,
isNumerical = isNumerical,
targetValue = targetValue,
computedEntries = computedEntries,
from = from,
to = to,
)
streaks.recompute(
computedEntries,
from,
to,
)
}
fun copyFrom(other: Habit) {
this.color = other.color
this.description = other.description
this.frequency = other.frequency
// this.id should not be copied
this.isArchived = other.isArchived
this.name = other.name
this.position = other.position
this.question = other.question
this.reminder = other.reminder
this.targetType = other.targetType
this.targetValue = other.targetValue
this.type = other.type
this.unit = other.unit
this.uuid = other.uuid
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Habit) return false
if (color != other.color) return false
if (description != other.description) return false
if (frequency != other.frequency) return false
if (id != other.id) return false
if (isArchived != other.isArchived) return false
if (name != other.name) return false
if (position != other.position) return false
if (question != other.question) return false
if (reminder != other.reminder) return false
if (targetType != other.targetType) return false
if (targetValue != other.targetValue) return false
if (type != other.type) return false
if (unit != other.unit) return false
if (uuid != other.uuid) return false
return true
}
override fun hashCode(): Int {
var result = color.hashCode()
result = 31 * result + description.hashCode()
result = 31 * result + frequency.hashCode()
result = 31 * result + (id?.hashCode() ?: 0)
result = 31 * result + isArchived.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + position
result = 31 * result + question.hashCode()
result = 31 * result + (reminder?.hashCode() ?: 0)
result = 31 * result + targetType
result = 31 * result + targetValue.hashCode()
result = 31 * result + type
result = 31 * result + unit.hashCode()
result = 31 * result + (uuid?.hashCode() ?: 0)
return result
}
companion object {
const val AT_LEAST = 0
const val AT_MOST = 1
const val NUMBER_HABIT = 1
const val YES_NO_HABIT = 0
}
}

View File

@@ -1,269 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models;
import androidx.annotation.*;
import com.opencsv.*;
import java.io.*;
import java.util.*;
import javax.annotation.concurrent.*;
/**
* An ordered collection of {@link Habit}s.
*/
@ThreadSafe
public abstract class HabitList implements Iterable<Habit>
{
private final ModelObservable observable;
@NonNull
protected final HabitMatcher filter;
/**
* Creates a new HabitList.
* <p>
* Depending on the implementation, this list can either be empty or be
* populated by some pre-existing habits, for example, from a certain
* database.
*/
public HabitList()
{
observable = new ModelObservable();
filter = new HabitMatcherBuilder().setArchivedAllowed(true).build();
}
protected HabitList(@NonNull HabitMatcher filter)
{
observable = new ModelObservable();
this.filter = filter;
}
/**
* Inserts a new habit in the list.
* <p>
* If the id of the habit is null, the list will assign it a new id, which
* is guaranteed to be unique in the scope of the list. If id is not null,
* the caller should make sure that the list does not already contain
* another habit with same id, otherwise a RuntimeException will be thrown.
*
* @param habit the habit to be inserted
* @throws IllegalArgumentException if the habit is already on the list.
*/
public abstract void add(@NonNull Habit habit)
throws IllegalArgumentException;
/**
* Returns the habit with specified id.
*
* @param id the id of the habit
* @return the habit, or null if none exist
*/
@Nullable
public abstract Habit getById(long id);
/**
* Returns the habit with specified UUID.
*
* @param uuid the UUID of the habit
* @return the habit, or null if none exist
*/
@Nullable
public abstract Habit getByUUID(String uuid);
/**
* Returns the habit that occupies a certain position.
*
* @param position the position of the desired habit
* @return the habit at that position
* @throws IndexOutOfBoundsException when the position is invalid
*/
@NonNull
public abstract Habit getByPosition(int position);
/**
* Returns the list of habits that match a given condition.
*
* @param matcher the matcher that checks the condition
* @return the list of matching habits
*/
@NonNull
public abstract HabitList getFiltered(HabitMatcher matcher);
public ModelObservable getObservable()
{
return observable;
}
public abstract Order getPrimaryOrder();
public abstract Order getSecondaryOrder();
/**
* Changes the order of the elements on the list.
*
* @param order the new order criterion
*/
public abstract void setPrimaryOrder(@NonNull Order order);
/**
* Changes the previous order of the elements on the list.
*
* @param order the new order criterion
*/
public abstract void setSecondaryOrder(@NonNull Order order);
/**
* Returns the index of the given habit in the list, or -1 if the list does
* not contain the habit.
*
* @param h the habit
* @return the index of the habit, or -1 if not in the list
*/
public abstract int indexOf(@NonNull Habit h);
public boolean isEmpty()
{
return size() == 0;
}
/**
* Removes the given habit from the list.
* <p>
* If the given habit is not in the list, does nothing.
*
* @param h the habit to be removed.
*/
public abstract void remove(@NonNull Habit h);
/**
* Removes all the habits from the list.
*/
public void removeAll()
{
List<Habit> copy = new LinkedList<>();
for (Habit h : this) copy.add(h);
for (Habit h : copy) remove(h);
observable.notifyListeners();
}
/**
* Changes the position of a habit in the list.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
public abstract void reorder(@NonNull Habit from, @NonNull Habit to);
public void repair()
{
}
/**
* Returns the number of habits in this list.
*
* @return number of habits
*/
public abstract int size();
/**
* Notifies the list that a certain list of habits has been modified.
* <p>
* Depending on the implementation, this operation might trigger a write to
* disk, or do nothing at all. To make sure that the habits get persisted,
* this operation must be called.
*
* @param habits the list of habits that have been modified.
*/
public abstract void update(List<Habit> habits);
/**
* Notifies the list that a certain habit has been modified.
* <p>
* See {@link #update(List)} for more details.
*
* @param habit the habit that has been modified.
*/
public void update(@NonNull Habit habit)
{
update(Collections.singletonList(habit));
}
/**
* Writes the list of habits to the given writer, in CSV format. There is
* one line for each habit, containing the fields name, description,
* frequency numerator, frequency denominator and color. The color is
* written in HTML format (#000000).
*
* @param out the writer that will receive the result
* @throws IOException if write operations fail
*/
public void writeCSV(@NonNull Writer out) throws IOException
{
String header[] = {
"Position",
"Name",
"Question",
"Description",
"NumRepetitions",
"Interval",
"Color"
};
CSVWriter csv = new CSVWriter(out);
csv.writeNext(header, false);
for (Habit habit : this)
{
Frequency freq = habit.getFrequency();
String[] cols = {
String.format("%03d", indexOf(habit) + 1),
habit.getName(),
habit.getQuestion(),
habit.getDescription(),
Integer.toString(freq.getNumerator()),
Integer.toString(freq.getDenominator()),
habit.getColor().toCsvColor(),
};
csv.writeNext(cols, false);
}
csv.close();
}
public abstract void resort();
public enum Order
{
BY_NAME_ASC,
BY_NAME_DESC,
BY_COLOR_ASC,
BY_COLOR_DESC,
BY_SCORE_ASC,
BY_SCORE_DESC,
BY_STATUS_ASC,
BY_STATUS_DESC,
BY_POSITION
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
data class HabitMatcher(
val isArchivedAllowed: Boolean = false,
val isReminderRequired: Boolean = false,
val isCompletedAllowed: Boolean = true,
) {
fun matches(habit: Habit): Boolean {
if (!isArchivedAllowed && habit.isArchived) return false
if (isReminderRequired && !habit.hasReminder()) return false
if (!isCompletedAllowed && habit.isCompletedToday()) return false
return true
}
companion object {
@JvmField
val WITH_ALARM = HabitMatcherBuilder()
.setArchivedAllowed(true)
.setReminderRequired(true)
.build()
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
class HabitMatcherBuilder {
private var archivedAllowed = false
private var reminderRequired = false
private var completedAllowed = true
fun build(): HabitMatcher {
return HabitMatcher(
isArchivedAllowed = archivedAllowed,
isReminderRequired = reminderRequired,
isCompletedAllowed = completedAllowed,
)
}
fun setArchivedAllowed(archivedAllowed: Boolean): HabitMatcherBuilder {
this.archivedAllowed = archivedAllowed
return this
}
fun setCompletedAllowed(completedAllowed: Boolean): HabitMatcherBuilder {
this.completedAllowed = completedAllowed
return this
}
fun setReminderRequired(reminderRequired: Boolean): HabitMatcherBuilder {
this.reminderRequired = reminderRequired
return this
}
}

View File

@@ -1,21 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
class HabitNotFoundException : RuntimeException()

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
import org.isoron.uhabits.core.database.Repository
import org.isoron.uhabits.core.models.sqlite.records.EntryRecord
import org.isoron.uhabits.core.models.sqlite.records.HabitRecord
/**
* Interface implemented by factories that provide concrete implementations of
* the core model classes.
*/
interface ModelFactory {
fun buildHabit(): Habit {
val scores = buildScoreList()
val streaks = buildStreakList()
val habit = Habit(
scores = scores,
streaks = streaks,
originalEntries = buildOriginalEntries(),
computedEntries = buildComputedEntries(),
)
return habit
}
fun buildComputedEntries(): EntryList
fun buildOriginalEntries(): EntryList
fun buildHabitList(): HabitList
fun buildScoreList(): ScoreList
fun buildStreakList(): StreakList
fun buildHabitListRepository(): Repository<HabitRecord>
fun buildRepetitionListRepository(): Repository<EntryRecord>
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models;
import java.util.*;
import javax.annotation.concurrent.*;
/**
* A ModelObservable allows objects to subscribe themselves to it and receive
* notifications whenever the model is changed.
*/
@ThreadSafe
public class ModelObservable
{
private List<Listener> listeners;
/**
* Creates a new ModelObservable with no listeners.
*/
public ModelObservable()
{
super();
listeners = new LinkedList<>();
}
/**
* Adds the given listener to the observable.
*
* @param l the listener to be added.
*/
public synchronized void addListener(Listener l)
{
listeners.add(l);
}
/**
* Notifies every listener that the model has changed.
* <p>
* Only models should call this method.
*/
public synchronized void notifyListeners()
{
for (Listener l : listeners) l.onModelChange();
}
/**
* Removes the given listener.
* <p>
* The listener will no longer be notified when the model changes. If the
* given listener is not subscribed to this observable, does nothing.
*
* @param l the listener to be removed
*/
public synchronized void removeListener(Listener l)
{
listeners.remove(l);
}
/**
* Interface implemented by objects that want to be notified when the model
* changes.
*/
public interface Listener
{
/**
* Called whenever the model associated to this observable has been
* modified.
*/
void onModelChange();
}
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright (C) 2016-2020 Á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.core.models
data class PaletteColor(val paletteIndex: Int) {
fun toCsvColor(): String {
return arrayOf(
"#D32F2F", // 0 red
"#E64A19", // 1 deep orange
"#F57C00", // 2 orange
"#FF8F00", // 3 amber
"#F9A825", // 4 yellow
"#AFB42B", // 5 lime
"#7CB342", // 6 light green
"#388E3C", // 7 green
"#00897B", // 8 teal
"#00ACC1", // 9 cyan
"#039BE5", // 10 light blue
"#1976D2", // 11 blue
"#303F9F", // 12 indigo
"#5E35B1", // 13 deep purple
"#8E24AA", // 14 purple
"#D81B60", // 15 pink
"#5D4037", // 16 brown
"#303030", // 17 dark grey
"#757575", // 18 grey
"#aaaaaa" // 19 light grey
)[paletteIndex]
}
fun compareTo(other: PaletteColor): Int {
return paletteIndex.compareTo(other.paletteIndex)
}
}

View File

@@ -1,30 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
import org.isoron.uhabits.core.utils.DateUtils
data class Reminder(
val hour: Int,
val minute: Int,
val days: WeekdayList,
) {
val timeInMillis: Long
get() = DateUtils.getUpcomingTimeInMillis(hour, minute)
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
import kotlin.math.sqrt
data class Score(
val timestamp: Timestamp,
val value: Double,
) {
companion object {
/**
* Given the frequency of the habit, the previous score, and the value of
* the current checkmark, computes the current score for the habit.
*
* The frequency of the habit is the number of repetitions divided by the
* length of the interval. For example, a habit that should be repeated 3
* times in 8 days has frequency 3.0 / 8.0 = 0.375.
*/
@JvmStatic
fun compute(
frequency: Double,
previousScore: Double,
checkmarkValue: Double,
): Double {
val multiplier = Math.pow(0.5, sqrt(frequency) / 13.0)
var score = previousScore * multiplier
score += checkmarkValue * (1 - multiplier)
return score
}
}
}

View File

@@ -1,120 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
import org.isoron.uhabits.core.models.Score.Companion.compute
import java.util.ArrayList
import java.util.HashMap
import javax.annotation.concurrent.ThreadSafe
import kotlin.math.min
@ThreadSafe
class ScoreList {
private val map = HashMap<Timestamp, Score>()
/**
* Returns the score for a given day. If the timestamp given happens before the first
* repetition of the habit or after the last computed score, returns a score with value zero.
*/
@Synchronized
operator fun get(timestamp: Timestamp): Score {
return map[timestamp] ?: Score(timestamp, 0.0)
}
/**
* Returns the list of scores that fall within the given interval.
*
* There is exactly one score per day in the interval. The endpoints of the interval are
* included. The list is ordered by timestamp (decreasing). That is, the first score
* corresponds to the newest timestamp, and the last score corresponds to the oldest timestamp.
*/
@Synchronized
fun getByInterval(
fromTimestamp: Timestamp,
toTimestamp: Timestamp,
): List<Score> {
val result: MutableList<Score> = ArrayList()
if (fromTimestamp.isNewerThan(toTimestamp)) return result
var current = toTimestamp
while (!current.isOlderThan(fromTimestamp)) {
result.add(get(current))
current = current.minus(1)
}
return result
}
/**
* Recomputes all scores between the provided [from] and [to] timestamps.
*/
@Synchronized
fun recompute(
frequency: Frequency,
isNumerical: Boolean,
targetValue: Double,
computedEntries: EntryList,
from: Timestamp,
to: Timestamp,
) {
map.clear()
if (computedEntries.getKnown().isEmpty()) return
if (from.isNewerThan(to)) return
var rollingSum = 0.0
var numerator = frequency.numerator
var denominator = frequency.denominator
val freq = frequency.toDouble()
val values = computedEntries.getByInterval(from, to).map { it.value }.toIntArray()
// For non-daily boolean habits, we double the numerator and the denominator to smooth
// out irregular repetition schedules (for example, weekly habits performed on different
// days of the week)
if (!isNumerical && freq < 1.0) {
numerator *= 2
denominator *= 2
}
var previousValue = 0.0
for (i in values.indices) {
val offset = values.size - i - 1
if (isNumerical) {
rollingSum += values[offset]
if (offset + denominator < values.size) {
rollingSum -= values[offset + denominator]
}
val percentageCompleted = min(1.0, rollingSum / 1000 / targetValue)
previousValue = compute(freq, previousValue, percentageCompleted)
} else {
if (values[offset] == Entry.YES_MANUAL) {
rollingSum += 1.0
}
if (offset + denominator < values.size) {
if (values[offset + denominator] == Entry.YES_MANUAL) {
rollingSum -= 1.0
}
}
if (values[offset] != Entry.SKIP) {
val percentageCompleted = Math.min(1.0, rollingSum / numerator)
previousValue = compute(freq, previousValue, percentageCompleted)
}
}
val timestamp = from.plus(i)
map[timestamp] = Score(timestamp, previousValue)
}
}
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
import java.lang.Long.signum
data class Streak(
val start: Timestamp,
val end: Timestamp,
) {
fun compareLonger(other: Streak): Int {
return if (length != other.length) signum(length - other.length.toLong())
else compareNewer(other)
}
fun compareNewer(other: Streak): Int {
return end.compareTo(other.end)
}
val length: Int
get() = start.daysUntil(end) + 1
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models
import javax.annotation.concurrent.ThreadSafe
import kotlin.math.min
@ThreadSafe
class StreakList {
private val list = ArrayList<Streak>()
@Synchronized
fun getBest(limit: Int): List<Streak> {
list.sortWith { s1: Streak, s2: Streak -> s2.compareLonger(s1) }
return list.subList(0, min(list.size, limit)).apply {
sortWith { s1: Streak, s2: Streak -> s2.compareNewer(s1) }
}.toList()
}
@Synchronized
fun recompute(
computedEntries: EntryList,
from: Timestamp,
to: Timestamp,
) {
list.clear()
val timestamps = computedEntries
.getByInterval(from, to)
.filter { it.value > 0 }
.map { it.timestamp }
.toTypedArray()
if (timestamps.isEmpty()) return
var begin = timestamps[0]
var end = timestamps[0]
for (i in 1 until timestamps.size) {
val current = timestamps[i]
if (current == begin.minus(1)) {
begin = current
} else {
list.add(Streak(begin, end))
begin = current
end = current
}
}
list.add(Streak(begin, end))
}
}

View File

@@ -1,159 +0,0 @@
/*
* Copyright (C) 2015-2017 Á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.core.models;
import org.isoron.platform.time.LocalDate;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
import static java.util.Calendar.*;
public final class Timestamp implements Comparable<Timestamp> {
public static final long DAY_LENGTH = 86400000;
public static final Timestamp ZERO = new Timestamp(0);
private final long unixTime;
public static Timestamp fromLocalDate(LocalDate date) {
return new Timestamp(946684800000L + date.getDaysSince2000() * 86400000L);
}
public Timestamp(long unixTime) {
if (unixTime < 0)
throw new IllegalArgumentException(
"Invalid unix time: " + unixTime);
if (unixTime % DAY_LENGTH != 0)
unixTime = (unixTime / DAY_LENGTH) * DAY_LENGTH;
this.unixTime = unixTime;
}
public Timestamp(GregorianCalendar cal) {
this(cal.getTimeInMillis());
}
public static Timestamp from(int year, int javaMonth, int day) {
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
cal.set(year, javaMonth, day, 0, 0, 0);
return new Timestamp(cal.getTimeInMillis());
}
public long getUnixTime() {
return unixTime;
}
public LocalDate toLocalDate() {
long millisSince2000 = unixTime - 946684800000L;
int daysSince2000 = (int) (millisSince2000 / 86400000);
return new LocalDate(daysSince2000);
}
/**
* Returns -1 if this timestamp is older than the given timestamp, 1 if this
* timestamp is newer, or zero if they are equal.
*/
@Override
public int compareTo(Timestamp other) {
return Long.signum(this.unixTime - other.unixTime);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Timestamp timestamp = (Timestamp) o;
return new EqualsBuilder()
.append(unixTime, timestamp.unixTime)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder(17, 37).append(unixTime).toHashCode();
}
/**
* Given two timestamps, returns whichever timestamp is the oldest one.
*/
public static Timestamp oldest(Timestamp first, Timestamp second) {
return first.unixTime < second.unixTime ? first : second;
}
public Timestamp minus(int days) {
return plus(-days);
}
public Timestamp plus(int days) {
return new Timestamp(unixTime + DAY_LENGTH * days);
}
/**
* Returns the number of days between this timestamp and the given one. If
* the other timestamp equals this one, returns zero. If the other timestamp
* is older than this one, returns a negative number.
*/
public int daysUntil(Timestamp other) {
return (int) ((other.unixTime - this.unixTime) / DAY_LENGTH);
}
public boolean isNewerThan(Timestamp other) {
return compareTo(other) > 0;
}
public boolean isOlderThan(Timestamp other) {
return compareTo(other) < 0;
}
public Date toJavaDate() {
return new Date(unixTime);
}
public GregorianCalendar toCalendar() {
GregorianCalendar day =
new GregorianCalendar(TimeZone.getTimeZone("GMT"));
day.setTimeInMillis(unixTime);
return day;
}
@Override
public String toString() {
return DateFormats.getCSVDateFormat().format(new Date(unixTime));
}
/**
* Returns an integer corresponding to the day of the week. Saturday maps
* to 0, Sunday maps to 1, and so on.
*/
public int getWeekday() {
return toCalendar().get(DAY_OF_WEEK) % 7;
}
Timestamp truncate(DateUtils.TruncateField field, int firstWeekday) {
return new Timestamp(DateUtils.truncate(field, unixTime, firstWeekday));
}
}

View File

@@ -1,101 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models;
import org.apache.commons.lang3.builder.*;
import java.util.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
public final class WeekdayList
{
public static final WeekdayList EVERY_DAY = new WeekdayList(127);
private final boolean[] weekdays;
public WeekdayList(int packedList)
{
weekdays = new boolean[7];
int current = 1;
for (int i = 0; i < 7; i++)
{
if ((packedList & current) != 0) weekdays[i] = true;
current = current << 1;
}
}
public WeekdayList(boolean weekdays[])
{
this.weekdays = Arrays.copyOf(weekdays, 7);
}
public boolean isEmpty()
{
for (boolean d : weekdays) if (d) return false;
return true;
}
public boolean[] toArray()
{
return Arrays.copyOf(weekdays, 7);
}
public int toInteger()
{
int packedList = 0;
int current = 1;
for (int i = 0; i < 7; i++)
{
if (weekdays[i]) packedList |= current;
current = current << 1;
}
return packedList;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WeekdayList that = (WeekdayList) o;
return new EqualsBuilder().append(weekdays, that.weekdays).isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37).append(weekdays).toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("weekdays", weekdays)
.toString();
}
}

View File

@@ -1,294 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models.memory;
import androidx.annotation.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
import static org.isoron.uhabits.core.models.HabitList.Order.*;
/**
* In-memory implementation of {@link HabitList}.
*/
public class MemoryHabitList extends HabitList
{
@NonNull
private LinkedList<Habit> list = new LinkedList<>();
@NonNull
private Order primaryOrder = Order.BY_POSITION;
@NonNull
private Order secondaryOrder = Order.BY_NAME_ASC;
private Comparator<Habit> comparator = getComposedComparatorByOrder(primaryOrder, secondaryOrder);
@Nullable
private MemoryHabitList parent = null;
public MemoryHabitList()
{
super();
}
protected MemoryHabitList(@NonNull HabitMatcher matcher,
Comparator<Habit> comparator,
@NonNull MemoryHabitList parent)
{
super(matcher);
this.parent = parent;
this.comparator = comparator;
this.primaryOrder = parent.primaryOrder;
this.secondaryOrder = parent.secondaryOrder;
parent.getObservable().addListener(this::loadFromParent);
loadFromParent();
}
@Override
public synchronized void add(@NonNull Habit habit)
throws IllegalArgumentException
{
throwIfHasParent();
if (list.contains(habit))
throw new IllegalArgumentException("habit already added");
Long id = habit.getId();
if (id != null && getById(id) != null)
throw new RuntimeException("duplicate id");
if (id == null) habit.setId((long) list.size());
list.addLast(habit);
resort();
}
@Override
public synchronized Habit getById(long id)
{
for (Habit h : list)
{
if (h.getId() == null) throw new IllegalStateException();
if (h.getId() == id) return h;
}
return null;
}
@Override
public synchronized Habit getByUUID(String uuid)
{
for (Habit h : list) if (h.getUuid().equals(uuid)) return h;
return null;
}
@NonNull
@Override
public synchronized Habit getByPosition(int position)
{
return list.get(position);
}
@NonNull
@Override
public synchronized HabitList getFiltered(HabitMatcher matcher)
{
return new MemoryHabitList(matcher, comparator, this);
}
@Override
public synchronized Order getPrimaryOrder()
{
return primaryOrder;
}
@Override
public synchronized Order getSecondaryOrder()
{
return secondaryOrder;
}
@Override
public synchronized void setPrimaryOrder(@NonNull Order order)
{
this.primaryOrder = order;
this.comparator = getComposedComparatorByOrder(this.primaryOrder, this.secondaryOrder);
resort();
}
@Override
public void setSecondaryOrder(@NonNull Order order)
{
this.secondaryOrder = order;
this.comparator = getComposedComparatorByOrder(this.primaryOrder, this.secondaryOrder);
resort();
}
private Comparator<Habit> getComposedComparatorByOrder(Order firstOrder, Order secondOrder)
{
return (h1, h2) -> {
int firstResult = getComparatorByOrder(firstOrder).compare(h1, h2);
if (firstResult != 0 || secondOrder == null) {
return firstResult;
}
return getComparatorByOrder(secondOrder).compare(h1, h2);
};
}
private Comparator<Habit> getComparatorByOrder(Order order) {
Comparator<Habit> nameComparatorAsc = (h1, h2) ->
h1.getName().compareTo(h2.getName());
Comparator<Habit> nameComparatorDesc = (h1, h2) ->
nameComparatorAsc.compare(h2, h1);
Comparator<Habit> colorComparatorAsc = (h1, h2) ->
h1.getColor().compareTo(h2.getColor());
Comparator<Habit> colorComparatorDesc = (h1, h2) ->
colorComparatorAsc.compare(h2, h1);
Comparator<Habit> scoreComparatorDesc = (h1, h2) ->
{
Timestamp today = DateUtils.getTodayWithOffset();
return Double.compare(
h1.getScores().get(today).getValue(),
h2.getScores().get(today).getValue());
};
Comparator<Habit> scoreComparatorAsc = (h1, h2) ->
scoreComparatorDesc.compare(h2, h1);
Comparator<Habit> positionComparator = (h1, h2) ->
Integer.compare(h1.getPosition(), h2.getPosition());
Comparator<Habit> statusComparatorDesc = (h1, h2) ->
{
if (h1.isCompletedToday() != h2.isCompletedToday()) {
return h1.isCompletedToday() ? -1 : 1;
}
if (h1.isNumerical() != h2.isNumerical()) {
return h1.isNumerical() ? -1 : 1;
}
Timestamp today = DateUtils.getTodayWithOffset();
Integer v1 = h1.getComputedEntries().get(today).getValue();
Integer v2 = h2.getComputedEntries().get(today).getValue();
return v2.compareTo(v1);
};
Comparator<Habit> statusComparatorAsc = (h1, h2) -> statusComparatorDesc.compare(h2, h1);
if (order == BY_POSITION) return positionComparator;
if (order == BY_NAME_ASC) return nameComparatorAsc;
if (order == BY_NAME_DESC) return nameComparatorDesc;
if (order == BY_COLOR_ASC) return colorComparatorAsc;
if (order == BY_COLOR_DESC) return colorComparatorDesc;
if (order == BY_SCORE_DESC) return scoreComparatorDesc;
if (order == BY_SCORE_ASC) return scoreComparatorAsc;
if (order == BY_STATUS_DESC) return statusComparatorDesc;
if (order == BY_STATUS_ASC) return statusComparatorAsc;
throw new IllegalStateException();
}
@Override
public synchronized int indexOf(@NonNull Habit h)
{
return list.indexOf(h);
}
@NonNull
@Override
public synchronized Iterator<Habit> iterator()
{
return new ArrayList<>(list).iterator();
}
@Override
public synchronized void remove(@NonNull Habit habit)
{
throwIfHasParent();
list.remove(habit);
getObservable().notifyListeners();
}
@Override
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
{
throwIfHasParent();
if (primaryOrder != BY_POSITION) throw new IllegalStateException(
"cannot reorder automatically sorted list");
if (indexOf(from) < 0) throw new IllegalArgumentException(
"list does not contain (from) habit");
int toPos = indexOf(to);
if (toPos < 0) throw new IllegalArgumentException(
"list does not contain (to) habit");
list.remove(from);
list.add(toPos, from);
int position = 0;
for(Habit h : list)
h.setPosition(position++);
getObservable().notifyListeners();
}
@Override
public synchronized int size()
{
return list.size();
}
@Override
public synchronized void update(List<Habit> habits)
{
resort();
}
private void throwIfHasParent()
{
if (parent != null) throw new IllegalStateException(
"Filtered lists cannot be modified directly. " +
"You should modify the parent list instead.");
}
private synchronized void loadFromParent()
{
if (parent == null) throw new IllegalStateException();
list.clear();
for (Habit h : parent) if (filter.matches(h)) list.add(h);
resort();
}
public synchronized void resort()
{
if (comparator != null) Collections.sort(list, comparator);
getObservable().notifyListeners();
}
}

View File

@@ -1,34 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.models.memory
import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.ScoreList
import org.isoron.uhabits.core.models.StreakList
class MemoryModelFactory : ModelFactory {
override fun buildComputedEntries() = EntryList()
override fun buildOriginalEntries() = EntryList()
override fun buildHabitList() = MemoryHabitList()
override fun buildScoreList() = ScoreList()
override fun buildStreakList() = StreakList()
override fun buildHabitListRepository() = throw NotImplementedError()
override fun buildRepetitionListRepository() = throw NotImplementedError()
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright (C) 2016 Á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/>.
*/
/**
* Provides in-memory implementation of core models.
*/
package org.isoron.uhabits.core.models.memory;

View File

@@ -1,51 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite
import org.isoron.uhabits.core.database.Database
import org.isoron.uhabits.core.database.Repository
import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.ScoreList
import org.isoron.uhabits.core.models.StreakList
import org.isoron.uhabits.core.models.sqlite.records.EntryRecord
import org.isoron.uhabits.core.models.sqlite.records.HabitRecord
import javax.inject.Inject
/**
* Factory that provides models backed by an SQLite database.
*/
class SQLModelFactory
@Inject constructor(
val database: Database,
) : ModelFactory {
override fun buildOriginalEntries() = SQLiteEntryList(database)
override fun buildComputedEntries() = EntryList()
override fun buildHabitList() = SQLiteHabitList(this)
override fun buildScoreList() = ScoreList()
override fun buildStreakList() = StreakList()
override fun buildHabitListRepository() =
Repository(HabitRecord::class.java, database)
override fun buildRepetitionListRepository() =
Repository(EntryRecord::class.java, database)
}

View File

@@ -1,92 +0,0 @@
/*
* Copyright (C) 2016-2020 Á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.core.models.sqlite
import org.isoron.uhabits.core.database.Database
import org.isoron.uhabits.core.database.Repository
import org.isoron.uhabits.core.models.Entry
import org.isoron.uhabits.core.models.EntryList
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.models.sqlite.records.EntryRecord
class SQLiteEntryList(database: Database) : EntryList() {
val repository = Repository(EntryRecord::class.java, database)
var habitId: Long? = null
var isLoaded = false
private fun loadRecords() {
if (isLoaded) return
val habitId = habitId ?: throw IllegalStateException("habitId must be set")
val records = repository.findAll(
"where habit = ? order by timestamp",
habitId.toString()
)
for (rec in records) super.add(rec.toEntry())
isLoaded = true
}
override fun get(timestamp: Timestamp): Entry {
loadRecords()
return super.get(timestamp)
}
override fun getByInterval(from: Timestamp, to: Timestamp): List<Entry> {
loadRecords()
return super.getByInterval(from, to)
}
override fun add(entry: Entry) {
loadRecords()
val habitId = habitId ?: throw IllegalStateException("habitId must be set")
// Remove existing rows
repository.execSQL(
"delete from repetitions where habit = ? and timestamp = ?",
habitId.toString(),
entry.timestamp.unixTime.toString()
)
// Add new row
val record = EntryRecord().apply { copyFrom(entry) }
record.habitId = habitId
repository.save(record)
// Add to memory list
super.add(entry)
}
override fun getKnown(): List<Entry> {
loadRecords()
return super.getKnown()
}
override fun recomputeFrom(originalEntries: EntryList, frequency: Frequency, isNumerical: Boolean) {
throw UnsupportedOperationException()
}
override fun clear() {
super.clear()
repository.execSQL(
"delete from repetitions where habit = ?",
habitId.toString()
)
}
}

View File

@@ -1,296 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite;
import androidx.annotation.*;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.models.memory.*;
import org.isoron.uhabits.core.models.sqlite.records.*;
import java.util.*;
import javax.inject.*;
/**
* Implementation of a {@link HabitList} that is backed by SQLite.
*/
public class SQLiteHabitList extends HabitList
{
@NonNull
private final Repository<HabitRecord> repository;
@NonNull
private final ModelFactory modelFactory;
@NonNull
private final MemoryHabitList list;
private boolean loaded = false;
@Inject
public SQLiteHabitList(@NonNull ModelFactory modelFactory)
{
super();
this.modelFactory = modelFactory;
this.list = new MemoryHabitList();
this.repository = modelFactory.buildHabitListRepository();
}
private void loadRecords()
{
if (loaded) return;
loaded = true;
list.removeAll();
List<HabitRecord> records = repository.findAll("order by position");
int expectedPosition = 0;
boolean shouldRebuildOrder = false;
for (HabitRecord rec : records)
{
if (rec.position != expectedPosition) shouldRebuildOrder = true;
expectedPosition++;
Habit h = modelFactory.buildHabit();
rec.copyTo(h);
((SQLiteEntryList) h.getOriginalEntries()).setHabitId(h.getId());
list.add(h);
}
if(shouldRebuildOrder) rebuildOrder();
}
@Override
public synchronized void add(@NonNull Habit habit)
{
loadRecords();
habit.setPosition(size());
HabitRecord record = new HabitRecord();
record.copyFrom(habit);
repository.save(record);
habit.setId(record.id);
((SQLiteEntryList) habit.getOriginalEntries()).setHabitId(record.id);
list.add(habit);
getObservable().notifyListeners();
}
@Override
@Nullable
public synchronized Habit getById(long id)
{
loadRecords();
return list.getById(id);
}
@Override
@Nullable
public synchronized Habit getByUUID(String uuid)
{
loadRecords();
return list.getByUUID(uuid);
}
@Override
@NonNull
public synchronized Habit getByPosition(int position)
{
loadRecords();
return list.getByPosition(position);
}
@NonNull
@Override
public synchronized HabitList getFiltered(HabitMatcher filter)
{
loadRecords();
return list.getFiltered(filter);
}
@Override
@NonNull
public Order getPrimaryOrder()
{
return list.getPrimaryOrder();
}
@Override
public Order getSecondaryOrder()
{
return list.getSecondaryOrder();
}
@Override
public synchronized void setPrimaryOrder(@NonNull Order order)
{
list.setPrimaryOrder(order);
getObservable().notifyListeners();
}
@Override
public synchronized void setSecondaryOrder(@NonNull Order order)
{
list.setSecondaryOrder(order);
getObservable().notifyListeners();
}
@Override
public synchronized int indexOf(@NonNull Habit h)
{
loadRecords();
return list.indexOf(h);
}
@Override
public synchronized Iterator<Habit> iterator()
{
loadRecords();
return list.iterator();
}
private synchronized void rebuildOrder()
{
List<HabitRecord> records = repository.findAll("order by position");
repository.executeAsTransaction(() ->
{
int pos = 0;
for (HabitRecord r : records)
{
if (r.position != pos)
{
r.position = pos;
repository.save(r);
}
pos++;
}
});
}
@Override
public synchronized void remove(@NonNull Habit habit)
{
loadRecords();
reorder(habit, list.getByPosition(size() - 1));
list.remove(habit);
HabitRecord record = repository.find(habit.getId());
if (record == null) throw new RuntimeException("habit not in database");
repository.executeAsTransaction(() ->
{
habit.getOriginalEntries().clear();
repository.remove(record);
});
getObservable().notifyListeners();
}
@Override
public synchronized void removeAll()
{
list.removeAll();
repository.execSQL("delete from habits");
repository.execSQL("delete from repetitions");
getObservable().notifyListeners();
}
@Override
public synchronized void reorder(@NonNull Habit from, @NonNull Habit to)
{
loadRecords();
list.reorder(from, to);
HabitRecord fromRecord = repository.find(from.getId());
HabitRecord toRecord = repository.find(to.getId());
if (fromRecord == null)
throw new RuntimeException("habit not in database");
if (toRecord == null)
throw new RuntimeException("habit not in database");
if (toRecord.position < fromRecord.position)
{
repository.execSQL("update habits set position = position + 1 " +
"where position >= ? and position < ?",
toRecord.position, fromRecord.position);
}
else
{
repository.execSQL("update habits set position = position - 1 " +
"where position > ? and position <= ?",
fromRecord.position, toRecord.position);
}
fromRecord.position = toRecord.position;
repository.save(fromRecord);
getObservable().notifyListeners();
}
@Override
public synchronized void repair()
{
loadRecords();
rebuildOrder();
getObservable().notifyListeners();
}
@Override
public synchronized int size()
{
loadRecords();
return list.size();
}
@Override
public synchronized void update(List<Habit> habits)
{
loadRecords();
list.update(habits);
for (Habit h : habits)
{
HabitRecord record = repository.find(h.getId());
if (record == null) continue;
record.copyFrom(h);
repository.save(record);
}
getObservable().notifyListeners();
}
@Override
public void resort()
{
list.resort();
getObservable().notifyListeners();
}
public synchronized void reload()
{
loaded = false;
}
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright (C) 2017 Á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/>.
*
*
*/
/**
* Provides SQLite implementations of the core models.
*/
package org.isoron.uhabits.core.models.sqlite;

View File

@@ -1,57 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite.records;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
/**
* The SQLite database record corresponding to a {@link Entry}.
*/
@Table(name = "Repetitions")
public class EntryRecord
{
public HabitRecord habit;
@Column(name = "habit")
public Long habitId;
@Column
public Long timestamp;
@Column
public Integer value;
@Column
public Long id;
public void copyFrom(Entry entry)
{
timestamp = entry.getTimestamp().getUnixTime();
value = entry.getValue();
}
public Entry toEntry()
{
return new Entry(new Timestamp(timestamp), value);
}
}

View File

@@ -1,141 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.models.sqlite.records;
import org.isoron.uhabits.core.database.*;
import org.isoron.uhabits.core.models.*;
/**
* The SQLite database record corresponding to a {@link Habit}.
*/
@Table(name = "habits")
public class HabitRecord
{
@Column
public String description;
@Column
public String question;
@Column
public String name;
@Column(name = "freq_num")
public Integer freqNum;
@Column(name = "freq_den")
public Integer freqDen;
@Column
public Integer color;
@Column
public Integer position;
@Column(name = "reminder_hour")
public Integer reminderHour;
@Column(name = "reminder_min")
public Integer reminderMin;
@Column(name = "reminder_days")
public Integer reminderDays;
@Column
public Integer highlight;
@Column
public Integer archived;
@Column
public Integer type;
@Column(name = "target_value")
public Double targetValue;
@Column(name = "target_type")
public Integer targetType;
@Column
public String unit;
@Column
public Long id;
@Column
public String uuid;
public void copyFrom(Habit model)
{
this.id = model.getId();
this.name = model.getName();
this.description = model.getDescription();
this.highlight = 0;
this.color = model.getColor().getPaletteIndex();
this.archived = model.isArchived() ? 1 : 0;
this.type = model.getType();
this.targetType = model.getTargetType();
this.targetValue = model.getTargetValue();
this.unit = model.getUnit();
this.position = model.getPosition();
this.question = model.getQuestion();
this.uuid = model.getUuid();
Frequency freq = model.getFrequency();
this.freqNum = freq.getNumerator();
this.freqDen = freq.getDenominator();
this.reminderDays = 0;
this.reminderMin = null;
this.reminderHour = null;
if (model.hasReminder())
{
Reminder reminder = model.getReminder();
this.reminderHour = reminder.getHour();
this.reminderMin = reminder.getMinute();
this.reminderDays = reminder.getDays().toInteger();
}
}
public void copyTo(Habit habit)
{
habit.setId(this.id);
habit.setName(this.name);
habit.setDescription(this.description);
habit.setQuestion(this.question);
habit.setFrequency(new Frequency(this.freqNum, this.freqDen));
habit.setColor(new PaletteColor(this.color));
habit.setArchived(this.archived != 0);
habit.setType(this.type);
habit.setTargetType(this.targetType);
habit.setTargetValue(this.targetValue);
habit.setUnit(this.unit);
habit.setPosition(this.position);
habit.setUuid(this.uuid);
if (reminderHour != null && reminderMin != null)
{
habit.setReminder(new Reminder(reminderHour, reminderMin,
new WeekdayList(reminderDays)));
}
}
}

View File

@@ -1,439 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.preferences;
import androidx.annotation.*;
import org.isoron.platform.time.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.ui.*;
import org.isoron.uhabits.core.utils.*;
import java.util.*;
public class Preferences
{
@NonNull
private final Storage storage;
@NonNull
private List<Listener> listeners;
@Nullable
private Boolean shouldReverseCheckmarks = null;
public Preferences(@NonNull Storage storage)
{
this.storage = storage;
listeners = new LinkedList<>();
storage.onAttached(this);
}
public void addListener(Listener listener)
{
listeners.add(listener);
}
public Integer getDefaultHabitColor(int fallbackColor)
{
return storage.getInt("pref_default_habit_palette_color",
fallbackColor);
}
public HabitList.Order getDefaultPrimaryOrder()
{
String name = storage.getString("pref_default_order", "BY_POSITION");
try
{
return HabitList.Order.valueOf(name);
}
catch (IllegalArgumentException e)
{
setDefaultPrimaryOrder(HabitList.Order.BY_POSITION);
return HabitList.Order.BY_POSITION;
}
}
public HabitList.Order getDefaultSecondaryOrder() {
String name = storage.getString("pref_default_secondary_order", "BY_NAME_ASC");
try
{
return HabitList.Order.valueOf(name);
}
catch (IllegalArgumentException e)
{
setDefaultSecondaryOrder(HabitList.Order.BY_NAME_ASC);
return HabitList.Order.BY_POSITION;
}
}
public void setDefaultPrimaryOrder(HabitList.Order order)
{
storage.putString("pref_default_order", order.name());
}
public void setDefaultSecondaryOrder(HabitList.Order order)
{
storage.putString("pref_default_secondary_order", order.name());
}
public int getScoreCardSpinnerPosition()
{
return Math.min(4, Math.max(0, storage.getInt("pref_score_view_interval", 1)));
}
public void setScoreCardSpinnerPosition(int position)
{
storage.putInt("pref_score_view_interval", position);
}
public int getBarCardBoolSpinnerPosition()
{
return Math.min(3, Math.max(0, storage.getInt("pref_bar_card_bool_spinner", 0)));
}
public void setBarCardBoolSpinnerPosition(int position)
{
storage.putInt("pref_bar_card_bool_spinner", position);
}
public int getBarCardNumericalSpinnerPosition()
{
return Math.min(4, Math.max(0, storage.getInt("pref_bar_card_numerical_spinner", 0)));
}
public void setBarCardNumericalSpinnerPosition(int position)
{
storage.putInt("pref_bar_card_numerical_spinner", position);
}
public int getLastHintNumber()
{
return storage.getInt("last_hint_number", -1);
}
public Timestamp getLastHintTimestamp()
{
long unixTime = storage.getLong("last_hint_timestamp", -1);
if (unixTime < 0) return null;
else return new Timestamp(unixTime);
}
public boolean getShowArchived()
{
return storage.getBoolean("pref_show_archived", false);
}
public void setShowArchived(boolean showArchived)
{
storage.putBoolean("pref_show_archived", showArchived);
}
public boolean getShowCompleted()
{
return storage.getBoolean("pref_show_completed", true);
}
public void setShowCompleted(boolean showCompleted)
{
storage.putBoolean("pref_show_completed", showCompleted);
}
public long getSnoozeInterval()
{
return Long.parseLong(storage.getString("pref_snooze_interval", "15"));
}
public void setSnoozeInterval(int interval)
{
storage.putString("pref_snooze_interval", String.valueOf(interval));
}
public int getTheme()
{
return storage.getInt("pref_theme", ThemeSwitcher.THEME_AUTOMATIC);
}
public void setTheme(int theme)
{
storage.putInt("pref_theme", theme);
}
public void incrementLaunchCount()
{
storage.putInt("launch_count", getLaunchCount() + 1);
}
public int getLaunchCount()
{
return storage.getInt("launch_count", 0);
}
public boolean isDeveloper()
{
return storage.getBoolean("pref_developer", false);
}
public void setDeveloper(boolean isDeveloper)
{
storage.putBoolean("pref_developer", isDeveloper);
}
public boolean isFirstRun()
{
return storage.getBoolean("pref_first_run", true);
}
public void setFirstRun(boolean isFirstRun)
{
storage.putBoolean("pref_first_run", isFirstRun);
}
public boolean isPureBlackEnabled()
{
return storage.getBoolean("pref_pure_black", false);
}
public void setPureBlackEnabled(boolean enabled)
{
storage.putBoolean("pref_pure_black", enabled);
}
public boolean isShortToggleEnabled()
{
return storage.getBoolean("pref_short_toggle", false);
}
public void setShortToggleEnabled(boolean enabled)
{
storage.putBoolean("pref_short_toggle", enabled);
}
public void removeListener(Listener listener)
{
listeners.remove(listener);
}
public void clear()
{
storage.clear();
}
public void setDefaultHabitColor(int color)
{
storage.putInt("pref_default_habit_palette_color", color);
}
public void setNotificationsSticky(boolean sticky)
{
storage.putBoolean("pref_sticky_notifications", sticky);
for (Listener l : listeners) l.onNotificationsChanged();
}
public void setNotificationsLed(boolean enabled)
{
storage.putBoolean("pref_led_notifications", enabled);
for (Listener l : listeners) l.onNotificationsChanged();
}
public boolean shouldMakeNotificationsSticky()
{
return storage.getBoolean("pref_sticky_notifications", false);
}
public boolean shouldMakeNotificationsLed()
{
return storage.getBoolean("pref_led_notifications", false);
}
public boolean isCheckmarkSequenceReversed()
{
if (shouldReverseCheckmarks == null) shouldReverseCheckmarks =
storage.getBoolean("pref_checkmark_reverse_order", false);
return shouldReverseCheckmarks;
}
public void setCheckmarkSequenceReversed(boolean reverse)
{
shouldReverseCheckmarks = reverse;
storage.putBoolean("pref_checkmark_reverse_order", reverse);
for (Listener l : listeners) l.onCheckmarkSequenceChanged();
}
public void updateLastHint(int number, Timestamp timestamp)
{
storage.putInt("last_hint_number", number);
storage.putLong("last_hint_timestamp", timestamp.getUnixTime());
}
public int getLastAppVersion()
{
return storage.getInt("last_version", 0);
}
public void setLastAppVersion(int version)
{
storage.putInt("last_version", version);
}
public int getWidgetOpacity()
{
return Integer.parseInt(storage.getString("pref_widget_opacity", "255"));
}
public void setWidgetOpacity(int value)
{
storage.putString("pref_widget_opacity", Integer.toString(value));
}
public boolean isSkipEnabled()
{
return storage.getBoolean("pref_skip_enabled", false);
}
public void setSkipEnabled(boolean value)
{
storage.putBoolean("pref_skip_enabled", value);
}
public String getSyncBaseURL()
{
return storage.getString("pref_sync_base_url", "");
}
public String getSyncKey()
{
return storage.getString("pref_sync_key", "");
}
public String getEncryptionKey()
{
return storage.getString("pref_encryption_key", "");
}
public boolean isSyncEnabled()
{
return storage.getBoolean("pref_sync_enabled", false);
}
public void enableSync(String syncKey, String encKey)
{
storage.putBoolean("pref_sync_enabled", true);
storage.putString("pref_sync_key", syncKey);
storage.putString("pref_encryption_key", encKey);
for (Listener l : listeners) l.onSyncEnabled();
}
public void disableSync()
{
storage.putBoolean("pref_sync_enabled", false);
storage.putString("pref_sync_key", "");
storage.putString("pref_encryption_key", "");
}
public boolean areQuestionMarksEnabled()
{
return storage.getBoolean("pref_unknown_enabled", false);
}
/**
* @return An integer representing the first day of the week. Sunday
* corresponds to 1, Monday to 2, and so on, until Saturday, which is
* represented by 7. By default, this is based on the current system locale,
* unless the user changed this in the settings.
*/
@Deprecated()
public int getFirstWeekdayInt()
{
String weekday = storage.getString("pref_first_weekday", "");
if (weekday.isEmpty()) return DateUtils.getFirstWeekdayNumberAccordingToLocale();
return Integer.parseInt(weekday);
}
public DayOfWeek getFirstWeekday()
{
int weekday = Integer.parseInt(storage.getString("pref_first_weekday", "-1"));
if (weekday < 0) weekday = DateUtils.getFirstWeekdayNumberAccordingToLocale();
switch (weekday) {
case 1: return DayOfWeek.SUNDAY;
case 2: return DayOfWeek.MONDAY;
case 3: return DayOfWeek.TUESDAY;
case 4: return DayOfWeek.WEDNESDAY;
case 5: return DayOfWeek.THURSDAY;
case 6: return DayOfWeek.FRIDAY;
case 7: return DayOfWeek.SATURDAY;
default: throw new IllegalArgumentException();
}
}
public interface Listener
{
default void onCheckmarkSequenceChanged()
{
}
default void onNotificationsChanged()
{
}
default void onSyncEnabled()
{
}
}
public interface Storage
{
void clear();
boolean getBoolean(String key, boolean defValue);
int getInt(String key, int defValue);
long getLong(String key, long defValue);
String getString(String key, String defValue);
void onAttached(Preferences preferences);
void putBoolean(String key, boolean value);
void putInt(String key, int value);
void putLong(String key, long value);
void putString(String key, String value);
void remove(String key);
default void putLongArray(String key, long[] values)
{
putString(key, StringUtils.joinLongs(values));
}
default long[] getLongArray(String key, long[] defValue)
{
String string = getString(key, "");
if (string.isEmpty()) return defValue;
else return StringUtils.splitLongs(string);
}
}
}

View File

@@ -1,134 +0,0 @@
/*
* Copyright (C) 2015-2017 Á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.core.preferences;
import androidx.annotation.*;
import java.io.*;
import java.util.*;
public class PropertiesStorage implements Preferences.Storage
{
@NonNull
private final Properties props;
@NonNull
private File file;
public PropertiesStorage(@NonNull File file)
{
try
{
this.file = file;
props = new Properties();
props.load(new FileInputStream(file));
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
@Override
public void clear()
{
for(String key : props.stringPropertyNames()) props.remove(key);
flush();
}
@Override
public boolean getBoolean(String key, boolean defValue)
{
String value = props.getProperty(key, Boolean.toString(defValue));
return Boolean.parseBoolean(value);
}
@Override
public int getInt(String key, int defValue)
{
String value = props.getProperty(key, Integer.toString(defValue));
return Integer.parseInt(value);
}
@Override
public long getLong(String key, long defValue)
{
String value = props.getProperty(key, Long.toString(defValue));
return Long.parseLong(value);
}
@Override
public String getString(String key, String defValue)
{
return props.getProperty(key, defValue);
}
@Override
public void onAttached(Preferences preferences)
{
// nop
}
@Override
public void putBoolean(String key, boolean value)
{
props.setProperty(key, Boolean.toString(value));
}
@Override
public void putInt(String key, int value)
{
props.setProperty(key, Integer.toString(value));
flush();
}
private void flush()
{
try
{
props.store(new FileOutputStream(file), "");
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
@Override
public void putLong(String key, long value)
{
props.setProperty(key, Long.toString(value));
flush();
}
@Override
public void putString(String key, String value)
{
props.setProperty(key, value);
flush();
}
@Override
public void remove(String key)
{
props.remove(key);
flush();
}
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.preferences;
import org.isoron.uhabits.core.AppScope;
import org.isoron.uhabits.core.models.HabitNotFoundException;
import javax.inject.Inject;
@AppScope
public class WidgetPreferences {
private Preferences.Storage storage;
@Inject
public WidgetPreferences(Preferences.Storage storage) {
this.storage = storage;
}
public void addWidget(int widgetId, long habitIds[]) {
storage.putLongArray(getHabitIdKey(widgetId), habitIds);
}
public long[] getHabitIdsFromWidgetId(int widgetId) {
long[] habitIds;
String habitIdKey = getHabitIdKey(widgetId);
try {
habitIds = storage.getLongArray(habitIdKey, new long[]{-1});
} catch (ClassCastException e) {
// Up to Loop 1.7.11, this preference was not an array, but a single
// long. Trying to read the old preference causes a cast exception.
habitIds = new long[1];
habitIds[0] = storage.getLong(habitIdKey, -1);
storage.putLongArray(habitIdKey, habitIds);
}
return habitIds;
}
public void removeWidget(int id) {
String habitIdKey = getHabitIdKey(id);
storage.remove(habitIdKey);
}
public long getSnoozeTime(long id)
{
return storage.getLong(getSnoozeKey(id), 0);
}
private String getHabitIdKey(int id) {
return String.format("widget-%06d-habit", id);
}
private String getSnoozeKey(long id)
{
return String.format("snooze-%06d", id);
}
public void removeSnoozeTime(long id)
{
storage.putLong(getSnoozeKey(id), 0);
}
public void setSnoozeTime(Long id, long time)
{
storage.putLong(getSnoozeKey(id), time);
}
}

View File

@@ -1,176 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.reminders;
import androidx.annotation.*;
import org.isoron.uhabits.core.*;
import org.isoron.uhabits.core.commands.*;
import org.isoron.uhabits.core.models.*;
import org.isoron.uhabits.core.preferences.*;
import java.util.*;
import javax.inject.*;
import static org.isoron.uhabits.core.utils.DateUtils.*;
@AppScope
public class ReminderScheduler implements CommandRunner.Listener
{
private final WidgetPreferences widgetPreferences;
private CommandRunner commandRunner;
private HabitList habitList;
private SystemScheduler sys;
@Inject
public ReminderScheduler(@NonNull CommandRunner commandRunner,
@NonNull HabitList habitList,
@NonNull SystemScheduler sys,
@NonNull WidgetPreferences widgetPreferences)
{
this.commandRunner = commandRunner;
this.habitList = habitList;
this.sys = sys;
this.widgetPreferences = widgetPreferences;
}
@Override
public synchronized void onCommandFinished(@Nullable Command command)
{
if (command instanceof CreateRepetitionCommand) return;
if (command instanceof ChangeHabitColorCommand) return;
scheduleAll();
}
public synchronized void schedule(@NonNull Habit habit)
{
if (habit.getId() == null)
{
sys.log("ReminderScheduler", "Habit has null id. Returning.");
return;
}
if (!habit.hasReminder())
{
sys.log("ReminderScheduler", "habit=" + habit.getId() + " has no reminder. Skipping.");
return;
}
long reminderTime = habit.getReminder().getTimeInMillis();
long snoozeReminderTime = widgetPreferences.getSnoozeTime(habit.getId());
if (snoozeReminderTime != 0)
{
long now = applyTimezone(getLocalTime());
sys.log("ReminderScheduler", String.format(
Locale.US,
"Habit %d has been snoozed until %d",
habit.getId(),
snoozeReminderTime));
if (snoozeReminderTime > now)
{
sys.log("ReminderScheduler", "Snooze time is in the future. Accepting.");
reminderTime = snoozeReminderTime;
}
else
{
sys.log("ReminderScheduler", "Snooze time is in the past. Discarding.");
widgetPreferences.removeSnoozeTime(habit.getId());
}
}
scheduleAtTime(habit, reminderTime);
}
public synchronized void scheduleAtTime(@NonNull Habit habit, long reminderTime)
{
sys.log("ReminderScheduler", "Scheduling alarm for habit=" + habit.getId());
if (!habit.hasReminder())
{
sys.log("ReminderScheduler", "habit=" + habit.getId() + " has no reminder. Skipping.");
return;
}
if (habit.isArchived())
{
sys.log("ReminderScheduler", "habit=" + habit.getId() + " is archived. Skipping.");
return;
}
long timestamp = getStartOfDayWithOffset(removeTimezone(reminderTime));
sys.log("ReminderScheduler",
String.format(
Locale.US,
"reminderTime=%d removeTimezone=%d timestamp=%d",
reminderTime,
removeTimezone(reminderTime),
timestamp));
sys.scheduleShowReminder(reminderTime, habit, timestamp);
}
public synchronized void scheduleAll()
{
sys.log("ReminderScheduler", "Scheduling all alarms");
HabitList reminderHabits =
habitList.getFiltered(HabitMatcher.WITH_ALARM);
for (Habit habit : reminderHabits)
schedule(habit);
}
public synchronized void startListening()
{
commandRunner.addListener(this);
}
public synchronized void stopListening()
{
commandRunner.removeListener(this);
}
public synchronized void snoozeReminder(Habit habit, long minutes)
{
long now = applyTimezone(getLocalTime());
long snoozedUntil = now + minutes * 60 * 1000;
widgetPreferences.setSnoozeTime(habit.getId(), snoozedUntil);
schedule(habit);
}
public interface SystemScheduler
{
SchedulerResult scheduleShowReminder(long reminderTime, Habit habit, long timestamp);
SchedulerResult scheduleWidgetUpdate(long updateTime);
void log(String componentName, String msg);
}
public enum SchedulerResult
{
IGNORED,
OK
}
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright (C) 2016-2020 Alinson Santos Xavier <git@axavier.org>
*
* 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.core.sync
interface AbstractSyncServer {
/**
* Generates and returns a new sync key, which can be used to store and retrive
* data.
*
* @throws ServiceUnavailable If key cannot be generated at this time, for example,
* due to insufficient server resources, temporary server maintenance or network problems.
*/
suspend fun register(): String
/**
* Replaces data for a given sync key.
*
* @throws KeyNotFoundException If key is not found
* @throws EditConflictException If the version of the data provided is not
* exactly the current data version plus one.
* @throws ServiceUnavailable If data cannot be put at this time, for example, due
* to insufficient server resources or network problems.
*/
suspend fun put(key: String, newData: SyncData)
/**
* Returns data for a given sync key.
*
* @throws KeyNotFoundException If key is not found
* @throws ServiceUnavailable If data cannot be retrieved at this time, for example, due
* to insufficient server resources or network problems.
*/
suspend fun getData(key: String): SyncData
/**
* Returns the current data version for the given key
*
* @throws KeyNotFoundException If key is not found
* @throws ServiceUnavailable If data cannot be retrieved at this time, for example, due
* to insufficient server resources or network problems.
*/
suspend fun getDataVersion(key: String): Long
}

View File

@@ -1,142 +0,0 @@
/*
* Copyright (C) 2016-2020 Á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/>.
*/
@file:Suppress("UnstableApiUsage")
package org.isoron.uhabits.core.sync
import com.google.common.io.ByteStreams
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import org.apache.commons.codec.binary.Base64
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.nio.ByteBuffer
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
* Encryption key which can be used with [File.encryptToString], [String.decryptToFile],
* [ByteArray.encrypt] and [ByteArray.decrypt].
*
* To randomly generate a new key, use [EncryptionKey.generate]. To load a key from a
* Base64-encoded string, use [EncryptionKey.fromBase64].
*/
class EncryptionKey private constructor(
val base64: String,
val secretKey: SecretKey
) {
companion object {
fun fromBase64(base64: String): EncryptionKey {
val keySpec = SecretKeySpec(base64.decodeBase64(), "AES")
return EncryptionKey(base64, keySpec)
}
private fun fromSecretKey(spec: SecretKey): EncryptionKey {
val base64 = spec.encoded.encodeBase64().trim()
return EncryptionKey(base64, spec)
}
suspend fun generate(): EncryptionKey = Dispatchers.IO {
try {
val generator = KeyGenerator.getInstance("AES").apply { init(256) }
return@IO fromSecretKey(generator.generateKey())
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
}
/**
* Encrypts the byte stream using the provided symmetric encryption key.
*
* The initialization vector (16 bytes) is prepended to the cipher text. To decrypt the result, use
* [ByteArray.decrypt], providing the same key.
*/
fun ByteArray.encrypt(key: EncryptionKey): ByteArray {
val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
cipher.init(Cipher.ENCRYPT_MODE, key.secretKey)
val encrypted = cipher.doFinal(this)
return ByteBuffer
.allocate(16 + encrypted.size)
.put(cipher.iv)
.put(encrypted)
.array()
}
/**
* Decrypts a byte stream generated by [ByteArray.encrypt].
*/
fun ByteArray.decrypt(key: EncryptionKey): ByteArray {
val buffer = ByteBuffer.wrap(this)
val iv = ByteArray(16)
buffer.get(iv)
val encrypted = ByteArray(buffer.remaining())
buffer.get(encrypted)
val cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING")
cipher.init(Cipher.DECRYPT_MODE, key.secretKey, IvParameterSpec(iv))
return cipher.doFinal(encrypted)
}
/**
* Takes a string produced by [File.encryptToString], decodes it with Base64, decompresses it with
* gzip, decrypts it with the provided key, then writes the output to the specified file.
*/
fun String.decryptToFile(key: EncryptionKey, output: File) {
val bytes = this.decodeBase64().decrypt(key)
ByteArrayInputStream(bytes).use { bytesInputStream ->
GZIPInputStream(bytesInputStream).use { gzipInputStream ->
FileOutputStream(output).use { fileOutputStream ->
ByteStreams.copy(gzipInputStream, fileOutputStream)
}
}
}
}
/**
* Compresses the file with gzip, encrypts it using the the provided key, then returns a string
* containing the Base64-encoded cipher bytes.
*
* To decrypt and decompress the cipher text back into a file, use [String.decryptToFile].
*/
fun File.encryptToString(key: EncryptionKey): String {
ByteArrayOutputStream().use { bytesOutputStream ->
FileInputStream(this).use { inputStream ->
GZIPOutputStream(bytesOutputStream).use { gzipOutputStream ->
ByteStreams.copy(inputStream, gzipOutputStream)
gzipOutputStream.close()
val bytes = bytesOutputStream.toByteArray()
return bytes.encrypt(key).encodeBase64()
}
}
}
}
fun ByteArray.encodeBase64(): String = Base64.encodeBase64(this).decodeToString()
fun String.decodeBase64(): ByteArray = Base64.decodeBase64(this.toByteArray())

View File

@@ -1,29 +0,0 @@
/*
* Copyright (C) 2016-2020 Á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.core.sync
interface NetworkManager {
fun addListener(listener: Listener)
fun remoteListener(listener: Listener)
interface Listener {
fun onNetworkAvailable()
fun onNetworkLost()
}
}

View File

@@ -1,29 +0,0 @@
/*
* Copyright (C) 2016-2020 Alinson Santos Xavier <git@axavier.org>
*
* 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.core.sync
data class SyncData(
val version: Long,
val content: String
)
data class RegisterReponse(val key: String)
data class GetDataVersionResponse(val version: Long)

View File

@@ -1,28 +0,0 @@
/*
* Copyright (C) 2016-2020 Alinson Santos Xavier <git@axavier.org>
*
* 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.core.sync
open class SyncException : RuntimeException()
class KeyNotFoundException : SyncException()
class ServiceUnavailable : SyncException()
class EditConflictException : SyncException()

View File

@@ -1,183 +0,0 @@
/*
* Copyright (C) 2016-2020 Á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.core.sync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import org.isoron.uhabits.core.AppScope
import org.isoron.uhabits.core.commands.Command
import org.isoron.uhabits.core.commands.CommandRunner
import org.isoron.uhabits.core.database.Database
import org.isoron.uhabits.core.io.Logging
import org.isoron.uhabits.core.io.LoopDBImporter
import org.isoron.uhabits.core.preferences.Preferences
import java.io.File
import javax.inject.Inject
@AppScope
class SyncManager @Inject constructor(
val preferences: Preferences,
val commandRunner: CommandRunner,
val logging: Logging,
val networkManager: NetworkManager,
val server: AbstractSyncServer,
val db: Database,
val dbImporter: LoopDBImporter,
) : Preferences.Listener, CommandRunner.Listener, NetworkManager.Listener {
private var logger = logging.getLogger("SyncManager")
private var connected = false
private val tmpFile = File.createTempFile("import", "")
private var currVersion = 1L
private var dirty = true
private lateinit var encryptionKey: EncryptionKey
private lateinit var syncKey: String
init {
preferences.addListener(this)
commandRunner.addListener(this)
networkManager.addListener(this)
}
fun sync() = CoroutineScope(Dispatchers.Main).launch {
if (!preferences.isSyncEnabled) {
logger.info("Device sync is disabled. Skipping sync.")
return@launch
}
encryptionKey = EncryptionKey.fromBase64(preferences.encryptionKey)
syncKey = preferences.syncKey
logger.info("Starting sync (key: $syncKey)")
try {
pull()
push()
logger.info("Sync finished successfully.")
} catch (e: ConnectionLostException) {
logger.info("Network unavailable. Aborting sync.")
} catch (e: ServiceUnavailable) {
logger.info("Sync service unavailable. Aborting sync.")
} catch (e: Exception) {
logger.error("Unexpected sync exception. Disabling sync.")
logger.error(e)
preferences.disableSync()
}
}
private suspend fun push(depth: Int = 0) {
if (depth >= 5) {
throw RuntimeException()
}
if (!dirty) {
logger.info("Local database not modified. Skipping push.")
return
}
logger.info("Encrypting local database...")
val encryptedDB = db.file!!.encryptToString(encryptionKey)
val size = encryptedDB.length / 1024
try {
logger.info("Pushing local database (version $currVersion, $size KB)")
assertConnected()
server.put(preferences.syncKey, SyncData(currVersion, encryptedDB))
dirty = false
} catch (e: EditConflictException) {
logger.info("Sync conflict detected while pushing.")
setCurrentVersion(0)
pull()
push(depth = depth + 1)
}
}
private suspend fun pull() = Dispatchers.IO {
logger.info("Querying remote database version...")
assertConnected()
val remoteVersion = server.getDataVersion(syncKey)
logger.info("Remote database version: $remoteVersion")
if (remoteVersion <= currVersion) {
logger.info("Local database is up-to-date. Skipping merge.")
} else {
logger.info("Pulling remote database...")
assertConnected()
val data = server.getData(syncKey)
val size = data.content.length / 1024
logger.info("Pulled remote database (version ${data.version}, $size KB)")
logger.info("Decrypting remote database and merging with local changes...")
data.content.decryptToFile(encryptionKey, tmpFile)
try {
db.beginTransaction()
dbImporter.importHabitsFromFile(tmpFile)
db.setTransactionSuccessful()
} catch (e: Exception) {
logger.error("Failed to import database")
logger.error(e)
} finally {
db.endTransaction()
}
dirty = true
setCurrentVersion(data.version + 1)
}
}
fun onResume() = sync()
fun onPause() = sync()
override fun onSyncEnabled() {
logger.info("Sync enabled.")
setCurrentVersion(1)
dirty = true
sync()
}
override fun onNetworkAvailable() {
logger.info("Network available.")
connected = true
sync()
}
override fun onNetworkLost() {
logger.info("Network unavailable.")
connected = false
}
override fun onCommandFinished(command: Command) {
if (!dirty) setCurrentVersion(currVersion + 1)
dirty = true
}
private fun assertConnected() {
if (!connected) throw ConnectionLostException()
}
private fun setCurrentVersion(v: Long) {
currVersion = v
logger.info("Setting local database version: $currVersion")
}
}
class ConnectionLostException : RuntimeException()

View File

@@ -1,81 +0,0 @@
/*
* Copyright (C) 2017 Á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.core.tasks;
import androidx.annotation.*;
import org.isoron.uhabits.core.io.*;
import org.isoron.uhabits.core.models.*;
import java.io.*;
import java.util.*;
public class ExportCSVTask implements Task
{
private String archiveFilename;
@NonNull
private final List<Habit> selectedHabits;
private File outputDir;
@NonNull
private final ExportCSVTask.Listener listener;
@NonNull
private final HabitList habitList;
public ExportCSVTask(@NonNull HabitList habitList,
@NonNull List<Habit> selectedHabits,
@NonNull File outputDir,
@NonNull Listener listener)
{
this.listener = listener;
this.habitList = habitList;
this.selectedHabits = selectedHabits;
this.outputDir = outputDir;
}
@Override
public void doInBackground()
{
try
{
HabitsCSVExporter exporter;
exporter = new HabitsCSVExporter(habitList, selectedHabits, outputDir);
archiveFilename = exporter.writeArchive();
}
catch (Exception e)
{
e.printStackTrace();
}
}
@Override
public void onPostExecute()
{
listener.onExportCSVFinished(archiveFilename);
}
public interface Listener
{
void onExportCSVFinished(@Nullable String archiveFilename);
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright (C) 2016-2020 Á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.core.tasks
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import java.io.File
import javax.inject.Inject
class ExportCSVTaskFactory
@Inject constructor(
val habitList: HabitList
) {
fun create(
selectedHabits: List<Habit>,
outputDir: File,
listener: ExportCSVTask.Listener,
) = ExportCSVTask(habitList, selectedHabits, outputDir, listener)
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.tasks;
import java.util.*;
public class SingleThreadTaskRunner implements TaskRunner
{
private List<Listener> listeners = new LinkedList<>();
@Override
public void addListener(Listener listener)
{
listeners.add(listener);
}
@Override
public void execute(Task task)
{
for(Listener l : listeners) l.onTaskStarted(task);
if(!task.isCanceled())
{
task.onAttached(this);
task.onPreExecute();
task.doInBackground();
task.onPostExecute();
}
for(Listener l : listeners) l.onTaskFinished(task);
}
@Override
public int getActiveTaskCount()
{
return 0;
}
@Override
public void publishProgress(Task task, int progress)
{
task.onProgressUpdate(progress);
}
@Override
public void removeListener(Listener listener)
{
listeners.remove(listener);
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.tasks;
import androidx.annotation.*;
public interface Task
{
default void cancel() {}
default boolean isCanceled()
{
return false;
}
void doInBackground();
default void onAttached(@NonNull TaskRunner runner) {}
default void onPostExecute() {}
default void onPreExecute() {}
default void onProgressUpdate(int value) {}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright (C) 2016 Á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.core.tasks;
public interface TaskRunner
{
void addListener(Listener listener);
void removeListener(Listener listener);
void execute(Task task);
void publishProgress(Task task, int progress);
int getActiveTaskCount();
interface Listener
{
void onTaskStarted(Task task);
void onTaskFinished(Task task);
}
}

Some files were not shown because too many files have changed in this diff Show More