mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-07 01:28:52 -06:00
Move uhabits-core to top level; all Java files to uhabits-core:jvmMain/jvmTest
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.platform.gui
|
||||
|
||||
enum class TextAlign {
|
||||
LEFT, CENTER, RIGHT
|
||||
}
|
||||
|
||||
enum class Font {
|
||||
REGULAR,
|
||||
BOLD,
|
||||
FONT_AWESOME
|
||||
}
|
||||
|
||||
interface Canvas {
|
||||
fun setColor(color: Color)
|
||||
fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double)
|
||||
fun drawText(text: String, x: Double, y: Double)
|
||||
fun fillRect(x: Double, y: Double, width: Double, height: Double)
|
||||
fun fillRoundRect(x: Double, y: Double, width: Double, height: Double, cornerRadius: Double)
|
||||
fun drawRect(x: Double, y: Double, width: Double, height: Double)
|
||||
fun getHeight(): Double
|
||||
fun getWidth(): Double
|
||||
fun setFont(font: Font)
|
||||
fun setFontSize(size: Double)
|
||||
fun setStrokeWidth(size: Double)
|
||||
fun fillArc(
|
||||
centerX: Double,
|
||||
centerY: Double,
|
||||
radius: Double,
|
||||
startAngle: Double,
|
||||
swipeAngle: Double
|
||||
)
|
||||
fun fillCircle(centerX: Double, centerY: Double, radius: Double)
|
||||
fun setTextAlign(align: TextAlign)
|
||||
fun toImage(): Image
|
||||
fun measureText(test: String): Double
|
||||
|
||||
/**
|
||||
* Fills entire canvas with the current color.
|
||||
*/
|
||||
fun fill() {
|
||||
fillRect(0.0, 0.0, getWidth(), getHeight())
|
||||
}
|
||||
|
||||
fun drawTestImage() {
|
||||
// Draw transparent background
|
||||
setColor(Color(0.1, 0.1, 0.1, 0.5))
|
||||
fillRect(0.0, 0.0, 500.0, 400.0)
|
||||
|
||||
// Draw center rectangle
|
||||
setColor(Color(0x606060))
|
||||
setStrokeWidth(25.0)
|
||||
drawRect(100.0, 100.0, 300.0, 200.0)
|
||||
|
||||
// Draw squares, circles and arcs
|
||||
setColor(Color.YELLOW)
|
||||
setStrokeWidth(1.0)
|
||||
drawRect(0.0, 0.0, 100.0, 100.0)
|
||||
fillCircle(50.0, 50.0, 30.0)
|
||||
drawRect(0.0, 100.0, 100.0, 100.0)
|
||||
fillArc(50.0, 150.0, 30.0, 90.0, 135.0)
|
||||
drawRect(0.0, 200.0, 100.0, 100.0)
|
||||
fillArc(50.0, 250.0, 30.0, 90.0, -135.0)
|
||||
drawRect(0.0, 300.0, 100.0, 100.0)
|
||||
fillArc(50.0, 350.0, 30.0, 45.0, 90.0)
|
||||
|
||||
// Draw two red crossing lines
|
||||
setColor(Color.RED)
|
||||
setStrokeWidth(2.0)
|
||||
drawLine(0.0, 0.0, 500.0, 400.0)
|
||||
drawLine(500.0, 0.0, 0.0, 400.0)
|
||||
|
||||
// Draw text
|
||||
setFont(Font.BOLD)
|
||||
setFontSize(50.0)
|
||||
setColor(Color.GREEN)
|
||||
setTextAlign(TextAlign.CENTER)
|
||||
drawText("HELLO", 250.0, 100.0)
|
||||
setTextAlign(TextAlign.RIGHT)
|
||||
drawText("HELLO", 250.0, 150.0)
|
||||
setTextAlign(TextAlign.LEFT)
|
||||
drawText("HELLO", 250.0, 200.0)
|
||||
|
||||
// Draw FontAwesome icon
|
||||
setFont(Font.FONT_AWESOME)
|
||||
drawText(FontAwesome.CHECK, 250.0, 300.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
data class Color(
|
||||
val red: Double,
|
||||
val green: Double,
|
||||
val blue: Double,
|
||||
val alpha: Double,
|
||||
) {
|
||||
val luminosity: Double
|
||||
get() {
|
||||
return 0.21 * red + 0.72 * green + 0.07 * blue
|
||||
}
|
||||
|
||||
constructor(rgb: Int) : this(
|
||||
((rgb shr 16) and 0xFF) / 255.0,
|
||||
((rgb shr 8) and 0xFF) / 255.0,
|
||||
((rgb shr 0) and 0xFF) / 255.0,
|
||||
1.0
|
||||
)
|
||||
|
||||
fun blendWith(other: Color, weight: Double): Color {
|
||||
return Color(
|
||||
red * (1 - weight) + other.red * weight,
|
||||
green * (1 - weight) + other.green * weight,
|
||||
blue * (1 - weight) + other.blue * weight,
|
||||
alpha * (1 - weight) + other.alpha * weight
|
||||
)
|
||||
}
|
||||
|
||||
fun contrast(other: Color): Double {
|
||||
val l1 = this.luminosity
|
||||
val l2 = other.luminosity
|
||||
val relativeLuminosity = (l1 + 0.05) / (l2 + 0.05)
|
||||
return if (relativeLuminosity >= 1) relativeLuminosity else 1 / relativeLuminosity
|
||||
}
|
||||
|
||||
fun withAlpha(newAlpha: Double) = Color(red, green, blue, newAlpha)
|
||||
|
||||
companion object {
|
||||
val TRANSPARENT = Color(0.0, 0.0, 0.0, 0.0)
|
||||
val RED = Color(1.0, 0.0, 0.0, 1.0)
|
||||
val GREEN = Color(0.0, 1.0, 0.0, 1.0)
|
||||
val BLUE = Color(1.0, 0.0, 1.0, 1.0)
|
||||
val YELLOW = Color(1.0, 1.0, 0.0, 1.0)
|
||||
val MAGENTA = Color(1.0, 0.0, 1.0, 1.0)
|
||||
val CYAN = Color(0.0, 1.0, 1.0, 1.0)
|
||||
val WHITE = Color(1.0, 1.0, 1.0, 1.0)
|
||||
val BLACK = Color(0.0, 0.0, 0.0, 1.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
class FontAwesome {
|
||||
companion object {
|
||||
val CHECK = "\uf00c"
|
||||
val TIMES = "\uf00d"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
interface Image {
|
||||
val width: Int
|
||||
val height: Int
|
||||
|
||||
fun getPixel(x: Int, y: Int): Color
|
||||
fun setPixel(x: Int, y: Int, color: Color)
|
||||
|
||||
suspend fun export(path: String)
|
||||
|
||||
fun diff(other: Image) {
|
||||
if (width != other.width) error("Width must match: $width !== ${other.width}")
|
||||
if (height != other.height) error("Height must match: $height !== ${other.height}")
|
||||
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
val p1 = getPixel(x, y)
|
||||
var l = 1.0
|
||||
for (dx in -2..2) {
|
||||
if (x + dx < 0 || x + dx >= width) continue
|
||||
for (dy in -2..2) {
|
||||
if (y + dy < 0 || y + dy >= height) continue
|
||||
val p2 = other.getPixel(x + dx, y + dy)
|
||||
l = min(l, abs(p1.luminosity - p2.luminosity))
|
||||
}
|
||||
}
|
||||
setPixel(x, y, Color(l, l, l, 1.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val averageLuminosity: Double
|
||||
get() {
|
||||
var luminosity = 0.0
|
||||
for (x in 0 until width) {
|
||||
for (y in 0 until height) {
|
||||
luminosity += getPixel(x, y).luminosity
|
||||
}
|
||||
}
|
||||
return luminosity / (width * height)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
interface View {
|
||||
fun draw(canvas: Canvas)
|
||||
fun onClick(x: Double, y: Double) {
|
||||
}
|
||||
}
|
||||
|
||||
interface DataView : View {
|
||||
var dataOffset: Int
|
||||
val dataColumnWidth: Double
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.io
|
||||
|
||||
import org.isoron.platform.gui.Image
|
||||
|
||||
interface FileOpener {
|
||||
/**
|
||||
* Opens a file which was shipped bundled with the application, such as a
|
||||
* migration file.
|
||||
*
|
||||
* The path is relative to the assets folder. For example, to open
|
||||
* assets/main/migrations/09.sql you should provide migrations/09.sql
|
||||
* as the path.
|
||||
*
|
||||
* This function always succeed, even if the file does not exist.
|
||||
*/
|
||||
fun openResourceFile(path: String): ResourceFile
|
||||
|
||||
/**
|
||||
* Opens a file which was not shipped with the application, such as
|
||||
* databases and logs.
|
||||
*
|
||||
* The path is relative to the user folder. For example, if the application
|
||||
* stores the user data at /home/user/.loop/ and you wish to open the file
|
||||
* /home/user/.loop/crash.log, you should provide crash.log as the path.
|
||||
*
|
||||
* This function always succeed, even if the file does not exist.
|
||||
*/
|
||||
fun openUserFile(path: String): UserFile
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a file that was created after the application was installed, as a
|
||||
* result of some user action, such as databases and logs.
|
||||
*/
|
||||
interface UserFile {
|
||||
/**
|
||||
* Deletes the user file. If the file does not exist, nothing happens.
|
||||
*/
|
||||
suspend fun delete()
|
||||
|
||||
/**
|
||||
* Returns true if the file exists.
|
||||
*/
|
||||
suspend fun exists(): Boolean
|
||||
|
||||
/**
|
||||
* Returns the lines of the file. If the file does not exist, throws an
|
||||
* exception.
|
||||
*/
|
||||
suspend fun lines(): List<String>
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a file that was shipped with the application, such as migration
|
||||
* files or database templates.
|
||||
*/
|
||||
interface ResourceFile {
|
||||
/**
|
||||
* Copies the resource file to the specified user file. If the user file
|
||||
* already exists, it is replaced. If not, a new file is created.
|
||||
*/
|
||||
suspend fun copyTo(dest: UserFile)
|
||||
|
||||
/**
|
||||
* Returns the lines of the resource file. If the file does not exist,
|
||||
* throws an exception.
|
||||
*/
|
||||
suspend fun lines(): List<String>
|
||||
|
||||
/**
|
||||
* Returns true if the file exists.
|
||||
*/
|
||||
suspend fun exists(): Boolean
|
||||
|
||||
/**
|
||||
* Loads resource file as an image.
|
||||
*/
|
||||
suspend fun toImage(): Image
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.time
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
|
||||
enum class DayOfWeek(val daysSinceSunday: Int) {
|
||||
SUNDAY(0),
|
||||
MONDAY(1),
|
||||
TUESDAY(2),
|
||||
WEDNESDAY(3),
|
||||
THURSDAY(4),
|
||||
FRIDAY(5),
|
||||
SATURDAY(6),
|
||||
}
|
||||
|
||||
data class LocalDate(val daysSince2000: Int) {
|
||||
|
||||
var yearCache = -1
|
||||
var monthCache = -1
|
||||
var dayCache = -1
|
||||
|
||||
constructor(year: Int, month: Int, day: Int) :
|
||||
this(daysSince2000(year, month, day))
|
||||
|
||||
val dayOfWeek: DayOfWeek
|
||||
get() {
|
||||
return when (daysSince2000 % 7) {
|
||||
0 -> DayOfWeek.SATURDAY
|
||||
1 -> DayOfWeek.SUNDAY
|
||||
2 -> DayOfWeek.MONDAY
|
||||
3 -> DayOfWeek.TUESDAY
|
||||
4 -> DayOfWeek.WEDNESDAY
|
||||
5 -> DayOfWeek.THURSDAY
|
||||
else -> DayOfWeek.FRIDAY
|
||||
}
|
||||
}
|
||||
|
||||
val year: Int
|
||||
get() {
|
||||
if (yearCache < 0) updateYearMonthDayCache()
|
||||
return yearCache
|
||||
}
|
||||
|
||||
val month: Int
|
||||
get() {
|
||||
if (monthCache < 0) updateYearMonthDayCache()
|
||||
return monthCache
|
||||
}
|
||||
|
||||
val day: Int
|
||||
get() {
|
||||
if (dayCache < 0) updateYearMonthDayCache()
|
||||
return dayCache
|
||||
}
|
||||
|
||||
private fun updateYearMonthDayCache() {
|
||||
var currYear = 2000
|
||||
var currDay = 0
|
||||
|
||||
while (true) {
|
||||
val currYearLength = if (isLeapYear(currYear)) 366 else 365
|
||||
if (daysSince2000 < currDay + currYearLength) {
|
||||
yearCache = currYear
|
||||
break
|
||||
} else {
|
||||
currYear++
|
||||
currDay += currYearLength
|
||||
}
|
||||
}
|
||||
|
||||
var currMonth = 1
|
||||
val monthOffset = if (isLeapYear(currYear)) leapOffset else nonLeapOffset
|
||||
|
||||
while (true) {
|
||||
if (daysSince2000 < currDay + monthOffset[currMonth]) {
|
||||
monthCache = currMonth
|
||||
break
|
||||
} else {
|
||||
currMonth++
|
||||
}
|
||||
}
|
||||
|
||||
currDay += monthOffset[currMonth - 1]
|
||||
dayCache = daysSince2000 - currDay + 1
|
||||
}
|
||||
|
||||
fun isOlderThan(other: LocalDate): Boolean {
|
||||
return daysSince2000 < other.daysSince2000
|
||||
}
|
||||
|
||||
fun isNewerThan(other: LocalDate): Boolean {
|
||||
return daysSince2000 > other.daysSince2000
|
||||
}
|
||||
|
||||
fun plus(days: Int): LocalDate {
|
||||
return LocalDate(daysSince2000 + days)
|
||||
}
|
||||
|
||||
fun minus(days: Int): LocalDate {
|
||||
return LocalDate(daysSince2000 - days)
|
||||
}
|
||||
|
||||
fun distanceTo(other: LocalDate): Int {
|
||||
return abs(daysSince2000 - other.daysSince2000)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "LocalDate($year-$month-$day)"
|
||||
}
|
||||
}
|
||||
|
||||
interface LocalDateFormatter {
|
||||
fun shortWeekdayName(weekday: DayOfWeek): String
|
||||
fun shortWeekdayName(date: LocalDate): String
|
||||
fun shortMonthName(date: LocalDate): String
|
||||
}
|
||||
|
||||
private fun isLeapYear(year: Int): Boolean {
|
||||
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
|
||||
}
|
||||
|
||||
val leapOffset = arrayOf(
|
||||
0, 31, 60, 91, 121, 152, 182,
|
||||
213, 244, 274, 305, 335, 366
|
||||
)
|
||||
val nonLeapOffset = arrayOf(
|
||||
0, 31, 59, 90, 120, 151, 181,
|
||||
212, 243, 273, 304, 334, 365
|
||||
)
|
||||
|
||||
private fun daysSince2000(year: Int, month: Int, day: Int): Int {
|
||||
|
||||
var result = 365 * (year - 2000)
|
||||
result += ceil((year - 2000) / 4.0).toInt()
|
||||
result -= ceil((year - 2000) / 100.0).toInt()
|
||||
result += ceil((year - 2000) / 400.0).toInt()
|
||||
if (isLeapYear(year)) {
|
||||
result += leapOffset[month - 1]
|
||||
} else {
|
||||
result += nonLeapOffset[month - 1]
|
||||
}
|
||||
result += (day - 1)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.isoron.platform.io.JavaFileOpener
|
||||
import org.isoron.platform.io.JavaResourceFile
|
||||
import java.awt.BasicStroke
|
||||
import java.awt.RenderingHints.KEY_ANTIALIASING
|
||||
import java.awt.RenderingHints.KEY_FRACTIONALMETRICS
|
||||
import java.awt.RenderingHints.KEY_TEXT_ANTIALIASING
|
||||
import java.awt.RenderingHints.VALUE_ANTIALIAS_ON
|
||||
import java.awt.RenderingHints.VALUE_FRACTIONALMETRICS_ON
|
||||
import java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON
|
||||
import java.awt.font.FontRenderContext
|
||||
import java.awt.geom.RoundRectangle2D
|
||||
import java.awt.image.BufferedImage
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class JavaCanvas(
|
||||
val image: BufferedImage,
|
||||
val pixelScale: Double = 2.0,
|
||||
) : Canvas {
|
||||
|
||||
override fun toImage(): Image {
|
||||
return JavaImage(image)
|
||||
}
|
||||
|
||||
override fun measureText(text: String): Double {
|
||||
val metrics = g2d.getFontMetrics(g2d.font)
|
||||
return toDp(metrics.stringWidth(text))
|
||||
}
|
||||
|
||||
private val frc = FontRenderContext(null, true, true)
|
||||
private var fontSize = 12.0
|
||||
private var font = Font.REGULAR
|
||||
private var textAlign = TextAlign.CENTER
|
||||
val widthPx = image.width
|
||||
val heightPx = image.height
|
||||
val g2d = image.createGraphics()
|
||||
|
||||
private val NOTO_REGULAR_FONT = createFont("fonts/NotoSans-Regular.ttf")
|
||||
private val NOTO_BOLD_FONT = createFont("fonts/NotoSans-Bold.ttf")
|
||||
private val FONT_AWESOME_FONT = createFont("fonts/FontAwesome.ttf")
|
||||
|
||||
init {
|
||||
g2d.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON)
|
||||
g2d.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON)
|
||||
g2d.setRenderingHint(KEY_FRACTIONALMETRICS, VALUE_FRACTIONALMETRICS_ON)
|
||||
updateFont()
|
||||
}
|
||||
|
||||
private fun toPixel(x: Double): Int {
|
||||
return (pixelScale * x).toInt()
|
||||
}
|
||||
|
||||
private fun toDp(x: Int): Double {
|
||||
return x / pixelScale
|
||||
}
|
||||
|
||||
override fun setColor(color: Color) {
|
||||
g2d.color = java.awt.Color(
|
||||
color.red.toFloat(),
|
||||
color.green.toFloat(),
|
||||
color.blue.toFloat(),
|
||||
color.alpha.toFloat()
|
||||
)
|
||||
}
|
||||
|
||||
override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) {
|
||||
g2d.drawLine(toPixel(x1), toPixel(y1), toPixel(x2), toPixel(y2))
|
||||
}
|
||||
|
||||
override fun drawText(text: String, x: Double, y: Double) {
|
||||
updateFont()
|
||||
val bounds = g2d.font.getStringBounds(text, frc)
|
||||
val bWidth = bounds.width.roundToInt()
|
||||
val bHeight = bounds.height.roundToInt()
|
||||
val bx = bounds.x.roundToInt()
|
||||
val by = bounds.y.roundToInt()
|
||||
|
||||
if (textAlign == TextAlign.CENTER) {
|
||||
g2d.drawString(
|
||||
text,
|
||||
toPixel(x) - bx - bWidth / 2,
|
||||
toPixel(y) - by - bHeight / 2
|
||||
)
|
||||
} else if (textAlign == TextAlign.LEFT) {
|
||||
g2d.drawString(
|
||||
text,
|
||||
toPixel(x) - bx,
|
||||
toPixel(y) - by - bHeight / 2
|
||||
)
|
||||
} else {
|
||||
g2d.drawString(
|
||||
text,
|
||||
toPixel(x) - bx - bWidth,
|
||||
toPixel(y) - by - bHeight / 2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fillRect(x: Double, y: Double, width: Double, height: Double) {
|
||||
g2d.fillRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height))
|
||||
}
|
||||
|
||||
override fun fillRoundRect(
|
||||
x: Double,
|
||||
y: Double,
|
||||
width: Double,
|
||||
height: Double,
|
||||
cornerRadius: Double,
|
||||
) {
|
||||
g2d.fill(
|
||||
RoundRectangle2D.Double(
|
||||
toPixel(x).toDouble(),
|
||||
toPixel(y).toDouble(),
|
||||
toPixel(width).toDouble(),
|
||||
toPixel(height).toDouble(),
|
||||
toPixel(cornerRadius).toDouble(),
|
||||
toPixel(cornerRadius).toDouble(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun drawRect(x: Double, y: Double, width: Double, height: Double) {
|
||||
g2d.drawRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height))
|
||||
}
|
||||
|
||||
override fun getHeight(): Double {
|
||||
return toDp(heightPx)
|
||||
}
|
||||
|
||||
override fun getWidth(): Double {
|
||||
return toDp(widthPx)
|
||||
}
|
||||
|
||||
override fun setFont(font: Font) {
|
||||
this.font = font
|
||||
updateFont()
|
||||
}
|
||||
|
||||
override fun setFontSize(size: Double) {
|
||||
fontSize = size
|
||||
updateFont()
|
||||
}
|
||||
|
||||
override fun setStrokeWidth(size: Double) {
|
||||
g2d.stroke = BasicStroke((size * pixelScale).toFloat())
|
||||
}
|
||||
|
||||
private fun updateFont() {
|
||||
val size = (fontSize * pixelScale).toFloat()
|
||||
g2d.font = when (font) {
|
||||
Font.REGULAR -> NOTO_REGULAR_FONT.deriveFont(size)
|
||||
Font.BOLD -> NOTO_BOLD_FONT.deriveFont(size)
|
||||
Font.FONT_AWESOME -> FONT_AWESOME_FONT.deriveFont(size)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fillCircle(centerX: Double, centerY: Double, radius: Double) {
|
||||
g2d.fillOval(
|
||||
toPixel(centerX - radius),
|
||||
toPixel(centerY - radius),
|
||||
toPixel(radius * 2),
|
||||
toPixel(radius * 2)
|
||||
)
|
||||
}
|
||||
|
||||
override fun fillArc(
|
||||
centerX: Double,
|
||||
centerY: Double,
|
||||
radius: Double,
|
||||
startAngle: Double,
|
||||
swipeAngle: Double,
|
||||
) {
|
||||
|
||||
g2d.fillArc(
|
||||
toPixel(centerX - radius),
|
||||
toPixel(centerY - radius),
|
||||
toPixel(radius * 2),
|
||||
toPixel(radius * 2),
|
||||
startAngle.roundToInt(),
|
||||
swipeAngle.roundToInt()
|
||||
)
|
||||
}
|
||||
|
||||
override fun setTextAlign(align: TextAlign) {
|
||||
this.textAlign = align
|
||||
}
|
||||
|
||||
private fun createFont(path: String) = runBlocking<java.awt.Font> {
|
||||
val file = JavaFileOpener().openResourceFile(path) as JavaResourceFile
|
||||
if (!file.exists()) throw RuntimeException("File not found: ${file.path}")
|
||||
java.awt.Font.createFont(0, file.stream())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.gui
|
||||
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
class JavaImage(val bufferedImage: BufferedImage) : Image {
|
||||
override fun setPixel(x: Int, y: Int, color: Color) {
|
||||
bufferedImage.setRGB(
|
||||
x,
|
||||
y,
|
||||
java.awt.Color(
|
||||
color.red.toFloat(),
|
||||
color.green.toFloat(),
|
||||
color.blue.toFloat()
|
||||
).rgb
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun export(path: String) {
|
||||
val file = File(path)
|
||||
file.parentFile.mkdirs()
|
||||
ImageIO.write(bufferedImage, "png", file)
|
||||
}
|
||||
|
||||
override val width: Int
|
||||
get() = bufferedImage.width
|
||||
|
||||
override val height: Int
|
||||
get() = bufferedImage.height
|
||||
|
||||
override fun getPixel(x: Int, y: Int): Color {
|
||||
return Color(bufferedImage.getRGB(x, y))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.io
|
||||
|
||||
import org.isoron.platform.gui.Image
|
||||
import org.isoron.platform.gui.JavaImage
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
@Suppress("NewApi")
|
||||
class JavaResourceFile(val path: String) : ResourceFile {
|
||||
private val javaPath: Path
|
||||
get() {
|
||||
val mainPath = Paths.get("assets/main/$path")
|
||||
val testPath = Paths.get("assets/test/$path")
|
||||
if (Files.exists(mainPath)) return mainPath
|
||||
else return testPath
|
||||
}
|
||||
|
||||
override suspend fun exists(): Boolean {
|
||||
return Files.exists(javaPath)
|
||||
}
|
||||
|
||||
override suspend fun lines(): List<String> {
|
||||
return Files.readAllLines(javaPath)
|
||||
}
|
||||
|
||||
override suspend fun copyTo(dest: UserFile) {
|
||||
if (dest.exists()) dest.delete()
|
||||
val destPath = (dest as JavaUserFile).path
|
||||
destPath.toFile().parentFile?.mkdirs()
|
||||
Files.copy(javaPath, destPath)
|
||||
}
|
||||
|
||||
fun stream(): InputStream {
|
||||
return Files.newInputStream(javaPath)
|
||||
}
|
||||
|
||||
override suspend fun toImage(): Image {
|
||||
return JavaImage(ImageIO.read(stream()))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NewApi")
|
||||
class JavaUserFile(val path: Path) : UserFile {
|
||||
override suspend fun lines(): List<String> {
|
||||
return Files.readAllLines(path)
|
||||
}
|
||||
|
||||
override suspend fun exists(): Boolean {
|
||||
return Files.exists(path)
|
||||
}
|
||||
|
||||
override suspend fun delete() {
|
||||
Files.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NewApi")
|
||||
class JavaFileOpener : FileOpener {
|
||||
override fun openUserFile(path: String): UserFile {
|
||||
val path = Paths.get("/tmp/$path")
|
||||
return JavaUserFile(path)
|
||||
}
|
||||
|
||||
override fun openResourceFile(path: String): ResourceFile {
|
||||
return JavaResourceFile(path)
|
||||
}
|
||||
}
|
||||
69
uhabits-core/src/jvmMain/java/org/isoron/time/JavaDates.kt
Normal file
69
uhabits-core/src/jvmMain/java/org/isoron/time/JavaDates.kt
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.platform.time
|
||||
|
||||
import java.util.Calendar.DAY_OF_MONTH
|
||||
import java.util.Calendar.DAY_OF_WEEK
|
||||
import java.util.Calendar.HOUR_OF_DAY
|
||||
import java.util.Calendar.LONG
|
||||
import java.util.Calendar.MILLISECOND
|
||||
import java.util.Calendar.MINUTE
|
||||
import java.util.Calendar.MONTH
|
||||
import java.util.Calendar.SECOND
|
||||
import java.util.Calendar.SHORT
|
||||
import java.util.Calendar.YEAR
|
||||
import java.util.GregorianCalendar
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
fun LocalDate.toGregorianCalendar(): GregorianCalendar {
|
||||
val cal = GregorianCalendar()
|
||||
cal.timeZone = TimeZone.getTimeZone("GMT")
|
||||
cal.set(MILLISECOND, 0)
|
||||
cal.set(SECOND, 0)
|
||||
cal.set(MINUTE, 0)
|
||||
cal.set(HOUR_OF_DAY, 0)
|
||||
cal.set(YEAR, this.year)
|
||||
cal.set(MONTH, this.month - 1)
|
||||
cal.set(DAY_OF_MONTH, this.day)
|
||||
return cal
|
||||
}
|
||||
|
||||
class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter {
|
||||
override fun shortMonthName(date: LocalDate): String {
|
||||
val cal = date.toGregorianCalendar()
|
||||
val longName = cal.getDisplayName(MONTH, LONG, locale)
|
||||
val shortName = cal.getDisplayName(MONTH, SHORT, locale)
|
||||
|
||||
// For some locales, such as Japan, SHORT name is exceedingly short
|
||||
return if (longName.length <= 3) longName else shortName
|
||||
}
|
||||
|
||||
override fun shortWeekdayName(weekday: DayOfWeek): String {
|
||||
val cal = GregorianCalendar()
|
||||
cal.set(DAY_OF_WEEK, weekday.daysSinceSunday - 1)
|
||||
return shortWeekdayName(LocalDate(cal.get(YEAR), cal.get(MONTH) + 1, cal.get(DAY_OF_MONTH)))
|
||||
}
|
||||
|
||||
override fun shortWeekdayName(date: LocalDate): String {
|
||||
val cal = date.toGregorianCalendar()
|
||||
return cal.getDisplayName(DAY_OF_WEEK, SHORT, locale)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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 = "")
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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?
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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 == ' '
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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")
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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()
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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()
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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>
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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;
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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;
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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())
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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)
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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()
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* 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()
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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) {}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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.test;
|
||||
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.models.sqlite.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
|
||||
import static org.isoron.uhabits.core.models.Entry.*;
|
||||
|
||||
public class HabitFixtures
|
||||
{
|
||||
public boolean NON_DAILY_HABIT_CHECKS[] = {
|
||||
true, false, false, true, true, true, false, false, true, true
|
||||
};
|
||||
|
||||
private final ModelFactory modelFactory;
|
||||
|
||||
private HabitList habitList;
|
||||
|
||||
public HabitFixtures(ModelFactory modelFactory, HabitList habitList)
|
||||
{
|
||||
this.modelFactory = modelFactory;
|
||||
this.habitList = habitList;
|
||||
}
|
||||
|
||||
public Habit createEmptyHabit()
|
||||
{
|
||||
Habit habit = modelFactory.buildHabit();
|
||||
habit.setName("Meditate");
|
||||
habit.setQuestion("Did you meditate this morning?");
|
||||
habit.setColor(new PaletteColor(3));
|
||||
habit.setFrequency(Frequency.DAILY);
|
||||
saveIfSQLite(habit);
|
||||
|
||||
return habit;
|
||||
}
|
||||
|
||||
public Habit createLongHabit()
|
||||
{
|
||||
Habit habit = createEmptyHabit();
|
||||
habit.setFrequency(new Frequency(3, 7));
|
||||
habit.setColor(new PaletteColor(4));
|
||||
|
||||
Timestamp today = DateUtils.getToday();
|
||||
int marks[] = {0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27,
|
||||
28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80,
|
||||
81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120};
|
||||
|
||||
for (int mark : marks)
|
||||
habit.getOriginalEntries().add(new Entry(today.minus(mark), YES_MANUAL));
|
||||
|
||||
habit.recompute();
|
||||
return habit;
|
||||
}
|
||||
|
||||
public Habit createNumericalHabit()
|
||||
{
|
||||
Habit habit = modelFactory.buildHabit();
|
||||
habit.setType(Habit.NUMBER_HABIT);
|
||||
habit.setName("Run");
|
||||
habit.setQuestion("How many miles did you run today?");
|
||||
habit.setUnit("miles");
|
||||
habit.setTargetType(Habit.AT_LEAST);
|
||||
habit.setTargetValue(2.0);
|
||||
habit.setColor(new PaletteColor(1));
|
||||
saveIfSQLite(habit);
|
||||
|
||||
Timestamp today = DateUtils.getToday();
|
||||
int times[] = {0, 1, 3, 5, 7, 8, 9, 10};
|
||||
int values[] = {100, 200, 300, 400, 500, 600, 700, 800};
|
||||
|
||||
for (int i = 0; i < times.length; i++)
|
||||
{
|
||||
Timestamp timestamp = today.minus(times[i]);
|
||||
habit.getOriginalEntries().add(new Entry(timestamp, values[i]));
|
||||
}
|
||||
|
||||
habit.recompute();
|
||||
return habit;
|
||||
}
|
||||
|
||||
public Habit createLongNumericalHabit(Timestamp reference)
|
||||
{
|
||||
Habit habit = modelFactory.buildHabit();
|
||||
habit.setType(Habit.NUMBER_HABIT);
|
||||
habit.setName("Walk");
|
||||
habit.setQuestion("How many steps did you walk today?");
|
||||
habit.setUnit("steps");
|
||||
habit.setTargetType(Habit.AT_LEAST);
|
||||
habit.setTargetValue(100);
|
||||
habit.setColor(new PaletteColor(1));
|
||||
saveIfSQLite(habit);
|
||||
|
||||
int times[] = {0, 5, 9, 15, 17, 21, 23, 27, 28, 35, 41, 45, 47, 53, 56, 62, 70, 73, 78,
|
||||
83, 86, 94, 101, 106, 113, 114, 120, 126, 130, 133, 141, 143, 148, 151, 157, 164,
|
||||
166, 171, 173, 176, 179, 183, 191, 259, 264, 268, 270, 275, 282, 284, 289, 295,
|
||||
302, 306, 310, 315, 323, 325, 328, 335, 343, 349, 351, 353, 357, 359, 360, 367,
|
||||
372, 376, 380, 385, 393, 400, 404, 412, 415, 418, 422, 425, 433, 437, 444, 449,
|
||||
455, 460, 462, 465, 470, 471, 479, 481, 485, 489, 494, 495, 500, 501, 503, 507};
|
||||
|
||||
int values[] = {230, 306, 148, 281, 134, 285, 104, 158, 325, 236, 303, 210, 118, 124,
|
||||
301, 201, 156, 376, 347, 367, 396, 134, 160, 381, 155, 354, 231, 134, 164, 354,
|
||||
236, 398, 199, 221, 208, 397, 253, 276, 214, 341, 299, 221, 353, 250, 341, 168,
|
||||
374, 205, 182, 217, 297, 321, 104, 237, 294, 110, 136, 229, 102, 271, 250, 294,
|
||||
158, 319, 379, 126, 282, 155, 288, 159, 215, 247, 207, 226, 244, 158, 371, 219,
|
||||
272, 228, 350, 153, 356, 279, 394, 202, 213, 214, 112, 248, 139, 245, 165, 256,
|
||||
370, 187, 208, 231, 341, 312};
|
||||
|
||||
for (int i = 0; i < times.length; i++)
|
||||
{
|
||||
Timestamp timestamp = reference.minus(times[i]);
|
||||
habit.getOriginalEntries().add(new Entry(timestamp, values[i]));
|
||||
}
|
||||
|
||||
habit.recompute();
|
||||
return habit;
|
||||
}
|
||||
|
||||
public Habit createShortHabit()
|
||||
{
|
||||
Habit habit = modelFactory.buildHabit();
|
||||
habit.setName("Wake up early");
|
||||
habit.setQuestion("Did you wake up before 6am?");
|
||||
habit.setFrequency(new Frequency(2, 3));
|
||||
saveIfSQLite(habit);
|
||||
|
||||
Timestamp timestamp = DateUtils.getToday();
|
||||
for (boolean c : NON_DAILY_HABIT_CHECKS)
|
||||
{
|
||||
int value = NO;
|
||||
if (c) value = YES_MANUAL;
|
||||
habit.getOriginalEntries().add(new Entry(timestamp, value));
|
||||
timestamp = timestamp.minus(1);
|
||||
}
|
||||
|
||||
habit.recompute();
|
||||
return habit;
|
||||
}
|
||||
|
||||
private void saveIfSQLite(Habit habit)
|
||||
{
|
||||
if (!(habit.getOriginalEntries() instanceof SQLiteEntryList)) return;
|
||||
habitList.add(habit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
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 org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
|
||||
@AppScope
|
||||
public class NotificationTray
|
||||
implements CommandRunner.Listener, Preferences.Listener
|
||||
{
|
||||
public static final String REMINDERS_CHANNEL_ID = "REMINDERS";
|
||||
|
||||
@NonNull
|
||||
private final TaskRunner taskRunner;
|
||||
|
||||
@NonNull
|
||||
private final CommandRunner commandRunner;
|
||||
|
||||
@NonNull
|
||||
private final Preferences preferences;
|
||||
|
||||
private SystemTray systemTray;
|
||||
|
||||
@NonNull
|
||||
private final HashMap<Habit, NotificationData> active;
|
||||
|
||||
@Inject
|
||||
public NotificationTray(@NonNull TaskRunner taskRunner,
|
||||
@NonNull CommandRunner commandRunner,
|
||||
@NonNull Preferences preferences,
|
||||
@NonNull SystemTray systemTray)
|
||||
{
|
||||
this.taskRunner = taskRunner;
|
||||
this.commandRunner = commandRunner;
|
||||
this.preferences = preferences;
|
||||
this.systemTray = systemTray;
|
||||
this.active = new HashMap<>();
|
||||
}
|
||||
|
||||
public void cancel(@NonNull Habit habit)
|
||||
{
|
||||
int notificationId = getNotificationId(habit);
|
||||
systemTray.removeNotification(notificationId);
|
||||
active.remove(habit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommandFinished(@Nullable Command command)
|
||||
{
|
||||
if (command instanceof CreateRepetitionCommand)
|
||||
{
|
||||
CreateRepetitionCommand createCmd = (CreateRepetitionCommand) command;
|
||||
Habit habit = createCmd.getHabit();
|
||||
cancel(habit);
|
||||
}
|
||||
|
||||
if (command instanceof DeleteHabitsCommand)
|
||||
{
|
||||
DeleteHabitsCommand deleteCommand = (DeleteHabitsCommand) command;
|
||||
List<Habit> deleted = deleteCommand.getSelected();
|
||||
for (Habit habit : deleted)
|
||||
cancel(habit);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationsChanged()
|
||||
{
|
||||
reshowAll();
|
||||
}
|
||||
|
||||
public void show(@NonNull Habit habit, Timestamp timestamp, long reminderTime)
|
||||
{
|
||||
NotificationData data = new NotificationData(timestamp, reminderTime);
|
||||
active.put(habit, data);
|
||||
taskRunner.execute(new ShowNotificationTask(habit, data));
|
||||
}
|
||||
|
||||
public void startListening()
|
||||
{
|
||||
commandRunner.addListener(this);
|
||||
preferences.addListener(this);
|
||||
}
|
||||
|
||||
public void stopListening()
|
||||
{
|
||||
commandRunner.removeListener(this);
|
||||
preferences.removeListener(this);
|
||||
}
|
||||
|
||||
private int getNotificationId(Habit habit)
|
||||
{
|
||||
Long id = habit.getId();
|
||||
if (id == null) return 0;
|
||||
return (int) (id % Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
private void reshowAll()
|
||||
{
|
||||
for (Habit habit : active.keySet())
|
||||
{
|
||||
NotificationData data = active.get(habit);
|
||||
taskRunner.execute(new ShowNotificationTask(habit, data));
|
||||
}
|
||||
}
|
||||
|
||||
public interface SystemTray
|
||||
{
|
||||
void removeNotification(int notificationId);
|
||||
|
||||
void showNotification(Habit habit,
|
||||
int notificationId,
|
||||
Timestamp timestamp,
|
||||
long reminderTime);
|
||||
|
||||
void log(String msg);
|
||||
}
|
||||
|
||||
class NotificationData
|
||||
{
|
||||
public final Timestamp timestamp;
|
||||
|
||||
public final long reminderTime;
|
||||
|
||||
public NotificationData(Timestamp timestamp, long reminderTime)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
this.reminderTime = reminderTime;
|
||||
}
|
||||
}
|
||||
|
||||
private class ShowNotificationTask implements Task
|
||||
{
|
||||
int todayValue;
|
||||
|
||||
private final Habit habit;
|
||||
|
||||
private final Timestamp timestamp;
|
||||
|
||||
private final long reminderTime;
|
||||
|
||||
public ShowNotificationTask(Habit habit, NotificationData data)
|
||||
{
|
||||
this.habit = habit;
|
||||
this.timestamp = data.timestamp;
|
||||
this.reminderTime = data.reminderTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doInBackground()
|
||||
{
|
||||
Timestamp today = DateUtils.getTodayWithOffset();
|
||||
todayValue = habit.getComputedEntries().get(today).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute()
|
||||
{
|
||||
systemTray.log("Showing notification for habit=" + habit.getId());
|
||||
|
||||
if (todayValue != Entry.UNKNOWN) {
|
||||
systemTray.log(String.format(
|
||||
Locale.US,
|
||||
"Habit %d already checked. Skipping.",
|
||||
habit.getId()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!habit.hasReminder()) {
|
||||
systemTray.log(String.format(
|
||||
Locale.US,
|
||||
"Habit %d does not have a reminder. Skipping.",
|
||||
habit.getId()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (habit.isArchived())
|
||||
{
|
||||
systemTray.log(String.format(
|
||||
Locale.US,
|
||||
"Habit %d is archived. Skipping.",
|
||||
habit.getId()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldShowReminderToday()) {
|
||||
systemTray.log(String.format(
|
||||
Locale.US,
|
||||
"Habit %d not supposed to run today. Skipping.",
|
||||
habit.getId()));
|
||||
return;
|
||||
}
|
||||
|
||||
systemTray.showNotification(habit, getNotificationId(habit), timestamp,
|
||||
reminderTime);
|
||||
}
|
||||
|
||||
private boolean shouldShowReminderToday()
|
||||
{
|
||||
if (!habit.hasReminder()) return false;
|
||||
Reminder reminder = habit.getReminder();
|
||||
|
||||
boolean reminderDays[] = reminder.getDays().toArray();
|
||||
int weekday = timestamp.getWeekday();
|
||||
|
||||
return reminderDays[weekday];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.ui;
|
||||
|
||||
import androidx.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.ui.views.*;
|
||||
|
||||
public abstract class ThemeSwitcher
|
||||
{
|
||||
public static final int THEME_DARK = 1;
|
||||
|
||||
public static final int THEME_LIGHT = 2;
|
||||
|
||||
public static final int THEME_AUTOMATIC = 0;
|
||||
|
||||
private final Preferences preferences;
|
||||
|
||||
public ThemeSwitcher(@NonNull Preferences preferences)
|
||||
{
|
||||
this.preferences = preferences;
|
||||
}
|
||||
|
||||
public void apply()
|
||||
{
|
||||
if (isNightMode())
|
||||
{
|
||||
if (preferences.isPureBlackEnabled()) applyPureBlackTheme();
|
||||
else applyDarkTheme();
|
||||
}
|
||||
else
|
||||
{
|
||||
applyLightTheme();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void applyDarkTheme();
|
||||
|
||||
public abstract void applyLightTheme();
|
||||
|
||||
public abstract void applyPureBlackTheme();
|
||||
|
||||
public abstract int getSystemTheme();
|
||||
|
||||
public abstract Theme getCurrentTheme();
|
||||
|
||||
public boolean isNightMode()
|
||||
{
|
||||
int systemTheme = getSystemTheme();
|
||||
int userTheme = preferences.getTheme();
|
||||
|
||||
return (userTheme == THEME_DARK ||
|
||||
(systemTheme == THEME_DARK && userTheme == THEME_AUTOMATIC));
|
||||
}
|
||||
|
||||
public void toggleNightMode()
|
||||
{
|
||||
int systemTheme = getSystemTheme();
|
||||
int userTheme = preferences.getTheme();
|
||||
|
||||
if(userTheme == THEME_AUTOMATIC)
|
||||
{
|
||||
if(systemTheme == THEME_LIGHT) preferences.setTheme(THEME_DARK);
|
||||
if(systemTheme == THEME_DARK) preferences.setTheme(THEME_LIGHT);
|
||||
}
|
||||
else if(userTheme == THEME_LIGHT)
|
||||
{
|
||||
if (systemTheme == THEME_LIGHT) preferences.setTheme(THEME_DARK);
|
||||
if (systemTheme == THEME_DARK) preferences.setTheme(THEME_AUTOMATIC);
|
||||
}
|
||||
else if(userTheme == THEME_DARK)
|
||||
{
|
||||
if (systemTheme == THEME_LIGHT) preferences.setTheme(THEME_AUTOMATIC);
|
||||
if (systemTheme == THEME_DARK) preferences.setTheme(THEME_LIGHT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.ui.callbacks;
|
||||
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
|
||||
public interface OnColorPickedCallback
|
||||
{
|
||||
void onColorPicked(PaletteColor color);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.ui.callbacks;
|
||||
|
||||
public interface OnConfirmedCallback
|
||||
{
|
||||
void onConfirmed();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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.ui.callbacks;
|
||||
|
||||
public interface OnFinishedCallback
|
||||
{
|
||||
void onFinish();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.ui.callbacks
|
||||
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
|
||||
interface OnToggleCheckmarkListener {
|
||||
fun onToggleEntry(timestamp: Timestamp, value: Int) {}
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.list;
|
||||
|
||||
import androidx.annotation.*;
|
||||
|
||||
import org.apache.commons.lang3.*;
|
||||
import org.isoron.uhabits.core.*;
|
||||
import org.isoron.uhabits.core.commands.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
/**
|
||||
* A HabitCardListCache fetches and keeps a cache of all the data necessary to
|
||||
* render a HabitCardListView.
|
||||
* <p>
|
||||
* This is needed since performing database lookups during scrolling can make
|
||||
* the ListView very slow. It also registers itself as an observer of the
|
||||
* models, in order to update itself automatically.
|
||||
* <p>
|
||||
* Note that this class is singleton-scoped, therefore it is shared among all
|
||||
* activities.
|
||||
*/
|
||||
@AppScope
|
||||
public class HabitCardListCache implements CommandRunner.Listener
|
||||
{
|
||||
private int checkmarkCount;
|
||||
|
||||
@Nullable
|
||||
private Task currentFetchTask;
|
||||
|
||||
@NonNull
|
||||
private Listener listener;
|
||||
|
||||
@NonNull
|
||||
private CacheData data;
|
||||
|
||||
@NonNull
|
||||
private final HabitList allHabits;
|
||||
|
||||
@NonNull
|
||||
private HabitList filteredHabits;
|
||||
|
||||
@NonNull
|
||||
private final TaskRunner taskRunner;
|
||||
|
||||
@NonNull
|
||||
private final CommandRunner commandRunner;
|
||||
|
||||
@Inject
|
||||
public HabitCardListCache(@NonNull HabitList allHabits,
|
||||
@NonNull CommandRunner commandRunner,
|
||||
@NonNull TaskRunner taskRunner)
|
||||
{
|
||||
if (allHabits == null) throw new NullPointerException();
|
||||
if (commandRunner == null) throw new NullPointerException();
|
||||
if (taskRunner == null) throw new NullPointerException();
|
||||
|
||||
this.allHabits = allHabits;
|
||||
this.commandRunner = commandRunner;
|
||||
this.filteredHabits = allHabits;
|
||||
this.taskRunner = taskRunner;
|
||||
|
||||
this.listener = new Listener()
|
||||
{
|
||||
};
|
||||
data = new CacheData();
|
||||
}
|
||||
|
||||
public synchronized void cancelTasks()
|
||||
{
|
||||
if (currentFetchTask != null) currentFetchTask.cancel();
|
||||
}
|
||||
|
||||
public synchronized int[] getCheckmarks(long habitId)
|
||||
{
|
||||
return data.checkmarks.get(habitId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the habits that occupies a certain position on the list.
|
||||
*
|
||||
* @param position the position of the habit
|
||||
* @return the habit at given position or null if position is invalid
|
||||
*/
|
||||
@Nullable
|
||||
public synchronized Habit getHabitByPosition(int position)
|
||||
{
|
||||
if (position < 0 || position >= data.habits.size()) return null;
|
||||
return data.habits.get(position);
|
||||
}
|
||||
|
||||
public synchronized int getHabitCount()
|
||||
{
|
||||
return data.habits.size();
|
||||
}
|
||||
|
||||
public synchronized HabitList.Order getPrimaryOrder()
|
||||
{
|
||||
return filteredHabits.getPrimaryOrder();
|
||||
}
|
||||
|
||||
public synchronized HabitList.Order getSecondaryOrder()
|
||||
{
|
||||
return filteredHabits.getSecondaryOrder();
|
||||
}
|
||||
|
||||
public synchronized double getScore(long habitId)
|
||||
{
|
||||
return data.scores.get(habitId);
|
||||
}
|
||||
|
||||
public synchronized void onAttached()
|
||||
{
|
||||
refreshAllHabits();
|
||||
commandRunner.addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onCommandFinished(@Nullable Command command)
|
||||
{
|
||||
if (command instanceof CreateRepetitionCommand) {
|
||||
Habit h = ((CreateRepetitionCommand) command).getHabit();
|
||||
Long id = h.getId();
|
||||
if (id != null) refreshHabit(id);
|
||||
} else {
|
||||
refreshAllHabits();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void onDetached()
|
||||
{
|
||||
commandRunner.removeListener(this);
|
||||
}
|
||||
|
||||
public synchronized void refreshAllHabits()
|
||||
{
|
||||
if (currentFetchTask != null) currentFetchTask.cancel();
|
||||
currentFetchTask = new RefreshTask();
|
||||
taskRunner.execute(currentFetchTask);
|
||||
}
|
||||
|
||||
public synchronized void refreshHabit(long id)
|
||||
{
|
||||
taskRunner.execute(new RefreshTask(id));
|
||||
}
|
||||
|
||||
public synchronized void remove(long id)
|
||||
{
|
||||
Habit h = data.id_to_habit.get(id);
|
||||
if (h == null) return;
|
||||
|
||||
int position = data.habits.indexOf(h);
|
||||
data.habits.remove(position);
|
||||
data.id_to_habit.remove(id);
|
||||
data.checkmarks.remove(id);
|
||||
data.scores.remove(id);
|
||||
|
||||
listener.onItemRemoved(position);
|
||||
}
|
||||
|
||||
public synchronized void reorder(int from, int to)
|
||||
{
|
||||
Habit fromHabit = data.habits.get(from);
|
||||
data.habits.remove(from);
|
||||
data.habits.add(to, fromHabit);
|
||||
listener.onItemMoved(from, to);
|
||||
}
|
||||
|
||||
public synchronized void setCheckmarkCount(int checkmarkCount)
|
||||
{
|
||||
this.checkmarkCount = checkmarkCount;
|
||||
}
|
||||
|
||||
public synchronized void setFilter(@NonNull HabitMatcher matcher)
|
||||
{
|
||||
if (matcher == null) throw new NullPointerException();
|
||||
filteredHabits = allHabits.getFiltered(matcher);
|
||||
}
|
||||
|
||||
public synchronized void setListener(@NonNull Listener listener)
|
||||
{
|
||||
if (listener == null) throw new NullPointerException();
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public synchronized void setPrimaryOrder(@NonNull HabitList.Order order)
|
||||
{
|
||||
if (order == null) throw new NullPointerException();
|
||||
allHabits.setPrimaryOrder(order);
|
||||
filteredHabits.setPrimaryOrder(order);
|
||||
refreshAllHabits();
|
||||
}
|
||||
|
||||
public synchronized void setSecondaryOrder(@NonNull HabitList.Order order)
|
||||
{
|
||||
allHabits.setSecondaryOrder(order);
|
||||
filteredHabits.setSecondaryOrder(order);
|
||||
refreshAllHabits();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interface definition for a callback to be invoked when the data on the
|
||||
* cache has been modified.
|
||||
*/
|
||||
public interface Listener
|
||||
{
|
||||
default void onItemChanged(int position)
|
||||
{
|
||||
}
|
||||
|
||||
default void onItemInserted(int position)
|
||||
{
|
||||
}
|
||||
|
||||
default void onItemMoved(int oldPosition, int newPosition)
|
||||
{
|
||||
}
|
||||
|
||||
default void onItemRemoved(int position)
|
||||
{
|
||||
}
|
||||
|
||||
default void onRefreshFinished()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheData
|
||||
{
|
||||
@NonNull
|
||||
public final HashMap<Long, Habit> id_to_habit;
|
||||
|
||||
@NonNull
|
||||
public final List<Habit> habits;
|
||||
|
||||
@NonNull
|
||||
public final HashMap<Long, int[]> checkmarks;
|
||||
|
||||
@NonNull
|
||||
public final HashMap<Long, Double> scores;
|
||||
|
||||
/**
|
||||
* Creates a new CacheData without any content.
|
||||
*/
|
||||
public CacheData()
|
||||
{
|
||||
id_to_habit = new HashMap<>();
|
||||
habits = new LinkedList<>();
|
||||
checkmarks = new HashMap<>();
|
||||
scores = new HashMap<>();
|
||||
}
|
||||
|
||||
public synchronized void copyCheckmarksFrom(@NonNull CacheData oldData)
|
||||
{
|
||||
if (oldData == null) throw new NullPointerException();
|
||||
|
||||
int[] empty = new int[checkmarkCount];
|
||||
|
||||
for (Long id : id_to_habit.keySet())
|
||||
{
|
||||
if (oldData.checkmarks.containsKey(id))
|
||||
checkmarks.put(id, oldData.checkmarks.get(id));
|
||||
else checkmarks.put(id, empty);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void copyScoresFrom(@NonNull CacheData oldData)
|
||||
{
|
||||
if (oldData == null) throw new NullPointerException();
|
||||
|
||||
for (Long id : id_to_habit.keySet())
|
||||
{
|
||||
if (oldData.scores.containsKey(id))
|
||||
scores.put(id, oldData.scores.get(id));
|
||||
else scores.put(id, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void fetchHabits()
|
||||
{
|
||||
for (Habit h : filteredHabits)
|
||||
{
|
||||
if (h.getId() == null) continue;
|
||||
habits.add(h);
|
||||
id_to_habit.put(h.getId(), h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RefreshTask implements Task
|
||||
{
|
||||
@NonNull
|
||||
private final CacheData newData;
|
||||
|
||||
@Nullable
|
||||
private final Long targetId;
|
||||
|
||||
private boolean isCancelled;
|
||||
|
||||
@Nullable
|
||||
private TaskRunner runner;
|
||||
|
||||
public RefreshTask()
|
||||
{
|
||||
newData = new CacheData();
|
||||
targetId = null;
|
||||
isCancelled = false;
|
||||
}
|
||||
|
||||
public RefreshTask(long targetId)
|
||||
{
|
||||
newData = new CacheData();
|
||||
this.targetId = targetId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void cancel()
|
||||
{
|
||||
isCancelled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void doInBackground()
|
||||
{
|
||||
newData.fetchHabits();
|
||||
newData.copyScoresFrom(data);
|
||||
newData.copyCheckmarksFrom(data);
|
||||
|
||||
Timestamp today = DateUtils.getTodayWithOffset();
|
||||
Timestamp dateFrom = today.minus(checkmarkCount - 1);
|
||||
|
||||
if (runner != null) runner.publishProgress(this, -1);
|
||||
|
||||
for (int position = 0; position < newData.habits.size(); position++)
|
||||
{
|
||||
if (isCancelled) return;
|
||||
|
||||
Habit habit = newData.habits.get(position);
|
||||
Long id = habit.getId();
|
||||
if (targetId != null && !targetId.equals(id)) continue;
|
||||
|
||||
newData.scores.put(id, habit.getScores().get(today).getValue());
|
||||
Integer[] entries = habit.getComputedEntries()
|
||||
.getByInterval(dateFrom, today)
|
||||
.stream()
|
||||
.map(Entry::getValue)
|
||||
.toArray(Integer[]::new);
|
||||
newData.checkmarks.put(id, ArrayUtils.toPrimitive(entries));
|
||||
|
||||
runner.publishProgress(this, position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onAttached(@NonNull TaskRunner runner)
|
||||
{
|
||||
if (runner == null) throw new NullPointerException();
|
||||
this.runner = runner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onPostExecute()
|
||||
{
|
||||
currentFetchTask = null;
|
||||
listener.onRefreshFinished();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onProgressUpdate(int currentPosition)
|
||||
{
|
||||
if (currentPosition < 0) processRemovedHabits();
|
||||
else processPosition(currentPosition);
|
||||
}
|
||||
|
||||
private synchronized void performInsert(Habit habit, int position)
|
||||
{
|
||||
Long id = habit.getId();
|
||||
data.habits.add(position, habit);
|
||||
data.id_to_habit.put(id, habit);
|
||||
data.scores.put(id, newData.scores.get(id));
|
||||
data.checkmarks.put(id, newData.checkmarks.get(id));
|
||||
listener.onItemInserted(position);
|
||||
}
|
||||
|
||||
private synchronized void performMove(@NonNull Habit habit,
|
||||
int fromPosition,
|
||||
int toPosition)
|
||||
{
|
||||
if(habit == null) throw new NullPointerException();
|
||||
data.habits.remove(fromPosition);
|
||||
data.habits.add(toPosition, habit);
|
||||
listener.onItemMoved(fromPosition, toPosition);
|
||||
}
|
||||
|
||||
private synchronized void performUpdate(long id, int position)
|
||||
{
|
||||
double oldScore = data.scores.get(id);
|
||||
int[] oldCheckmarks = data.checkmarks.get(id);
|
||||
|
||||
double newScore = newData.scores.get(id);
|
||||
int[] newCheckmarks = newData.checkmarks.get(id);
|
||||
|
||||
boolean unchanged = true;
|
||||
if (oldScore != newScore) unchanged = false;
|
||||
if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false;
|
||||
if (unchanged) return;
|
||||
|
||||
data.scores.put(id, newScore);
|
||||
data.checkmarks.put(id, newCheckmarks);
|
||||
listener.onItemChanged(position);
|
||||
}
|
||||
|
||||
private synchronized void processPosition(int currentPosition)
|
||||
{
|
||||
Habit habit = newData.habits.get(currentPosition);
|
||||
Long id = habit.getId();
|
||||
|
||||
int prevPosition = data.habits.indexOf(habit);
|
||||
|
||||
if (prevPosition < 0)
|
||||
{
|
||||
performInsert(habit, currentPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (prevPosition != currentPosition)
|
||||
performMove(habit, prevPosition, currentPosition);
|
||||
|
||||
performUpdate(id, currentPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void processRemovedHabits()
|
||||
{
|
||||
Set<Long> before = data.id_to_habit.keySet();
|
||||
Set<Long> after = newData.id_to_habit.keySet();
|
||||
|
||||
Set<Long> removed = new TreeSet<>(before);
|
||||
removed.removeAll(after);
|
||||
|
||||
for (Long id : removed) remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.list;
|
||||
|
||||
import androidx.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
|
||||
/**
|
||||
* Provides a list of hints to be shown at the application startup, and takes
|
||||
* care of deciding when a new hint should be shown.
|
||||
*/
|
||||
public class HintList
|
||||
{
|
||||
private final Preferences prefs;
|
||||
|
||||
@NonNull
|
||||
private final String[] hints;
|
||||
|
||||
/**
|
||||
* Constructs a new list containing the provided hints.
|
||||
*
|
||||
* @param hints initial list of hints
|
||||
*/
|
||||
public HintList(@NonNull Preferences prefs,
|
||||
@NonNull String hints[])
|
||||
{
|
||||
this.prefs = prefs;
|
||||
this.hints = hints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new hint to be shown to the user.
|
||||
* <p>
|
||||
* The hint returned is marked as read on the list, and will not be returned
|
||||
* again. In case all hints have already been read, and there is nothing
|
||||
* left, returns null.
|
||||
*
|
||||
* @return the next hint to be shown, or null if none
|
||||
*/
|
||||
public String pop()
|
||||
{
|
||||
int next = prefs.getLastHintNumber() + 1;
|
||||
if (next >= hints.length) return null;
|
||||
|
||||
prefs.updateLastHint(next, DateUtils.getToday());
|
||||
return hints[next];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether it is time to show a new hint or not.
|
||||
*
|
||||
* @return true if hint should be shown, false otherwise
|
||||
*/
|
||||
public boolean shouldShow()
|
||||
{
|
||||
Timestamp today = DateUtils.getToday();
|
||||
Timestamp lastHintTimestamp = prefs.getLastHintTimestamp();
|
||||
return (lastHintTimestamp.isOlderThan(today));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.list
|
||||
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import javax.inject.Inject
|
||||
|
||||
class HintListFactory
|
||||
@Inject constructor(
|
||||
val preferences: Preferences,
|
||||
) {
|
||||
fun create(hints: Array<String>) = HintList(preferences, hints)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.list;
|
||||
|
||||
import androidx.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.commands.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.tasks.*;
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
import org.isoron.uhabits.core.utils.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
public class ListHabitsBehavior
|
||||
{
|
||||
@NonNull
|
||||
private final HabitList habitList;
|
||||
|
||||
@NonNull
|
||||
private final DirFinder dirFinder;
|
||||
|
||||
@NonNull
|
||||
private final TaskRunner taskRunner;
|
||||
|
||||
@NonNull
|
||||
private final Screen screen;
|
||||
|
||||
@NonNull
|
||||
private final CommandRunner commandRunner;
|
||||
|
||||
@NonNull
|
||||
private final Preferences prefs;
|
||||
|
||||
@NonNull
|
||||
private final BugReporter bugReporter;
|
||||
|
||||
@Inject
|
||||
public ListHabitsBehavior(@NonNull HabitList habitList,
|
||||
@NonNull DirFinder dirFinder,
|
||||
@NonNull TaskRunner taskRunner,
|
||||
@NonNull Screen screen,
|
||||
@NonNull CommandRunner commandRunner,
|
||||
@NonNull Preferences prefs,
|
||||
@NonNull BugReporter bugReporter)
|
||||
{
|
||||
this.habitList = habitList;
|
||||
this.dirFinder = dirFinder;
|
||||
this.taskRunner = taskRunner;
|
||||
this.screen = screen;
|
||||
this.commandRunner = commandRunner;
|
||||
this.prefs = prefs;
|
||||
this.bugReporter = bugReporter;
|
||||
}
|
||||
|
||||
public void onClickHabit(@NonNull Habit h)
|
||||
{
|
||||
screen.showHabitScreen(h);
|
||||
}
|
||||
|
||||
public void onEdit(@NonNull Habit habit, Timestamp timestamp)
|
||||
{
|
||||
EntryList entries = habit.getComputedEntries();
|
||||
double oldValue = entries.get(timestamp).getValue();
|
||||
|
||||
screen.showNumberPicker(oldValue / 1000, habit.getUnit(), newValue ->
|
||||
{
|
||||
newValue = Math.round(newValue * 1000);
|
||||
commandRunner.run(
|
||||
new CreateRepetitionCommand(habitList, habit, timestamp, (int) newValue)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public void onExportCSV()
|
||||
{
|
||||
List<Habit> selected = new LinkedList<>();
|
||||
for (Habit h : habitList) selected.add(h);
|
||||
File outputDir = dirFinder.getCSVOutputDir();
|
||||
|
||||
taskRunner.execute(
|
||||
new ExportCSVTask(habitList, selected, outputDir, filename ->
|
||||
{
|
||||
if (filename != null) screen.showSendFileScreen(filename);
|
||||
else screen.showMessage(Message.COULD_NOT_EXPORT);
|
||||
}));
|
||||
}
|
||||
|
||||
public void onFirstRun()
|
||||
{
|
||||
prefs.setFirstRun(false);
|
||||
prefs.updateLastHint(-1, DateUtils.getToday());
|
||||
screen.showIntroScreen();
|
||||
}
|
||||
|
||||
public void onReorderHabit(@NonNull Habit from, @NonNull Habit to)
|
||||
{
|
||||
taskRunner.execute(() -> habitList.reorder(from, to));
|
||||
}
|
||||
|
||||
public void onRepairDB()
|
||||
{
|
||||
taskRunner.execute(() ->
|
||||
{
|
||||
habitList.repair();
|
||||
screen.showMessage(Message.DATABASE_REPAIRED);
|
||||
});
|
||||
}
|
||||
|
||||
public void onSendBugReport()
|
||||
{
|
||||
bugReporter.dumpBugReportToFile();
|
||||
|
||||
try
|
||||
{
|
||||
String log = bugReporter.getBugReport();
|
||||
screen.showSendBugReportToDeveloperScreen(log);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
screen.showMessage(Message.COULD_NOT_GENERATE_BUG_REPORT);
|
||||
}
|
||||
}
|
||||
|
||||
public void onStartup()
|
||||
{
|
||||
prefs.incrementLaunchCount();
|
||||
if (prefs.isFirstRun()) onFirstRun();
|
||||
}
|
||||
|
||||
public void onToggle(@NonNull Habit habit, Timestamp timestamp, int value)
|
||||
{
|
||||
commandRunner.run(
|
||||
new CreateRepetitionCommand(habitList, habit, timestamp, value)
|
||||
);
|
||||
}
|
||||
|
||||
public void onSyncKeyOffer(@NotNull String syncKey, @NotNull String encryptionKey)
|
||||
{
|
||||
if(prefs.getSyncKey().equals(syncKey)) {
|
||||
screen.showMessage(Message.SYNC_KEY_ALREADY_INSTALLED);
|
||||
return;
|
||||
}
|
||||
screen.showConfirmInstallSyncKey(() -> {
|
||||
prefs.enableSync(syncKey, encryptionKey);
|
||||
screen.showMessage(Message.SYNC_ENABLED);
|
||||
});
|
||||
}
|
||||
|
||||
public enum Message
|
||||
{
|
||||
COULD_NOT_EXPORT, IMPORT_SUCCESSFUL, IMPORT_FAILED, DATABASE_REPAIRED,
|
||||
COULD_NOT_GENERATE_BUG_REPORT, FILE_NOT_RECOGNIZED, SYNC_ENABLED, SYNC_KEY_ALREADY_INSTALLED
|
||||
}
|
||||
|
||||
public interface BugReporter
|
||||
{
|
||||
void dumpBugReportToFile();
|
||||
|
||||
String getBugReport() throws IOException;
|
||||
}
|
||||
|
||||
public interface DirFinder
|
||||
{
|
||||
File getCSVOutputDir();
|
||||
}
|
||||
|
||||
public interface NumberPickerCallback
|
||||
{
|
||||
void onNumberPicked(double newValue);
|
||||
|
||||
default void onNumberPickerDismissed() {}
|
||||
}
|
||||
|
||||
public interface Screen
|
||||
{
|
||||
void showHabitScreen(@NonNull Habit h);
|
||||
|
||||
void showIntroScreen();
|
||||
|
||||
void showMessage(@NonNull Message m);
|
||||
|
||||
void showNumberPicker(double value,
|
||||
@NonNull String unit,
|
||||
@NonNull NumberPickerCallback callback);
|
||||
|
||||
void showSendBugReportToDeveloperScreen(String log);
|
||||
|
||||
void showSendFileScreen(@NonNull String filename);
|
||||
|
||||
void showConfirmInstallSyncKey(@NonNull OnConfirmedCallback callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.list;
|
||||
|
||||
import androidx.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.preferences.*;
|
||||
import org.isoron.uhabits.core.ui.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
public class ListHabitsMenuBehavior
|
||||
{
|
||||
@NonNull
|
||||
private final Screen screen;
|
||||
|
||||
@NonNull
|
||||
private final Adapter adapter;
|
||||
|
||||
@NonNull
|
||||
private final Preferences preferences;
|
||||
|
||||
@NonNull
|
||||
private final ThemeSwitcher themeSwitcher;
|
||||
|
||||
private boolean showCompleted;
|
||||
|
||||
private boolean showArchived;
|
||||
|
||||
@Inject
|
||||
public ListHabitsMenuBehavior(@NonNull Screen screen,
|
||||
@NonNull Adapter adapter,
|
||||
@NonNull Preferences preferences,
|
||||
@NonNull ThemeSwitcher themeSwitcher)
|
||||
{
|
||||
this.screen = screen;
|
||||
this.adapter = adapter;
|
||||
this.preferences = preferences;
|
||||
this.themeSwitcher = themeSwitcher;
|
||||
|
||||
showCompleted = preferences.getShowCompleted();
|
||||
showArchived = preferences.getShowArchived();
|
||||
updateAdapterFilter();
|
||||
}
|
||||
|
||||
public void onCreateHabit()
|
||||
{
|
||||
screen.showSelectHabitTypeDialog();
|
||||
}
|
||||
|
||||
public void onViewFAQ()
|
||||
{
|
||||
screen.showFAQScreen();
|
||||
}
|
||||
|
||||
public void onViewAbout()
|
||||
{
|
||||
screen.showAboutScreen();
|
||||
}
|
||||
|
||||
public void onViewSettings()
|
||||
{
|
||||
screen.showSettingsScreen();
|
||||
}
|
||||
|
||||
public void onToggleShowArchived()
|
||||
{
|
||||
showArchived = !showArchived;
|
||||
preferences.setShowArchived(showArchived);
|
||||
updateAdapterFilter();
|
||||
}
|
||||
|
||||
public void onToggleShowCompleted()
|
||||
{
|
||||
showCompleted = !showCompleted;
|
||||
preferences.setShowCompleted(showCompleted);
|
||||
updateAdapterFilter();
|
||||
}
|
||||
|
||||
public void onSortByManually()
|
||||
{
|
||||
adapter.setPrimaryOrder(HabitList.Order.BY_POSITION);
|
||||
}
|
||||
|
||||
public void onSortByColor()
|
||||
{
|
||||
onSortToggleBy(HabitList.Order.BY_COLOR_ASC, HabitList.Order.BY_COLOR_DESC);
|
||||
}
|
||||
|
||||
|
||||
public void onSortByScore()
|
||||
{
|
||||
onSortToggleBy(HabitList.Order.BY_SCORE_DESC, HabitList.Order.BY_SCORE_ASC);
|
||||
}
|
||||
|
||||
public void onSortByName()
|
||||
{
|
||||
onSortToggleBy(HabitList.Order.BY_NAME_ASC, HabitList.Order.BY_NAME_DESC);
|
||||
}
|
||||
|
||||
public void onSortByStatus()
|
||||
{
|
||||
onSortToggleBy(HabitList.Order.BY_STATUS_ASC, HabitList.Order.BY_STATUS_DESC);
|
||||
}
|
||||
|
||||
private void onSortToggleBy(HabitList.Order defaultOrder, HabitList.Order reversedOrder)
|
||||
{
|
||||
if (adapter.getPrimaryOrder() != defaultOrder) {
|
||||
if (adapter.getPrimaryOrder() != reversedOrder) {
|
||||
adapter.setSecondaryOrder(adapter.getPrimaryOrder());
|
||||
}
|
||||
adapter.setPrimaryOrder(defaultOrder);
|
||||
} else {
|
||||
adapter.setPrimaryOrder(reversedOrder);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void onToggleNightMode()
|
||||
{
|
||||
themeSwitcher.toggleNightMode();
|
||||
screen.applyTheme();
|
||||
}
|
||||
|
||||
private void updateAdapterFilter()
|
||||
{
|
||||
adapter.setFilter(new HabitMatcherBuilder()
|
||||
.setArchivedAllowed(showArchived)
|
||||
.setCompletedAllowed(showCompleted)
|
||||
.build());
|
||||
adapter.refresh();
|
||||
}
|
||||
|
||||
public interface Adapter
|
||||
{
|
||||
void refresh();
|
||||
|
||||
void setFilter(HabitMatcher build);
|
||||
|
||||
void setPrimaryOrder(HabitList.Order order);
|
||||
|
||||
void setSecondaryOrder(HabitList.Order order);
|
||||
|
||||
HabitList.Order getPrimaryOrder();
|
||||
}
|
||||
|
||||
public interface Screen
|
||||
{
|
||||
void applyTheme();
|
||||
|
||||
void showAboutScreen();
|
||||
|
||||
void showFAQScreen();
|
||||
|
||||
void showSettingsScreen();
|
||||
|
||||
void showSelectHabitTypeDialog();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.list;
|
||||
|
||||
import androidx.annotation.*;
|
||||
|
||||
import org.isoron.uhabits.core.commands.*;
|
||||
import org.isoron.uhabits.core.models.*;
|
||||
import org.isoron.uhabits.core.ui.callbacks.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import javax.inject.*;
|
||||
|
||||
public class ListHabitsSelectionMenuBehavior
|
||||
{
|
||||
@NonNull
|
||||
private final Screen screen;
|
||||
|
||||
@NonNull
|
||||
CommandRunner commandRunner;
|
||||
|
||||
@NonNull
|
||||
private final Adapter adapter;
|
||||
|
||||
@NonNull
|
||||
private final HabitList habitList;
|
||||
|
||||
@Inject
|
||||
public ListHabitsSelectionMenuBehavior(@NonNull HabitList habitList,
|
||||
@NonNull Screen screen,
|
||||
@NonNull Adapter adapter,
|
||||
@NonNull CommandRunner commandRunner)
|
||||
{
|
||||
this.habitList = habitList;
|
||||
this.screen = screen;
|
||||
this.adapter = adapter;
|
||||
this.commandRunner = commandRunner;
|
||||
}
|
||||
|
||||
public boolean canArchive()
|
||||
{
|
||||
for (Habit h : adapter.getSelected())
|
||||
if (h.isArchived()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean canEdit()
|
||||
{
|
||||
return (adapter.getSelected().size() == 1);
|
||||
}
|
||||
|
||||
public boolean canUnarchive()
|
||||
{
|
||||
for (Habit h : adapter.getSelected())
|
||||
if (!h.isArchived()) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onArchiveHabits()
|
||||
{
|
||||
commandRunner.run(
|
||||
new ArchiveHabitsCommand(habitList, adapter.getSelected()));
|
||||
adapter.clearSelection();
|
||||
}
|
||||
|
||||
public void onChangeColor()
|
||||
{
|
||||
List<Habit> selected = adapter.getSelected();
|
||||
Habit first = selected.get(0);
|
||||
|
||||
screen.showColorPicker(first.getColor(), selectedColor ->
|
||||
{
|
||||
commandRunner.run(
|
||||
new ChangeHabitColorCommand(habitList, selected, selectedColor)
|
||||
);
|
||||
adapter.clearSelection();
|
||||
});
|
||||
}
|
||||
|
||||
public void onDeleteHabits()
|
||||
{
|
||||
List<Habit> selected = adapter.getSelected();
|
||||
screen.showDeleteConfirmationScreen(() ->
|
||||
{
|
||||
adapter.performRemove(selected);
|
||||
commandRunner.run(new DeleteHabitsCommand(habitList, selected)
|
||||
);
|
||||
adapter.clearSelection();
|
||||
}, selected.size());
|
||||
}
|
||||
|
||||
public void onEditHabits()
|
||||
{
|
||||
screen.showEditHabitsScreen(adapter.getSelected());
|
||||
adapter.clearSelection();
|
||||
}
|
||||
|
||||
public void onUnarchiveHabits()
|
||||
{
|
||||
commandRunner.run(
|
||||
new UnarchiveHabitsCommand(habitList, adapter.getSelected()));
|
||||
adapter.clearSelection();
|
||||
}
|
||||
|
||||
public interface Adapter
|
||||
{
|
||||
void clearSelection();
|
||||
|
||||
List<Habit> getSelected();
|
||||
|
||||
void performRemove(List<Habit> selected);
|
||||
}
|
||||
|
||||
public interface Screen
|
||||
{
|
||||
void showColorPicker(PaletteColor defaultColor,
|
||||
@NonNull OnColorPickedCallback callback);
|
||||
|
||||
void showDeleteConfirmationScreen(
|
||||
@NonNull OnConfirmedCallback callback,
|
||||
int quantity);
|
||||
|
||||
void showEditHabitsScreen(@NonNull List<Habit> selected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.show
|
||||
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.HabitList
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.BarCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.FrequencyCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.HistoryCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.NotesCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.OverviewCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.ScoreCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.StreakCartPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.SubtitleCardState
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardPresenter
|
||||
import org.isoron.uhabits.core.ui.screens.habits.show.views.TargetCardState
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
|
||||
data class ShowHabitState(
|
||||
val title: String = "",
|
||||
val isNumerical: Boolean = false,
|
||||
val color: PaletteColor = PaletteColor(1),
|
||||
val subtitle: SubtitleCardState,
|
||||
val overview: OverviewCardState,
|
||||
val notes: NotesCardState,
|
||||
val target: TargetCardState,
|
||||
val streaks: StreakCardState,
|
||||
val scores: ScoreCardState,
|
||||
val frequency: FrequencyCardState,
|
||||
val history: HistoryCardState,
|
||||
val bar: BarCardState,
|
||||
)
|
||||
|
||||
class ShowHabitPresenter(
|
||||
val habit: Habit,
|
||||
val habitList: HabitList,
|
||||
val preferences: Preferences,
|
||||
val screen: Screen,
|
||||
val commandRunner: CommandRunner,
|
||||
) {
|
||||
val historyCardPresenter = HistoryCardPresenter(
|
||||
commandRunner = commandRunner,
|
||||
habit = habit,
|
||||
habitList = habitList,
|
||||
preferences = preferences,
|
||||
screen = screen,
|
||||
)
|
||||
|
||||
val barCardPresenter = BarCardPresenter(
|
||||
preferences = preferences,
|
||||
screen = screen,
|
||||
)
|
||||
|
||||
val scoreCardPresenter = ScoreCardPresenter(
|
||||
preferences = preferences,
|
||||
screen = screen,
|
||||
)
|
||||
|
||||
companion object {
|
||||
fun buildState(
|
||||
habit: Habit,
|
||||
preferences: Preferences,
|
||||
theme: Theme,
|
||||
): ShowHabitState {
|
||||
return ShowHabitState(
|
||||
title = habit.name,
|
||||
color = habit.color,
|
||||
isNumerical = habit.isNumerical,
|
||||
subtitle = SubtitleCardPresenter.buildState(
|
||||
habit = habit,
|
||||
),
|
||||
overview = OverviewCardPresenter.buildState(
|
||||
habit = habit,
|
||||
),
|
||||
notes = NotesCardPresenter.buildState(
|
||||
habit = habit,
|
||||
),
|
||||
target = TargetCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
),
|
||||
streaks = StreakCartPresenter.buildState(
|
||||
habit = habit,
|
||||
),
|
||||
scores = ScoreCardPresenter.buildState(
|
||||
spinnerPosition = preferences.scoreCardSpinnerPosition,
|
||||
habit = habit,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
),
|
||||
frequency = FrequencyCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
),
|
||||
history = HistoryCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = preferences.firstWeekday,
|
||||
theme = theme,
|
||||
),
|
||||
bar = BarCardPresenter.buildState(
|
||||
habit = habit,
|
||||
firstWeekday = preferences.firstWeekdayInt,
|
||||
boolSpinnerPosition = preferences.barCardBoolSpinnerPosition,
|
||||
numericalSpinnerPosition = preferences.barCardNumericalSpinnerPosition,
|
||||
theme = theme,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
interface Screen :
|
||||
BarCardPresenter.Screen,
|
||||
ScoreCardPresenter.Screen,
|
||||
HistoryCardPresenter.Screen
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.show
|
||||
|
||||
import org.isoron.uhabits.core.commands.CommandRunner
|
||||
import org.isoron.uhabits.core.commands.DeleteHabitsCommand
|
||||
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.tasks.ExportCSVTask
|
||||
import org.isoron.uhabits.core.tasks.TaskRunner
|
||||
import org.isoron.uhabits.core.ui.callbacks.OnConfirmedCallback
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
import java.io.File
|
||||
import java.util.Random
|
||||
|
||||
class ShowHabitMenuPresenter(
|
||||
private val commandRunner: CommandRunner,
|
||||
private val habit: Habit,
|
||||
private val habitList: HabitList,
|
||||
private val screen: Screen,
|
||||
private val system: System,
|
||||
private val taskRunner: TaskRunner,
|
||||
) {
|
||||
fun onEditHabit() {
|
||||
screen.showEditHabitScreen(habit)
|
||||
}
|
||||
|
||||
fun onExportCSV() {
|
||||
val outputDir = system.getCSVOutputDir()
|
||||
taskRunner.execute(
|
||||
ExportCSVTask(habitList, listOf(habit), outputDir) { filename: String? ->
|
||||
if (filename != null) {
|
||||
screen.showSendFileScreen(filename)
|
||||
} else {
|
||||
screen.showMessage(Message.COULD_NOT_EXPORT)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun onDeleteHabit() {
|
||||
screen.showDeleteConfirmationScreen {
|
||||
commandRunner.run(DeleteHabitsCommand(habitList, listOf(habit)))
|
||||
screen.close()
|
||||
}
|
||||
}
|
||||
|
||||
fun onRandomize() {
|
||||
val random = Random()
|
||||
habit.originalEntries.clear()
|
||||
var strength = 50.0
|
||||
for (i in 0 until 365 * 5) {
|
||||
if (i % 7 == 0) strength = Math.max(0.0, Math.min(100.0, strength + 10 * random.nextGaussian()))
|
||||
if (random.nextInt(100) > strength) continue
|
||||
var value = Entry.YES_MANUAL
|
||||
if (habit.isNumerical) value = (1000 + 250 * random.nextGaussian() * strength / 100).toInt() * 1000
|
||||
habit.originalEntries.add(Entry(DateUtils.getToday().minus(i), value))
|
||||
}
|
||||
habit.recompute()
|
||||
screen.refresh()
|
||||
}
|
||||
|
||||
enum class Message {
|
||||
COULD_NOT_EXPORT
|
||||
}
|
||||
|
||||
interface Screen {
|
||||
fun showEditHabitScreen(habit: Habit)
|
||||
fun showMessage(m: Message?)
|
||||
fun showSendFileScreen(filename: String)
|
||||
fun showDeleteConfirmationScreen(callback: OnConfirmedCallback)
|
||||
fun close()
|
||||
fun refresh()
|
||||
}
|
||||
|
||||
interface System {
|
||||
fun getCSVOutputDir(): File
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.show.views
|
||||
|
||||
import org.isoron.uhabits.core.models.Entry
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.groupedSum
|
||||
import org.isoron.uhabits.core.preferences.Preferences
|
||||
import org.isoron.uhabits.core.ui.views.Theme
|
||||
import org.isoron.uhabits.core.utils.DateUtils
|
||||
|
||||
data class BarCardState(
|
||||
val theme: Theme,
|
||||
val boolSpinnerPosition: Int,
|
||||
val bucketSize: Int,
|
||||
val color: PaletteColor,
|
||||
val entries: List<Entry>,
|
||||
val isNumerical: Boolean,
|
||||
val numericalSpinnerPosition: Int,
|
||||
)
|
||||
|
||||
class BarCardPresenter(
|
||||
val preferences: Preferences,
|
||||
val screen: Screen,
|
||||
) {
|
||||
companion object {
|
||||
val numericalBucketSizes = intArrayOf(1, 7, 31, 92, 365)
|
||||
val boolBucketSizes = intArrayOf(7, 31, 92, 365)
|
||||
|
||||
fun buildState(
|
||||
habit: Habit,
|
||||
firstWeekday: Int,
|
||||
numericalSpinnerPosition: Int,
|
||||
boolSpinnerPosition: Int,
|
||||
theme: Theme,
|
||||
): BarCardState {
|
||||
val bucketSize = if (habit.isNumerical) {
|
||||
numericalBucketSizes[numericalSpinnerPosition]
|
||||
} else {
|
||||
boolBucketSizes[boolSpinnerPosition]
|
||||
}
|
||||
val today = DateUtils.getToday()
|
||||
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
|
||||
val entries = habit.computedEntries.getByInterval(oldest, today).groupedSum(
|
||||
truncateField = ScoreCardPresenter.getTruncateField(bucketSize),
|
||||
firstWeekday = firstWeekday,
|
||||
isNumerical = habit.isNumerical,
|
||||
)
|
||||
return BarCardState(
|
||||
theme = theme,
|
||||
entries = entries,
|
||||
bucketSize = bucketSize,
|
||||
color = habit.color,
|
||||
isNumerical = habit.isNumerical,
|
||||
numericalSpinnerPosition = numericalSpinnerPosition,
|
||||
boolSpinnerPosition = boolSpinnerPosition,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onNumericalSpinnerPosition(position: Int) {
|
||||
preferences.barCardNumericalSpinnerPosition = position
|
||||
screen.updateWidgets()
|
||||
screen.refresh()
|
||||
}
|
||||
|
||||
fun onBoolSpinnerPosition(position: Int) {
|
||||
preferences.barCardBoolSpinnerPosition = position
|
||||
screen.updateWidgets()
|
||||
screen.refresh()
|
||||
}
|
||||
|
||||
interface Screen {
|
||||
fun updateWidgets()
|
||||
fun refresh()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.ui.screens.habits.show.views
|
||||
|
||||
import org.isoron.uhabits.core.models.Habit
|
||||
import org.isoron.uhabits.core.models.PaletteColor
|
||||
import org.isoron.uhabits.core.models.Timestamp
|
||||
import java.util.HashMap
|
||||
|
||||
data class FrequencyCardState(
|
||||
val color: PaletteColor,
|
||||
val firstWeekday: Int,
|
||||
val frequency: HashMap<Timestamp, Array<Int>>,
|
||||
)
|
||||
|
||||
class FrequencyCardPresenter {
|
||||
companion object {
|
||||
fun buildState(
|
||||
habit: Habit,
|
||||
firstWeekday: Int,
|
||||
) = FrequencyCardState(
|
||||
color = habit.color,
|
||||
frequency = habit.originalEntries.computeWeekdayFrequency(
|
||||
isNumerical = habit.isNumerical
|
||||
),
|
||||
firstWeekday = firstWeekday,
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user