Move uhabits-core to top level; all Java files to uhabits-core:jvmMain/jvmTest
@@ -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
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
include ':uhabits-android', ':uhabits-core-legacy', ':android-pickers', ':uhabits-core'
|
||||
include ':uhabits-android', ':android-pickers'
|
||||
|
||||
@@ -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"
|
||||
|
||||
1
android/uhabits-core-legacy/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -1,2 +0,0 @@
|
||||
Hello World!
|
||||
This is a resource.
|
||||
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 350 B |
|
Before Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 424 B |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 = "")
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 == ' '
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||