From ec08b602f373a6e8b63294fcaefd65b21a988e31 Mon Sep 17 00:00:00 2001 From: Dharanish Date: Sat, 18 May 2024 13:28:39 +0200 Subject: [PATCH] Skip days implemented. Scores not correct yet --- .../habits/edit/EditHabitActivity.kt | 37 +++++++++++++++++++ .../main/res/layout/activity_edit_habit.xml | 19 ++++++++++ .../src/main/res/values/strings.xml | 2 + .../assets/main/migrations/009.sql | 2 +- .../isoron/uhabits/models/HabitRepository.kt | 36 ++++++++++-------- .../isoron/uhabits/core/models/EntryList.kt | 11 +++--- .../isoron/uhabits/core/models/Frequency.kt | 2 +- .../org/isoron/uhabits/core/models/Habit.kt | 9 +++++ .../isoron/uhabits/core/models/ScoreList.kt | 4 +- .../isoron/uhabits/core/models/WeekdayList.kt | 12 ++++++ .../core/models/sqlite/SQLiteEntryList.kt | 10 ++--- .../core/models/sqlite/records/HabitRecord.kt | 10 +++++ .../screens/habits/list/HabitCardListCache.kt | 4 +- .../screens/habits/list/ListHabitsBehavior.kt | 3 +- .../src/jvmMain/resources/migrations/09.sql | 2 + 15 files changed, 132 insertions(+), 31 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt index a2f329d71..ab2371afc 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/edit/EditHabitActivity.kt @@ -81,6 +81,8 @@ class EditHabitActivity : AppCompatActivity() { var androidColor = 0 var freqNum = 1 var freqDen = 1 + var isSkipDays = false + var listSkipDays: WeekdayList = WeekdayList.NO_DAY var reminderHour = -1 var reminderMin = -1 var reminderDays: WeekdayList = WeekdayList.EVERY_DAY @@ -104,6 +106,8 @@ class EditHabitActivity : AppCompatActivity() { color = habit.color freqNum = habit.frequency.numerator freqDen = habit.frequency.denominator + isSkipDays = habit.skipDays + listSkipDays = habit.skipDaysList targetType = habit.targetType habit.reminder?.let { reminderHour = it.hour @@ -125,6 +129,8 @@ class EditHabitActivity : AppCompatActivity() { color = PaletteColor(state.getInt("paletteColor")) freqNum = state.getInt("freqNum") freqDen = state.getInt("freqDen") + isSkipDays = state.getBoolean("isSkipDays", false) + listSkipDays = WeekdayList(state.getInt("listSkipDays", 0)) reminderHour = state.getInt("reminderHour") reminderMin = state.getInt("reminderMin") reminderDays = WeekdayList(state.getInt("reminderDays")) @@ -241,12 +247,31 @@ class EditHabitActivity : AppCompatActivity() { dialog.setListener { days: WeekdayList -> reminderDays = days if (reminderDays.isEmpty) reminderDays = WeekdayList.EVERY_DAY + if (isSkipDays) reminderDays = WeekdayList(reminderDays.toArray(),listSkipDays.toArray()) populateReminder() } dialog.setSelectedDays(reminderDays) dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker") } + populateSkipDays() + binding.skipDaysPicker.setOnClickListener { + val dialog = WeekdayPickerDialog() + + dialog.setListener { days: WeekdayList -> + listSkipDays = days + if (listSkipDays.isEmpty) listSkipDays = WeekdayList.NO_DAY + isSkipDays = (listSkipDays != WeekdayList.NO_DAY) + if (reminderHour >= 0 && isSkipDays) { + reminderDays = WeekdayList(reminderDays.toArray(),listSkipDays.toArray()) + populateReminder() + } + populateSkipDays() + } + dialog.setSelectedDays(listSkipDays) + dialog.dismissCurrentAndShow(supportFragmentManager, "dayPicker") + } + binding.buttonSave.setOnClickListener { if (validate()) save() } @@ -277,6 +302,8 @@ class EditHabitActivity : AppCompatActivity() { } habit.frequency = Frequency(freqNum, freqDen) + habit.skipDays = isSkipDays + habit.skipDaysList = listSkipDays if (habitType == HabitType.NUMERICAL) { habit.targetValue = binding.targetInput.text.toString().toDouble() habit.targetType = targetType @@ -330,6 +357,14 @@ class EditHabitActivity : AppCompatActivity() { } } + private fun populateSkipDays() { + if (isSkipDays) { + binding.skipDaysPicker.text = listSkipDays.toFormattedString(this) + } else { + binding.skipDaysPicker.text = getString(R.string.skip_days_off) + } + } + @SuppressLint("StringFormatMatches") private fun populateFrequency() { binding.booleanFrequencyPicker.text = formatFrequency(freqNum, freqDen, resources) @@ -372,6 +407,8 @@ class EditHabitActivity : AppCompatActivity() { putInt("androidColor", androidColor) putInt("freqNum", freqNum) putInt("freqDen", freqDen) + putBoolean("isSkipDays", isSkipDays) + putInt("listSkipDays", listSkipDays.toInteger()) putInt("reminderHour", reminderHour) putInt("reminderMin", reminderMin) putInt("reminderDays", reminderDays.toInteger()) diff --git a/uhabits-android/src/main/res/layout/activity_edit_habit.xml b/uhabits-android/src/main/res/layout/activity_edit_habit.xml index 75dddb551..06ff19f35 100644 --- a/uhabits-android/src/main/res/layout/activity_edit_habit.xml +++ b/uhabits-android/src/main/res/layout/activity_edit_habit.xml @@ -73,6 +73,7 @@ android:paddingRight="4dp"> + + + + + + + + + + + History Clear Reminder + Skip days Save Streaks You have no active habits You\'re all done for today! Press-and-hold to check or uncheck Off + Off Create habit Edit habit Check diff --git a/uhabits-core-legacy/assets/main/migrations/009.sql b/uhabits-core-legacy/assets/main/migrations/009.sql index 5a4afd962..8a37d40fd 100644 --- a/uhabits-core-legacy/assets/main/migrations/009.sql +++ b/uhabits-core-legacy/assets/main/migrations/009.sql @@ -1,4 +1,4 @@ -create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer ) +create table Habits ( id integer primary key autoincrement, archived integer, color integer, description text, freq_den integer, freq_num integer, skip_days integer, skip_days_list integer, highlight integer, name text, position integer, reminder_hour integer, reminder_min integer ) create table Checkmarks ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer, value integer ) create table Repetitions ( id integer primary key autoincrement, habit integer references habits(id), timestamp integer ) create table Streak ( id integer primary key autoincrement, end integer, habit integer references habits(id), length integer, start integer ) diff --git a/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt b/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt index 961956f51..20e86fcbe 100644 --- a/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt +++ b/uhabits-core-legacy/src/main/common/org/isoron/uhabits/models/HabitRepository.kt @@ -28,9 +28,9 @@ import org.isoron.platform.io.nextId class HabitRepository(var db: Database) { companion object { - const val SELECT_COLUMNS = "id, name, description, freq_num, freq_den, color, archived, position, unit, target_value, type" - const val SELECT_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?" - const val UPDATE_COLUMNS = "id=?, name=?, description=?, freq_num=?, freq_den=?, color=?, archived=?, position=?, unit=?, target_value=?, type=?" + const val SELECT_COLUMNS = "id, name, description, freq_num, freq_den, skip_days, skip_days_list, color, archived, position, unit, target_value, type" + const val SELECT_PLACEHOLDERS = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?" + const val UPDATE_COLUMNS = "id=?, name=?, description=?, freq_num=?, freq_den=?, skip_days=?, skip_days_list=?, color=?, archived=?, position=?, unit=?, target_value=?, type=?" } private val findAllStatement = db.prepareStatement("select $SELECT_COLUMNS from habits order by position") @@ -60,7 +60,7 @@ class HabitRepository(var db: Database) { fun update(habit: Habit) { bindHabitToStatement(habit, updateStatement) - updateStatement.bindInt(11, habit.id) + updateStatement.bindInt(13, habit.id) updateStatement.step() updateStatement.reset() } @@ -70,12 +70,14 @@ class HabitRepository(var db: Database) { name = stmt.getText(1), description = stmt.getText(2), frequency = Frequency(stmt.getInt(3), stmt.getInt(4)), - color = PaletteColor(stmt.getInt(5)), - isArchived = stmt.getInt(6) != 0, - position = stmt.getInt(7), - unit = stmt.getText(8), - target = stmt.getReal(9), - type = if (stmt.getInt(10) == 0) HabitType.BOOLEAN_HABIT else HabitType.NUMERICAL_HABIT) + skipDays = (stmt.getInt(5) == 1), + skipDaysList = WeekDayList(stmt.getInt(6)), + color = PaletteColor(stmt.getInt(7)), + isArchived = stmt.getInt(8) != 0, + position = stmt.getInt(9), + unit = stmt.getText(10), + target = stmt.getReal(11), + type = if (stmt.getInt(12) == 0) HabitType.BOOLEAN_HABIT else HabitType.NUMERICAL_HABIT) } private fun bindHabitToStatement(habit: Habit, statement: PreparedStatement) { @@ -84,12 +86,14 @@ class HabitRepository(var db: Database) { statement.bindText(2, habit.description) statement.bindInt(3, habit.frequency.numerator) statement.bindInt(4, habit.frequency.denominator) - statement.bindInt(5, habit.color.index) - statement.bindInt(6, if (habit.isArchived) 1 else 0) - statement.bindInt(7, habit.position) - statement.bindText(8, habit.unit) - statement.bindReal(9, habit.target) - statement.bindInt(10, habit.type.code) + statement.bindInt(5, if (habit.skipDays) 1 else 0) + statement.bindInt(6, habit.skipDaysList.toInteger()) + statement.bindInt(7, habit.color.index) + statement.bindInt(8, if (habit.isArchived) 1 else 0) + statement.bindInt(9, habit.position) + statement.bindText(10, habit.unit) + statement.bindReal(11, habit.target) + statement.bindInt(12, habit.type.code) } fun delete(habit: Habit) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt index 5c5499f49..f04207be2 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/EntryList.kt @@ -38,11 +38,12 @@ open class EntryList { /** * Returns the entry corresponding to the given timestamp. If no entry with such timestamp - * has been previously added, returns Entry(timestamp, UNKNOWN). + * has been previously added, returns Entry(timestamp, UNKNOWN). or Entry(timestamp, SKIP) if + * skip days are enabled and that day is to be skipped */ @Synchronized - open fun get(timestamp: Timestamp): Entry { - return entriesByTimestamp[timestamp] ?: Entry(timestamp, UNKNOWN) + open fun get(timestamp: Timestamp, skipDays: Boolean = false, skipDaysList: WeekdayList = WeekdayList.NO_DAY): Entry { + return entriesByTimestamp[timestamp] ?: if (skipDays && skipDaysList.isDayTrue(timestamp.weekday)) Entry(timestamp, SKIP) else Entry(timestamp, UNKNOWN) } /** @@ -51,12 +52,12 @@ open class EntryList { * included. */ @Synchronized - open fun getByInterval(from: Timestamp, to: Timestamp): List { + open fun getByInterval(from: Timestamp, to: Timestamp, skipDays: Boolean = false, skipDaysList: WeekdayList = WeekdayList.NO_DAY): List { val result = mutableListOf() if (from.isNewerThan(to)) return result var current = to while (current >= from) { - result.add(get(current)) + result.add(get(current, skipDays, skipDaysList)) current = current.minus(1) } return result diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt index b8673211a..64ee3287f 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Frequency.kt @@ -20,7 +20,7 @@ package org.isoron.uhabits.core.models data class Frequency( var numerator: Int, - var denominator: Int + var denominator: Int, ) { init { if (numerator == denominator) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt index a06d01ec9..0f3a7272b 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/Habit.kt @@ -25,6 +25,8 @@ data class Habit( var color: PaletteColor = PaletteColor(8), var description: String = "", var frequency: Frequency = Frequency.DAILY, + var skipDays: Boolean = false, + var skipDaysList: WeekdayList = WeekdayList.NO_DAY, var id: Long? = null, var isArchived: Boolean = false, var name: String = "", @@ -90,6 +92,8 @@ data class Habit( scores.recompute( frequency = frequency, isNumerical = isNumerical, + skipDays = skipDays, + skipDaysList = skipDaysList, numericalHabitType = targetType, targetValue = targetValue, computedEntries = computedEntries, @@ -108,6 +112,8 @@ data class Habit( this.color = other.color this.description = other.description this.frequency = other.frequency + this.skipDays = other.skipDays + this.skipDaysList = other.skipDaysList // this.id should not be copied this.isArchived = other.isArchived this.name = other.name @@ -128,6 +134,8 @@ data class Habit( if (color != other.color) return false if (description != other.description) return false if (frequency != other.frequency) return false + if (skipDays != other.skipDays) return false + if (skipDaysList != other.skipDaysList) return false if (id != other.id) return false if (isArchived != other.isArchived) return false if (name != other.name) return false @@ -147,6 +155,7 @@ data class Habit( var result = color.hashCode() result = 31 * result + description.hashCode() result = 31 * result + frequency.hashCode() + result = 31 * result + skipDaysList.hashCode() result = 31 * result + (id?.hashCode() ?: 0) result = 31 * result + isArchived.hashCode() result = 31 * result + name.hashCode() diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt index 3acc19e1c..94c093da9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/ScoreList.kt @@ -68,6 +68,8 @@ class ScoreList { fun recompute( frequency: Frequency, isNumerical: Boolean, + skipDays: Boolean, + skipDaysList: WeekdayList, numericalHabitType: NumericalHabitType, targetValue: Double, computedEntries: EntryList, @@ -79,7 +81,7 @@ class ScoreList { var numerator = frequency.numerator var denominator = frequency.denominator val freq = frequency.toDouble() - val values = computedEntries.getByInterval(from, to).map { it.value }.toIntArray() + val values = computedEntries.getByInterval(from, to, skipDays, skipDaysList).map { it.value }.toIntArray() val isAtMost = numericalHabitType == NumericalHabitType.AT_MOST // For non-daily boolean habits, we double the numerator and the denominator to smooth diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt index 55dfa3b9b..1430a7dc9 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/WeekdayList.kt @@ -38,6 +38,13 @@ class WeekdayList { this.weekdays = Arrays.copyOf(weekdays, 7) } + constructor(addDays: BooleanArray, removeDays: BooleanArray) { + weekdays = BooleanArray(7) + for (i in 0..6) { + weekdays[i] = addDays[i] && !removeDays[i] + } + } + val isEmpty: Boolean get() { for (d in weekdays) if (d) return false @@ -58,6 +65,10 @@ class WeekdayList { return packedList } + fun isDayTrue(dayNum: Int): Boolean { + return weekdays[dayNum] + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false @@ -73,5 +84,6 @@ class WeekdayList { companion object { val EVERY_DAY = WeekdayList(127) + val NO_DAY = WeekdayList(booleanArrayOf(false, false, false, false, false, false, false)) } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt index 128accd13..e3186fad7 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/SQLiteEntryList.kt @@ -25,6 +25,7 @@ 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.WeekdayList import org.isoron.uhabits.core.models.sqlite.records.EntryRecord class SQLiteEntryList(database: Database) : EntryList() { @@ -43,14 +44,13 @@ class SQLiteEntryList(database: Database) : EntryList() { isLoaded = true } - override fun get(timestamp: Timestamp): Entry { + override fun get(timestamp: Timestamp, skipDays: Boolean, skipDaysList: WeekdayList): Entry { loadRecords() - return super.get(timestamp) + return super.get(timestamp, skipDays, skipDaysList) } - - override fun getByInterval(from: Timestamp, to: Timestamp): List { + override fun getByInterval(from: Timestamp, to: Timestamp, skipDays: Boolean, skipDaysList: WeekdayList): List { loadRecords() - return super.getByInterval(from, to) + return super.getByInterval(from, to, skipDays, skipDaysList) } override fun add(entry: Entry) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt index dc0386799..c30c94bd2 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/models/sqlite/records/HabitRecord.kt @@ -49,6 +49,12 @@ class HabitRecord { @field:Column(name = "freq_den") var freqDen: Int? = null + @field:Column(name = "skip_days") + var skipDays: Int? = null + + @field:Column(name = "skip_days_list") + var skipDaysList: Int? = null + @field:Column var color: Int? = null @@ -105,6 +111,8 @@ class HabitRecord { val (numerator, denominator) = model.frequency freqNum = numerator freqDen = denominator + skipDays = if (model.skipDays) 1 else 0 + skipDaysList = model.skipDaysList.toInteger() reminderDays = 0 reminderMin = null reminderHour = null @@ -122,6 +130,8 @@ class HabitRecord { habit.description = description!! habit.question = question!! habit.frequency = Frequency(freqNum!!, freqDen!!) + habit.skipDays = (skipDays!! == 1) + habit.skipDaysList = WeekdayList(skipDaysList!!) habit.color = PaletteColor(color!!) habit.isArchived = archived != 0 habit.type = HabitType.fromInt(type!!) diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt index c7d861813..cbf64201d 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -311,7 +311,9 @@ class HabitCardListCache @Inject constructor( newData.scores[habit.id] = habit.scores[today].value val list: MutableList = ArrayList() val notes: MutableList = ArrayList() - for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) { + val skipDays = habit.skipDays + val skipDaysList = habit.skipDaysList + for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today, skipDays, skipDaysList)) { list.add(value) notes.add(note) } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt index b66b08be6..d9682ab04 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/ListHabitsBehavior.kt @@ -56,7 +56,8 @@ open class ListHabitsBehavior @Inject constructor( if (habit.type == HabitType.NUMERICAL) { val oldValue = entry.value.toDouble() / 1000 screen.showNumberPopup(oldValue, entry.notes) { newValue: Double, newNotes: String, x: Float, y: Float -> - val value = (newValue * 1000).roundToInt() + val value = if (habit.skipDays && habit.skipDaysList.isDayTrue(timestamp.weekday)) 3 else (newValue * 1000).roundToInt() + if (newValue != oldValue) { if ( (habit.targetType == AT_LEAST && newValue >= habit.targetValue) || diff --git a/uhabits-core/src/jvmMain/resources/migrations/09.sql b/uhabits-core/src/jvmMain/resources/migrations/09.sql index 5fb4502b4..d8dff31f8 100644 --- a/uhabits-core/src/jvmMain/resources/migrations/09.sql +++ b/uhabits-core/src/jvmMain/resources/migrations/09.sql @@ -5,6 +5,8 @@ create table Habits ( description text, freq_den integer, freq_num integer, + skip_days integer, + skip_days_list integer, highlight integer, name text, position integer,