Merge branch 'dev' into feature/sync

This commit is contained in:
2021-08-22 05:13:37 -05:00
165 changed files with 2313 additions and 1415 deletions

View File

@@ -43,13 +43,13 @@ kotlin {
val jvmMain by getting {
dependencies {
implementation(kotlin("stdlib-jdk8"))
compileOnly("com.google.dagger:dagger:2.35.1")
compileOnly("com.google.dagger:dagger:2.38.1")
implementation("com.google.guava:guava:30.1.1-android")
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.4.2")
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.21")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1")
implementation("androidx.annotation:annotation:1.2.0")
implementation("com.google.code.findbugs:jsr305:3.0.2")
implementation("com.opencsv:opencsv:5.4")
implementation("com.opencsv:opencsv:5.5.1")
implementation("commons-codec:commons-codec:1.15")
implementation("org.apache.commons:commons-lang3:3.12.0")
}
@@ -59,7 +59,7 @@ kotlin {
dependencies {
implementation(kotlin("test"))
implementation(kotlin("test-junit"))
implementation("org.xerial:sqlite-jdbc:3.34.0")
implementation("org.xerial:sqlite-jdbc:3.36.0.1")
implementation("org.hamcrest:hamcrest:2.2")
implementation("org.apache.commons:commons-io:1.3.2")
implementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
@@ -67,3 +67,10 @@ kotlin {
}
}
}
tasks.named<org.gradle.language.jvm.tasks.ProcessResources>("jvmProcessResources") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
tasks.named<org.gradle.language.jvm.tasks.ProcessResources>("jvmTestProcessResources") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}

View File

@@ -72,10 +72,20 @@ data class LocalDate(val daysSince2000: Int) {
return dayCache
}
val monthLength: Int
get() = when (month) {
4, 6, 9, 11 -> 30
2 -> if (isLeapYear(year)) 29 else 28
else -> 31
}
private fun updateYearMonthDayCache() {
var currYear = 2000
var currDay = 0
if (daysSince2000 < 0) {
currYear -= 400
currDay -= 146097
}
while (true) {
val currYearLength = if (isLeapYear(currYear)) 366 else 365
if (daysSince2000 < currDay + currYearLength) {
@@ -86,10 +96,8 @@ data class LocalDate(val daysSince2000: Int) {
currDay += currYearLength
}
}
var currMonth = 1
val monthOffset = if (isLeapYear(currYear)) leapOffset else nonLeapOffset
while (true) {
if (daysSince2000 < currDay + monthOffset[currMonth]) {
monthCache = currMonth
@@ -98,7 +106,6 @@ data class LocalDate(val daysSince2000: Int) {
currMonth++
}
}
currDay += monthOffset[currMonth - 1]
dayCache = daysSince2000 - currDay + 1
}

View File

@@ -248,8 +248,17 @@ open class EntryList {
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)
var size = den
if (den == 30 || den == 31) {
val beginDate = begin.toLocalDate()
size = if (beginDate.day == beginDate.monthLength) {
beginDate.plus(1).monthLength
} else {
beginDate.monthLength
}
}
if (begin.daysUntil(center) < size) {
val end = begin.plus(size - 1)
intervals.add(Interval(begin, center, end))
}
}

View File

@@ -31,9 +31,9 @@ data class Habit(
var position: Int = 0,
var question: String = "",
var reminder: Reminder? = null,
var targetType: Int = AT_LEAST,
var targetType: NumericalHabitType = NumericalHabitType.AT_LEAST,
var targetValue: Double = 0.0,
var type: Int = YES_NO_HABIT,
var type: HabitType = HabitType.YES_NO,
var unit: String = "",
var uuid: String? = null,
val computedEntries: EntryList,
@@ -48,7 +48,7 @@ data class Habit(
var observable = ModelObservable()
val isNumerical: Boolean
get() = type == NUMBER_HABIT
get() = type == HabitType.NUMERICAL
val uriString: String
get() = "content://org.isoron.uhabits/habit/$id"
@@ -59,16 +59,28 @@ data class Habit(
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
when (targetType) {
NumericalHabitType.AT_LEAST -> value / 1000.0 >= targetValue
NumericalHabitType.AT_MOST -> value / 1000.0 <= targetValue
}
} else {
value != Entry.NO && value != Entry.UNKNOWN
}
}
fun isFailedToday(): Boolean {
val today = DateUtils.getTodayWithOffset()
val value = computedEntries.get(today).value
return if (isNumerical) {
when (targetType) {
NumericalHabitType.AT_LEAST -> value / 1000.0 < targetValue
NumericalHabitType.AT_MOST -> value / 1000.0 > targetValue
}
} else {
value == Entry.NO
}
}
fun recompute() {
computedEntries.recomputeFrom(
originalEntries = originalEntries,
@@ -146,18 +158,11 @@ data class Habit(
result = 31 * result + position
result = 31 * result + question.hashCode()
result = 31 * result + (reminder?.hashCode() ?: 0)
result = 31 * result + targetType
result = 31 * result + targetType.value
result = 31 * result + targetValue.hashCode()
result = 31 * result + type
result = 31 * result + type.value
result = 31 * result + unit.hashCode()
result = 31 * result + (uuid?.hashCode() ?: 0)
return result
}
companion object {
const val AT_LEAST = 0
const val AT_MOST = 1
const val NUMBER_HABIT = 1
const val YES_NO_HABIT = 0
}
}

View File

@@ -26,7 +26,7 @@ data class HabitMatcher(
fun matches(habit: Habit): Boolean {
if (!isArchivedAllowed && habit.isArchived) return false
if (isReminderRequired && !habit.hasReminder()) return false
if (!isCompletedAllowed && habit.isCompletedToday()) return false
if (!isCompletedAllowed && (habit.isCompletedToday() || habit.isFailedToday())) return false
return true
}

View File

@@ -0,0 +1,17 @@
package org.isoron.uhabits.core.models
import java.lang.IllegalStateException
enum class HabitType(val value: Int) {
YES_NO(0), NUMERICAL(1);
companion object {
fun fromInt(value: Int): HabitType {
return when (value) {
YES_NO.value -> YES_NO
NUMERICAL.value -> NUMERICAL
else -> throw IllegalStateException()
}
}
}
}

View File

@@ -0,0 +1,17 @@
package org.isoron.uhabits.core.models
import java.lang.IllegalStateException
enum class NumericalHabitType(val value: Int) {
AT_LEAST(0), AT_MOST(1);
companion object {
fun fromInt(value: Int): NumericalHabitType {
return when (value) {
AT_LEAST.value -> AT_LEAST
AT_MOST.value -> AT_MOST
else -> throw IllegalStateException()
}
}
}
}

View File

@@ -22,6 +22,7 @@ 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.max
import kotlin.math.min
@ThreadSafe
@@ -93,11 +94,15 @@ class ScoreList {
for (i in values.indices) {
val offset = values.size - i - 1
if (isNumerical) {
rollingSum += values[offset]
rollingSum += max(0, values[offset])
if (offset + denominator < values.size) {
rollingSum -= values[offset + denominator]
}
val percentageCompleted = min(1.0, rollingSum / 1000 / targetValue)
val percentageCompleted = if (targetValue > 0) {
min(1.0, rollingSum / 1000 / targetValue)
} else {
1.0
}
previousValue = compute(freq, previousValue, percentageCompleted)
} else {
if (values[offset] == Entry.YES_MANUAL) {

View File

@@ -22,10 +22,12 @@ import org.isoron.uhabits.core.database.Column
import org.isoron.uhabits.core.database.Table
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitType
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList
import java.util.Objects
import java.util.Objects.requireNonNull
/**
* The SQLite database record corresponding to a [Habit].
@@ -93,8 +95,8 @@ class HabitRecord {
highlight = 0
color = model.color.paletteIndex
archived = if (model.isArchived) 1 else 0
type = model.type
targetType = model.targetType
type = model.type.value
targetType = model.targetType.value
targetValue = model.targetValue
unit = model.unit
position = model.position
@@ -108,7 +110,7 @@ class HabitRecord {
reminderHour = null
if (model.hasReminder()) {
val reminder = model.reminder
reminderHour = Objects.requireNonNull(reminder)!!.hour
reminderHour = requireNonNull(reminder)!!.hour
reminderMin = reminder!!.minute
reminderDays = reminder.days.toInteger()
}
@@ -122,8 +124,8 @@ class HabitRecord {
habit.frequency = Frequency(freqNum!!, freqDen!!)
habit.color = PaletteColor(color!!)
habit.isArchived = archived != 0
habit.type = type!!
habit.targetType = targetType!!
habit.type = HabitType.fromInt(type!!)
habit.targetType = NumericalHabitType.fromInt(targetType!!)
habit.targetValue = targetValue!!
habit.unit = unit!!
habit.position = position!!

View File

@@ -226,9 +226,12 @@ open class Preferences(private val storage: Storage) {
storage.putString("pref_encryption_key", "")
}
fun areQuestionMarksEnabled(): Boolean {
return storage.getBoolean("pref_unknown_enabled", false)
}
var areQuestionMarksEnabled: Boolean
get() = storage.getBoolean("pref_unknown_enabled", false)
set(value) {
storage.putBoolean("pref_unknown_enabled", value)
for (l in listeners) l.onQuestionMarksChanged()
}
/**
* @return An integer representing the first day of the week. Sunday
@@ -261,6 +264,7 @@ open class Preferences(private val storage: Storage) {
interface Listener {
fun onCheckmarkSequenceChanged() {}
fun onNotificationsChanged() {}
fun onQuestionMarksChanged() {}
fun onSyncEnabled() {}
}
@@ -280,7 +284,7 @@ open class Preferences(private val storage: Storage) {
putString(key, joinLongs(values))
}
fun getLongArray(key: String, defValue: LongArray): LongArray? {
fun getLongArray(key: String, defValue: LongArray): LongArray {
val string = getString(key, "")
return if (string.isEmpty()) defValue else splitLongs(
string

View File

@@ -27,19 +27,18 @@ class WidgetPreferences @Inject constructor(private val storage: Preferences.Sto
storage.putLongArray(getHabitIdKey(widgetId), habitIds)
}
fun getHabitIdsFromWidgetId(widgetId: Int): LongArray? {
var habitIds: LongArray?
fun getHabitIdsFromWidgetId(widgetId: Int): LongArray {
val habitIdKey = getHabitIdKey(widgetId)
try {
habitIds = storage.getLongArray(habitIdKey, longArrayOf(-1))
return try {
storage.getLongArray(habitIdKey, longArrayOf())
} catch (e: ClassCastException) {
// 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 = LongArray(1)
habitIds[0] = storage.getLong(habitIdKey, -1)
storage.putLongArray(habitIdKey, habitIds)
when (val habitId = storage.getLong(habitIdKey, -1)) {
-1L -> longArrayOf()
else -> longArrayOf(habitId)
}
}
return habitIds
}
fun removeWidget(id: Int) {

View File

@@ -31,7 +31,7 @@ class SingleThreadTaskRunner : TaskRunner {
override fun execute(task: Task) {
for (l in listeners) l.onTaskStarted(task)
if (!task.isCanceled) {
if (!task.isCanceled()) {
task.onAttached(this)
task.onPreExecute()
task.doInBackground()

View File

@@ -20,9 +20,7 @@ package org.isoron.uhabits.core.tasks
fun interface Task {
fun cancel() {}
val isCanceled: Boolean
get() = false
fun isCanceled() = false
fun doInBackground()
fun onAttached(runner: TaskRunner) {}
fun onPostExecute() {}

View File

@@ -22,7 +22,9 @@ 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.HabitType
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Timestamp
import org.isoron.uhabits.core.models.sqlite.SQLiteEntryList
@@ -65,11 +67,11 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
fun createNumericalHabit(): Habit {
val habit = modelFactory.buildHabit()
habit.type = Habit.NUMBER_HABIT
habit.type = HabitType.NUMERICAL
habit.name = "Run"
habit.question = "How many miles did you run today?"
habit.unit = "miles"
habit.targetType = Habit.AT_LEAST
habit.targetType = NumericalHabitType.AT_LEAST
habit.targetValue = 2.0
habit.color = PaletteColor(1)
saveIfSQLite(habit)
@@ -86,11 +88,11 @@ class HabitFixtures(private val modelFactory: ModelFactory, private val habitLis
fun createLongNumericalHabit(reference: Timestamp): Habit {
val habit = modelFactory.buildHabit()
habit.type = Habit.NUMBER_HABIT
habit.type = HabitType.NUMERICAL
habit.name = "Walk"
habit.question = "How many steps did you walk today?"
habit.unit = "steps"
habit.targetType = Habit.AT_LEAST
habit.targetType = NumericalHabitType.AT_LEAST
habit.targetValue = 100.0
habit.color = PaletteColor(1)
saveIfSQLite(habit)

View File

@@ -23,6 +23,7 @@ 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.commands.CreateRepetitionCommand
import org.isoron.uhabits.core.io.Logging
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.HabitList.Order
@@ -54,8 +55,12 @@ import javax.inject.Inject
class HabitCardListCache @Inject constructor(
private val allHabits: HabitList,
private val commandRunner: CommandRunner,
taskRunner: TaskRunner
taskRunner: TaskRunner,
logging: Logging,
) : CommandRunner.Listener {
private val logger = logging.getLogger("HabitCardListCache")
private var checkmarkCount = 0
private var currentFetchTask: Task? = null
private var listener: Listener
@@ -316,8 +321,17 @@ class HabitCardListCache @Inject constructor(
toPosition: Int
) {
data.habits.removeAt(fromPosition)
data.habits.add(toPosition, habit)
listener.onItemMoved(fromPosition, toPosition)
// Workaround for https://github.com/iSoron/uhabits/issues/968
val checkedToPosition = if (toPosition > data.habits.size) {
logger.error("performMove: $toPosition is strictly higher than ${data.habits.size}")
data.habits.size
} else {
toPosition
}
data.habits.add(checkedToPosition, habit)
listener.onItemMoved(fromPosition, checkedToPosition)
}
@Synchronized

View File

@@ -57,6 +57,7 @@ data class ShowHabitState(
val frequency: FrequencyCardState,
val history: HistoryCardState,
val bar: BarCardState,
val theme: Theme,
)
class ShowHabitPresenter(
@@ -94,11 +95,14 @@ class ShowHabitPresenter(
title = habit.name,
color = habit.color,
isNumerical = habit.isNumerical,
theme = theme,
subtitle = SubtitleCardPresenter.buildState(
habit = habit,
theme = theme,
),
overview = OverviewCardPresenter.buildState(
habit = habit,
theme = theme,
),
notes = NotesCardPresenter.buildState(
habit = habit,
@@ -106,18 +110,22 @@ class ShowHabitPresenter(
target = TargetCardPresenter.buildState(
habit = habit,
firstWeekday = preferences.firstWeekdayInt,
theme = theme,
),
streaks = StreakCartPresenter.buildState(
habit = habit,
theme = theme,
),
scores = ScoreCardPresenter.buildState(
spinnerPosition = preferences.scoreCardSpinnerPosition,
habit = habit,
firstWeekday = preferences.firstWeekdayInt,
theme = theme,
),
frequency = FrequencyCardPresenter.buildState(
habit = habit,
firstWeekday = preferences.firstWeekdayInt,
theme = theme,
),
history = HistoryCardPresenter.buildState(
habit = habit,

View File

@@ -22,12 +22,14 @@ 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 org.isoron.uhabits.core.ui.views.Theme
import java.util.HashMap
data class FrequencyCardState(
val color: PaletteColor,
val firstWeekday: Int,
val frequency: HashMap<Timestamp, Array<Int>>,
val theme: Theme,
)
class FrequencyCardPresenter {
@@ -35,12 +37,14 @@ class FrequencyCardPresenter {
fun buildState(
habit: Habit,
firstWeekday: Int,
theme: Theme
) = FrequencyCardState(
color = habit.color,
frequency = habit.originalEntries.computeWeekdayFrequency(
isNumerical = habit.isNumerical
),
firstWeekday = firstWeekday,
theme = theme,
)
}
}

View File

@@ -22,6 +22,7 @@ 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.ui.views.Theme
import org.isoron.uhabits.core.utils.DateUtils
data class OverviewCardState(
@@ -30,11 +31,12 @@ data class OverviewCardState(
val scoreYearDiff: Float,
val scoreToday: Float,
val totalCount: Long,
val theme: Theme,
)
class OverviewCardPresenter {
companion object {
fun buildState(habit: Habit): OverviewCardState {
fun buildState(habit: Habit, theme: Theme): OverviewCardState {
val today = DateUtils.getTodayWithOffset()
val lastMonth = today.minus(30)
val lastYear = today.minus(365)
@@ -52,6 +54,7 @@ class OverviewCardPresenter {
scoreMonthDiff = scoreToday - scoreLastMonth,
scoreYearDiff = scoreToday - scoreLastYear,
totalCount = totalCount,
theme = theme,
)
}
}

View File

@@ -23,6 +23,7 @@ import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Score
import org.isoron.uhabits.core.preferences.Preferences
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.core.utils.DateUtils
data class ScoreCardState(
@@ -30,6 +31,7 @@ data class ScoreCardState(
val bucketSize: Int,
val spinnerPosition: Int,
val color: PaletteColor,
val theme: Theme,
)
class ScoreCardPresenter(
@@ -53,6 +55,7 @@ class ScoreCardPresenter(
habit: Habit,
firstWeekday: Int,
spinnerPosition: Int,
theme: Theme,
): ScoreCardState {
val bucketSize = BUCKET_SIZES[spinnerPosition]
val today = DateUtils.getTodayWithOffset()
@@ -77,6 +80,7 @@ class ScoreCardPresenter(
scores = scores,
bucketSize = bucketSize,
spinnerPosition = spinnerPosition,
theme = theme,
)
}
}

View File

@@ -22,18 +22,21 @@ 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.Streak
import org.isoron.uhabits.core.ui.views.Theme
data class StreakCardState(
val color: PaletteColor,
val bestStreaks: List<Streak>
val bestStreaks: List<Streak>,
val theme: Theme,
)
class StreakCartPresenter {
companion object {
fun buildState(habit: Habit): StreakCardState {
fun buildState(habit: Habit, theme: Theme): StreakCardState {
return StreakCardState(
color = habit.color,
bestStreaks = habit.streaks.getBest(10),
theme = theme,
)
}
}

View File

@@ -23,6 +23,7 @@ import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.ui.views.Theme
data class SubtitleCardState(
val color: PaletteColor,
@@ -32,12 +33,14 @@ data class SubtitleCardState(
val reminder: Reminder?,
val targetValue: Double,
val unit: String,
val theme: Theme,
)
class SubtitleCardPresenter {
companion object {
fun buildState(
habit: Habit,
theme: Theme,
): SubtitleCardState = SubtitleCardState(
color = habit.color,
frequency = habit.frequency,
@@ -46,6 +49,7 @@ class SubtitleCardPresenter {
reminder = habit.reminder,
targetValue = habit.targetValue,
unit = habit.unit,
theme = theme,
)
}
}

View File

@@ -22,6 +22,7 @@ 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.groupedSum
import org.isoron.uhabits.core.ui.views.Theme
import org.isoron.uhabits.core.utils.DateUtils
import java.util.ArrayList
import java.util.Calendar
@@ -31,6 +32,7 @@ data class TargetCardState(
val values: List<Double> = listOf(),
val targets: List<Double> = listOf(),
val intervals: List<Int> = listOf(),
val theme: Theme,
)
class TargetCardPresenter {
@@ -38,6 +40,7 @@ class TargetCardPresenter {
fun buildState(
habit: Habit,
firstWeekday: Int,
theme: Theme,
): TargetCardState {
val today = DateUtils.getTodayWithOffset()
val oldest = habit.computedEntries.getKnown().lastOrNull()?.timestamp ?: today
@@ -106,6 +109,7 @@ class TargetCardPresenter {
values = values,
targets = targets,
intervals = intervals,
theme = theme,
)
}
}

View File

@@ -136,6 +136,7 @@ class BarChart(
canvas.drawLine(0.0, y, width, y)
canvas.setColor(theme.mediumContrastTextColor)
canvas.setTextAlign(TextAlign.CENTER)
canvas.setFontSize(theme.smallTextSize)
var prevMonth = -1
var prevYear = -1
val isLargeInterval = axis.size < 2 || (axis[0].distanceTo(axis[1]) > 300)

View File

@@ -20,6 +20,7 @@
package org.isoron.uhabits.core.ui.views
import org.isoron.platform.gui.Color
import org.isoron.uhabits.core.models.PaletteColor
abstract class Theme {
open val appBackgroundColor = Color(0xf4f4f4)
@@ -35,6 +36,10 @@ abstract class Theme {
open val toolbarBackgroundColor = Color(0xf4f4f4)
open val toolbarColor = Color(0xffffff)
fun color(paletteColor: PaletteColor): Color {
return color(paletteColor.paletteIndex)
}
open fun color(paletteIndex: Int): Color {
return when (paletteIndex) {
0 -> Color(0xD32F2F)
@@ -109,6 +114,12 @@ open class DarkTheme : Theme() {
}
}
class PureBlackTheme : DarkTheme() {
override val appBackgroundColor = Color(0x000000)
override val cardBackgroundColor = Color(0x000000)
override val lowContrastTextColor = Color(0x212121)
}
class WidgetTheme : LightTheme() {
override val cardBackgroundColor = Color.TRANSPARENT
override val highContrastTextColor = Color.WHITE

View File

@@ -46,8 +46,8 @@ class WidgetBehavior @Inject constructor(
setValue(habit, timestamp, Entry.NO)
}
fun onToggleRepetition(habit: Habit, timestamp: Timestamp?) {
val currentValue = habit.originalEntries.get(timestamp!!).value
fun onToggleRepetition(habit: Habit, timestamp: Timestamp) {
val currentValue = habit.originalEntries.get(timestamp).value
val newValue: Int
newValue =
if (preferences.isSkipEnabled) nextToggleValueWithSkip(

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2016-2021 Álinson 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.platform.gui
import org.isoron.platform.time.LocalDate
import org.junit.Assert.assertEquals
import org.junit.Test
class DatesTest {
@Test
fun testDatesBefore2000() {
val date = LocalDate(-1)
assertEquals(date.day, 31)
assertEquals(date.month, 12)
assertEquals(date.year, 1999)
}
}

View File

@@ -80,12 +80,22 @@ class HabitTest : BaseUnitTest() {
assertTrue(h.isCompletedToday())
}
@Test
@Throws(Exception::class)
fun test_isFailed() {
val h = modelFactory.buildHabit()
assertFalse(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), Entry.NO))
h.recompute()
assertTrue(h.isFailedToday())
}
@Test
@Throws(Exception::class)
fun test_isCompleted_numerical() {
val h = modelFactory.buildHabit()
h.type = Habit.NUMBER_HABIT
h.targetType = Habit.AT_LEAST
h.type = HabitType.NUMERICAL
h.targetType = NumericalHabitType.AT_LEAST
h.targetValue = 100.0
assertFalse(h.isCompletedToday())
h.originalEntries.add(Entry(getToday(), 200000))
@@ -97,7 +107,7 @@ class HabitTest : BaseUnitTest() {
h.originalEntries.add(Entry(getToday(), 50000))
h.recompute()
assertFalse(h.isCompletedToday())
h.targetType = Habit.AT_MOST
h.targetType = NumericalHabitType.AT_MOST
h.originalEntries.add(Entry(getToday(), 200000))
h.recompute()
assertFalse(h.isCompletedToday())
@@ -109,6 +119,35 @@ class HabitTest : BaseUnitTest() {
assertTrue(h.isCompletedToday())
}
@Test
@Throws(Exception::class)
fun test_isFailedNumerical() {
val h = modelFactory.buildHabit()
h.type = HabitType.NUMERICAL
h.targetType = NumericalHabitType.AT_LEAST
h.targetValue = 100.0
assertTrue(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 200000))
h.recompute()
assertFalse(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 100000))
h.recompute()
assertFalse(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 50000))
h.recompute()
assertTrue(h.isFailedToday())
h.targetType = NumericalHabitType.AT_MOST
h.originalEntries.add(Entry(getToday(), 200000))
h.recompute()
assertTrue(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 100000))
h.recompute()
assertFalse(h.isFailedToday())
h.originalEntries.add(Entry(getToday(), 50000))
h.recompute()
assertFalse(h.isFailedToday())
}
@Test
@Throws(Exception::class)
fun testURI() {

View File

@@ -18,6 +18,7 @@
*/
package org.isoron.uhabits.core.models
import junit.framework.Assert.assertTrue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.number.IsCloseTo
import org.hamcrest.number.OrderingComparison
@@ -121,6 +122,14 @@ class ScoreListTest : BaseUnitTest() {
checkScoreValues(expectedValues)
}
@Test
fun test_withZeroTarget() {
habit = fixtures.createNumericalHabit()
habit.targetValue = 0.0
habit.recompute()
assertTrue(habit.scores[today].value.isFinite())
}
@Test
fun test_imperfectNonDaily() {
// If the habit should be performed 3 times per week and the user misses 1 repetition

View File

@@ -22,7 +22,8 @@ import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.isoron.uhabits.core.BaseUnitTest
import org.isoron.uhabits.core.models.Frequency
import org.isoron.uhabits.core.models.Habit
import org.isoron.uhabits.core.models.HabitType
import org.isoron.uhabits.core.models.NumericalHabitType
import org.isoron.uhabits.core.models.PaletteColor
import org.isoron.uhabits.core.models.Reminder
import org.isoron.uhabits.core.models.WeekdayList
@@ -59,9 +60,9 @@ class HabitRecordTest : BaseUnitTest() {
reminder = null
id = 1L
position = 15
type = Habit.NUMBER_HABIT
type = HabitType.NUMERICAL
targetValue = 100.0
targetType = Habit.AT_LEAST
targetType = NumericalHabitType.AT_LEAST
unit = "miles"
}
val record = HabitRecord()

View File

@@ -43,7 +43,7 @@ class HabitCardListCacheTest : BaseUnitTest() {
for (i in 0..9) {
if (i == 3) habitList.add(fixtures.createLongHabit()) else habitList.add(fixtures.createShortHabit())
}
cache = HabitCardListCache(habitList, commandRunner, taskRunner)
cache = HabitCardListCache(habitList, commandRunner, taskRunner, mock())
cache.setCheckmarkCount(10)
cache.refreshAllHabits()
cache.onAttached()