Implement listing sub habits

pull/2020/head
Dharanish 1 year ago
parent 0a1cdd45cb
commit 35c9a1a0ab

@ -18,6 +18,7 @@
*/
package org.isoron.uhabits.activities.habits.list.views
import android.annotation.SuppressLint
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView.Adapter
import org.isoron.uhabits.activities.habits.list.MAX_CHECKMARK_COUNT
@ -74,9 +75,14 @@ class HabitCardListAdapter @Inject constructor(
return cache.hasNoHabitGroup()
}
fun hasNoSubHabits(): Boolean {
return cache.hasNoSubHabits()
}
/**
* Sets all items as not selected.
*/
@SuppressLint("NotifyDataSetChanged")
override fun clearSelection() {
selectedHabits.clear()
selectedHabitGroups.clear()
@ -116,7 +122,7 @@ class HabitCardListAdapter @Inject constructor(
}
override fun getItemId(position: Int): Long {
val uuidString = getItemUUID(position)
val uuidString = cache.getUUIDByPosition(position)
return if (uuidString != null) {
val formattedUUIDString = formatUUID(uuidString)
val uuid = UUID.fromString(formattedUUIDString)
@ -126,18 +132,6 @@ class HabitCardListAdapter @Inject constructor(
}
}
fun getItemUUID(position: Int): String? {
val h = cache.getHabitByPosition(position)
val hgr = cache.getHabitGroupByPosition(position)
return if (h != null) {
h.uuid!!
} else if (hgr != null) {
hgr.uuid!!
} else {
null
}
}
private fun formatUUID(uuidString: String): String {
return uuidString.substring(0, 8) + "-" +
uuidString.substring(8, 12) + "-" +
@ -207,7 +201,7 @@ class HabitCardListAdapter @Inject constructor(
// function to override getItemViewType and return the type of the view. The view can either be a HabitCardView or a HabitGroupCardView
override fun getItemViewType(position: Int): Int {
return if (position < cache.habitCount) {
return if (cache.getHabitByPosition(position) != null) {
0
} else {
1
@ -322,6 +316,7 @@ class HabitCardListAdapter @Inject constructor(
*
* @param position position of the item to be toggled
*/
@SuppressLint("NotifyDataSetChanged")
fun toggleSelection(position: Int) {
val h = cache.getHabitByPosition(position)
val hgr = cache.getHabitGroupByPosition(position)

@ -136,11 +136,10 @@ class HabitCardView(
init {
scoreRing = RingView(context).apply {
val thickness = dp(3f)
val rightMargin = dp(8f).toInt()
val margin = dp(8f).toInt()
val ringSize = dp(15f).toInt()
val leftMargin = if (habit?.isSubHabit() == true) dp(30f).toInt() else dp(8f).toInt()
layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply {
setMargins(leftMargin, 0, rightMargin, 0)
setMargins(margin, 0, margin, 0)
gravity = Gravity.CENTER
}
setThickness(thickness)
@ -268,6 +267,16 @@ class HabitCardView(
}
scoreRing.apply {
setColor(c)
if (h.isSubHabit()) {
val rightMargin = dp(8f).toInt()
val ringSize = dp(15f).toInt()
val leftMargin =
if (habit?.isSubHabit() == true) dp(30f).toInt() else dp(8f).toInt()
layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply {
setMargins(leftMargin, 0, rightMargin, 0)
gravity = Gravity.CENTER
}
}
}
checkmarkPanel.apply {
color = c

@ -67,6 +67,20 @@ abstract class HabitList : Iterable<Habit> {
@Throws(IllegalArgumentException::class)
abstract fun add(habit: Habit)
/**
* Inserts a new habit in the list at the given position.
*
* 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.
*/
@Throws(IllegalArgumentException::class)
abstract fun add(position: Int, habit: Habit)
/**
* Returns the habit with specified id.
*

@ -195,7 +195,11 @@ class MemoryHabitGroupList : HabitGroupList {
private fun loadFromParent() {
checkNotNull(parent)
list.clear()
for (h in parent!!) if (filter.matches(h)) list.add(h)
for (h in parent!!) {
if (filter.matches(h)) {
list.add(h)
}
}
resort()
}

@ -77,6 +77,17 @@ class MemoryHabitList : HabitList {
resort()
}
@Synchronized
@Throws(IllegalArgumentException::class)
override fun add(position: Int, habit: Habit) {
throwIfHasParent()
require(!list.contains(habit)) { "habit already added" }
val id = habit.id
if (id != null && getById(id) != null) throw RuntimeException("duplicate id")
if (id == null) habit.id = list.size.toLong()
list.add(position, habit)
}
@Synchronized
override fun getById(id: Long): Habit? {
for (h in list) {

@ -64,6 +64,19 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory
observable.notifyListeners()
}
@Synchronized
override fun add(position: Int, habit: Habit) {
loadRecords()
habit.position = size()
val record = HabitRecord()
record.copyFrom(habit)
repository.save(record)
habit.id = record.id
(habit.originalEntries as SQLiteEntryList).habitId = record.id
list.add(position, habit)
observable.notifyListeners()
}
@Synchronized
override fun getById(id: Long): Habit? {
loadRecords()

@ -53,8 +53,8 @@ import javax.inject.Inject
*/
@AppScope
class HabitCardListCache @Inject constructor(
private val allHabits: HabitList,
private val allHabitGroups: HabitGroupList,
private val habits: HabitList,
private val habitGroups: HabitGroupList,
private val commandRunner: CommandRunner,
taskRunner: TaskRunner,
logging: Logging
@ -68,6 +68,7 @@ class HabitCardListCache @Inject constructor(
private val data: CacheData
private var filteredHabits: HabitList
private var filteredHabitGroups: HabitGroupList
private var filteredSubHabits: MutableList<HabitList>
private val taskRunner: TaskRunner
@Synchronized
@ -87,12 +88,17 @@ class HabitCardListCache @Inject constructor(
@Synchronized
fun hasNoHabit(): Boolean {
return allHabits.isEmpty
return habits.isEmpty
}
@Synchronized
fun hasNoHabitGroup(): Boolean {
return allHabitGroups.isEmpty
return habitGroups.isEmpty
}
@Synchronized
fun hasNoSubHabits(): Boolean {
return habitGroups.all { it.habitList.isEmpty }
}
/**
@ -103,11 +109,7 @@ class HabitCardListCache @Inject constructor(
*/
@Synchronized
fun getHabitByPosition(position: Int): Habit? {
return if (position < 0 || position >= data.habits.size) {
null
} else {
data.habits[position]
}
return data.positionToHabit[position]
}
/**
@ -118,16 +120,21 @@ class HabitCardListCache @Inject constructor(
*/
@Synchronized
fun getHabitGroupByPosition(position: Int): HabitGroup? {
return if (position < data.habits.size || position >= data.habits.size + data.habitGroups.size) {
null
return data.positionToHabitGroup[position]
}
@Synchronized
fun getUUIDByPosition(position: Int): String? {
return if (data.positionTypes[position] == STANDALONE_HABIT || data.positionTypes[position] == SUB_HABIT) {
data.positionToHabit[position]!!.uuid
} else {
data.habitGroups[position - data.habits.size]
data.positionToHabitGroup[position]!!.uuid
}
}
@get:Synchronized
val itemCount: Int
get() = habitCount + habitGroupCount
get() = habitCount + habitGroupCount + subHabitCount
@get:Synchronized
val habitCount: Int
@ -137,15 +144,20 @@ class HabitCardListCache @Inject constructor(
val habitGroupCount: Int
get() = data.habitGroups.size
@get:Synchronized
val subHabitCount: Int
get() = data.subHabits.sumOf { it.size() }
@get:Synchronized
@set:Synchronized
var primaryOrder: Order
get() = filteredHabits.primaryOrder
set(order) {
allHabits.primaryOrder = order
habits.primaryOrder = order
habitGroups.primaryOrder = order
filteredHabits.primaryOrder = order
allHabitGroups.primaryOrder = order
filteredHabitGroups.primaryOrder = order
filteredSubHabits.forEach { it.primaryOrder = order }
refreshAllHabits()
}
@ -154,16 +166,17 @@ class HabitCardListCache @Inject constructor(
var secondaryOrder: Order
get() = filteredHabits.secondaryOrder
set(order) {
allHabits.secondaryOrder = order
habits.secondaryOrder = order
habitGroups.secondaryOrder = order
filteredHabits.secondaryOrder = order
allHabitGroups.secondaryOrder = order
filteredHabitGroups.secondaryOrder = order
filteredSubHabits.forEach { it.secondaryOrder = order }
refreshAllHabits()
}
@Synchronized
fun getScore(habitUUID: String): Double {
return data.scores[habitUUID]!!
fun getScore(uuid: String): Double {
return data.scores[uuid]!!
}
@Synchronized
@ -201,42 +214,68 @@ class HabitCardListCache @Inject constructor(
@Synchronized
fun remove(uuid: String) {
val h = data.uuidToHabit[uuid]
if (h != null) {
val position = data.habits.indexOf(h)
data.habits.removeAt(position)
data.uuidToHabit.remove(uuid)
data.checkmarks.remove(uuid)
data.notes.remove(uuid)
data.scores.remove(uuid)
listener.onItemRemoved(position)
} else {
val type = data.positionTypes[data.uuidToPosition[uuid]!!]
if (type == STANDALONE_HABIT) {
val h = data.uuidToHabit[uuid]
if (h != null) {
val position = data.habits.indexOf(h)
data.habits.removeAt(position)
data.checkmarks.remove(uuid)
data.notes.remove(uuid)
data.scores.remove(uuid)
data.decrementPositions(position + 1, data.positionTypes.size)
listener.onItemRemoved(position)
}
} else if (type == SUB_HABIT) {
val h = data.uuidToHabit[uuid]
if (h != null) {
val position = data.uuidToPosition[uuid]!!
val hgrUUID = h.parentUUID
val hgr = data.uuidToHabitGroup[hgrUUID]
val hgrIdx = data.habitGroups.indexOf(hgr)
data.subHabits[hgrIdx].remove(h)
data.checkmarks.remove(uuid)
data.notes.remove(uuid)
data.scores.remove(uuid)
data.decrementPositions(position + 1, data.positionTypes.size)
listener.onItemRemoved(position)
}
} else if (type == HABIT_GROUP) {
val hgr = data.uuidToHabitGroup[uuid]
if (hgr != null) {
val position = data.habitGroups.indexOf(hgr)
data.habitGroups.removeAt(position)
data.uuidToHabitGroup.remove(uuid)
listener.onItemRemoved(position + data.habits.size)
val position = data.uuidToPosition[uuid]!!
val hgrIdx = data.habitGroups.indexOf(hgr)
for (habit in data.subHabits[hgrIdx].reversed()) {
data.checkmarks.remove(habit.uuid)
data.notes.remove(habit.uuid)
data.scores.remove(habit.uuid)
listener.onItemRemoved(data.uuidToPosition[habit.uuid]!!)
}
data.subHabits.removeAt(hgrIdx)
data.habitGroups.removeAt(hgrIdx)
data.scores.remove(hgr.uuid)
data.rebuildPositions()
listener.onItemRemoved(position)
}
}
}
@Synchronized
fun reorder(from: Int, to: Int) {
if (data.habits.size in (from + 1)..to || data.habits.size in (to + 1)..from) {
logger.error("reorder: from and to are in different sections")
return
}
if (from < data.habits.size) {
val fromHabit = data.habits[from]
data.habits.removeAt(from)
data.habits.add(to, fromHabit)
if (from == to) return
val uuid = if (data.positionTypes[from] == STANDALONE_HABIT) {
data.positionToHabit[from]!!.uuid
} else {
val fromHabitGroup = data.habitGroups[from]
data.habitGroups.removeAt(from - data.habits.size)
data.habitGroups.add(to - data.habits.size, fromHabitGroup)
data.positionToHabitGroup[from]!!.uuid
}
if (data.positionTypes[from] == STANDALONE_HABIT) {
val habit = data.positionToHabit[from]!!
data.performMove(habit, from, to)
} else if (data.positionTypes[from] == HABIT_GROUP) {
val habitGroup = data.positionToHabitGroup[from]!!
data.performMove(habitGroup, from, to)
}
listener.onItemMoved(from, to)
}
@Synchronized
@ -246,8 +285,11 @@ class HabitCardListCache @Inject constructor(
@Synchronized
fun setFilter(matcher: HabitMatcher) {
filteredHabits = allHabits.getFiltered(matcher)
filteredHabitGroups = allHabitGroups.getFiltered(matcher)
filteredHabits = habits.getFiltered(matcher)
filteredHabitGroups = habitGroups.getFiltered(matcher)
for (idx in filteredSubHabits.indices) {
filteredSubHabits[idx] = filteredSubHabits[idx].getFiltered(matcher)
}
}
@Synchronized
@ -272,6 +314,11 @@ class HabitCardListCache @Inject constructor(
val uuidToHabitGroup: HashMap<String?, HabitGroup> = HashMap()
val habits: MutableList<Habit>
val habitGroups: MutableList<HabitGroup>
val subHabits: MutableList<HabitList>
val uuidToPosition: HashMap<String?, Int>
val positionTypes: MutableList<Int>
val positionToHabit: HashMap<Int, Habit>
val positionToHabitGroup: HashMap<Int, HabitGroup>
val checkmarks: HashMap<String?, IntArray>
val scores: HashMap<String?, Double>
val notes: HashMap<String?, Array<String>>
@ -327,14 +374,186 @@ class HabitCardListCache @Inject constructor(
for (h in filteredHabits) {
if (h.uuid == null) continue
habits.add(h)
uuidToHabit[h.uuid] = h
}
for (hgr in filteredHabitGroups) {
if (hgr.uuid == null) continue
habitGroups.add(hgr)
val habitList = hgr.habitList
subHabits.add(habitList)
for (h in habitList) {
if (h.uuid == null) continue
}
}
}
@Synchronized
fun rebuildPositions() {
positionToHabit.clear()
positionToHabitGroup.clear()
uuidToPosition.clear()
positionTypes.clear()
var position = 0
for (h in habits) {
uuidToHabit[h.uuid] = h
uuidToPosition[h.uuid] = position
positionToHabit[position] = h
positionTypes.add(STANDALONE_HABIT)
position++
}
for ((idx, hgr) in habitGroups.withIndex()) {
uuidToHabitGroup[hgr.uuid] = hgr
uuidToPosition[hgr.uuid] = position
positionToHabitGroup[position] = hgr
positionTypes.add(HABIT_GROUP)
val habitList = subHabits[idx]
position++
for (h in habitList) {
uuidToHabit[h.uuid] = h
uuidToPosition[h.uuid] = position
positionToHabit[position] = h
positionTypes.add(SUB_HABIT)
position++
}
}
}
@Synchronized
fun isValidInsert(habit: Habit, position: Int): Boolean {
if (habit.parentUUID == null) {
return position <= habits.size
} else {
val parent = uuidToHabitGroup[habit.parentUUID]
if (parent == null) {
return false
}
val parentPosition = uuidToPosition[habit.parentUUID]!!
val parentIndex = habitGroups.indexOf(parent)
val nextGroup = habitGroups.getOrNull(parentIndex + 1)
val nextGroupPosition = uuidToPosition[nextGroup?.uuid]
return (position > parentPosition && position <= positionTypes.size) && (nextGroupPosition == null || position <= nextGroupPosition)
}
}
@Synchronized
fun isValidInsert(habitGroup: HabitGroup, position: Int): Boolean {
return (position == positionTypes.size) || (positionTypes[position] == HABIT_GROUP)
}
@Synchronized
fun incrementPositions(from: Int, to: Int) {
for (pos in positionToHabit.keys.sortedByDescending { it }) {
if (pos in from..to) {
positionToHabit[pos + 1] = positionToHabit[pos]!!
positionToHabit.remove(pos)
}
}
for (pos in positionToHabitGroup.keys.sortedByDescending { it }) {
if (pos in from..to) {
positionToHabitGroup[pos + 1] = positionToHabitGroup[pos]!!
positionToHabitGroup.remove(pos)
}
}
for ((key, pos) in uuidToPosition.entries) {
if (pos in from..to) {
uuidToPosition[key] = pos + 1
}
}
}
@Synchronized
fun decrementPositions(fromPosition: Int, toPosition: Int) {
positionTypes.removeAt(fromPosition)
for (pos in positionToHabit.keys.sortedBy { it }) {
if (pos in fromPosition..toPosition) {
positionToHabit[pos - 1] = positionToHabit[pos]!!
positionToHabit.remove(pos)
}
}
for (pos in positionToHabitGroup.keys.sortedBy { it }) {
if (pos in fromPosition..toPosition) {
positionToHabitGroup[pos - 1] = positionToHabitGroup[pos]!!
positionToHabitGroup.remove(pos)
}
}
for ((key, pos) in uuidToPosition.entries) {
if (pos in fromPosition..toPosition) {
uuidToPosition[key] = pos - 1
}
}
}
@Synchronized
fun performMove(
habit: Habit,
fromPosition: Int,
toPosition: Int
) {
val type = positionTypes[fromPosition]
if (type == HABIT_GROUP) return
// Workaround for https://github.com/iSoron/uhabits/issues/968
val checkedToPosition = if (toPosition > positionTypes.size) {
logger.error("performMove: $toPosition for habit is strictly higher than ${habits.size}")
positionTypes.size
} else {
toPosition
}
val verifyPosition = if (fromPosition > checkedToPosition) checkedToPosition else checkedToPosition + 1
if (!isValidInsert(habit, verifyPosition)) return
if (type == STANDALONE_HABIT) {
habits.removeAt(fromPosition)
if (fromPosition < checkedToPosition) {
decrementPositions(fromPosition + 1, checkedToPosition)
} else {
incrementPositions(toPosition, fromPosition - 1)
}
habits.add(checkedToPosition, habit)
positionTypes.add(checkedToPosition, STANDALONE_HABIT)
} else {
val hgr = uuidToHabitGroup[habit.parentUUID]
val hgrIdx = habitGroups.indexOf(hgr)
val h = positionToHabit[fromPosition]!!
subHabits[hgrIdx].remove(h)
if (fromPosition < checkedToPosition) {
decrementPositions(fromPosition + 1, checkedToPosition)
} else {
incrementPositions(toPosition, fromPosition - 1)
}
subHabits[hgrIdx].add(checkedToPosition - uuidToPosition[hgr!!.uuid]!! - 1, habit)
positionTypes.add(checkedToPosition, SUB_HABIT)
}
positionToHabit[checkedToPosition] = habit
uuidToPosition[habit.uuid] = checkedToPosition
listener.onItemMoved(fromPosition, checkedToPosition)
}
@Synchronized
fun performMove(
habitGroup: HabitGroup,
fromPosition: Int,
toPosition: Int
) {
if (positionTypes[fromPosition] != HABIT_GROUP) return
if (!isValidInsert(habitGroup, toPosition)) return
val fromIdx = habitGroups.indexOf(habitGroup)
val habitList = subHabits[fromIdx]
val toIdx = habitGroups.indexOf(positionToHabitGroup[toPosition]) - (if (fromPosition < toPosition) 1 else 0)
habitGroups.removeAt(fromIdx)
subHabits.removeAt(fromIdx)
habitGroups.add(toIdx, habitGroup)
subHabits.add(toIdx, habitList)
rebuildPositions()
listener.onItemMoved(fromPosition, toPosition)
}
/**
@ -343,6 +562,11 @@ class HabitCardListCache @Inject constructor(
init {
habits = LinkedList()
habitGroups = LinkedList()
subHabits = LinkedList()
positionTypes = LinkedList()
uuidToPosition = HashMap()
positionToHabit = HashMap()
positionToHabitGroup = HashMap()
checkmarks = HashMap()
scores = HashMap()
notes = HashMap()
@ -374,35 +598,35 @@ class HabitCardListCache @Inject constructor(
@Synchronized
override fun doInBackground() {
newData.fetchHabits()
newData.rebuildPositions()
newData.copyScoresFrom(data)
newData.copyCheckmarksFrom(data)
newData.copyNoteIndicatorsFrom(data)
val today = getTodayWithOffset()
val dateFrom = today.minus(checkmarkCount - 1)
if (runner != null) runner!!.publishProgress(this, -1)
for (position in newData.habits.indices) {
for ((position, type) in newData.positionTypes.withIndex()) {
if (isCancelled) return
val habit = newData.habits[position]
if (targetUUID != null && targetUUID != habit.uuid) continue
newData.scores[habit.uuid] = habit.scores[today].value
val list: MutableList<Int> = ArrayList()
val notes: MutableList<String> = ArrayList()
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) {
list.add(value)
notes.add(note)
if (type == STANDALONE_HABIT || type == SUB_HABIT) {
val habit = newData.positionToHabit[position]!!
if (targetUUID != null && targetUUID != habit.uuid) continue
newData.scores[habit.uuid] = habit.scores[today].value
val list: MutableList<Int> = ArrayList()
val notes: MutableList<String> = ArrayList()
for ((_, value, note) in habit.computedEntries.getByInterval(dateFrom, today)) {
list.add(value)
notes.add(note)
}
val entries = list.toTypedArray()
newData.checkmarks[habit.uuid] = ArrayUtils.toPrimitive(entries)
newData.notes[habit.uuid] = notes.toTypedArray()
runner!!.publishProgress(this, position)
} else if (type == HABIT_GROUP) {
val habitGroup = newData.positionToHabitGroup[position]!!
if (targetUUID != null && targetUUID != habitGroup.uuid) continue
newData.scores[habitGroup.uuid] = habitGroup.scores[today].value
runner!!.publishProgress(this, position)
}
val entries = list.toTypedArray()
newData.checkmarks[habit.uuid] = ArrayUtils.toPrimitive(entries)
newData.notes[habit.uuid] = notes.toTypedArray()
runner!!.publishProgress(this, position)
}
for (position in newData.habitGroups.indices) {
if (isCancelled) return
val hgr = newData.habitGroups[position]
if (targetUUID != null && targetUUID != hgr.uuid) continue
newData.scores[hgr.uuid] = hgr.scores[today].value
runner!!.publishProgress(this, position + newData.habits.size)
}
}
@ -424,8 +648,21 @@ class HabitCardListCache @Inject constructor(
@Synchronized
private fun performInsert(habit: Habit, position: Int) {
if (!data.isValidInsert(habit, position)) return
val uuid = habit.uuid
data.habits.add(position, habit)
if (habit.parentUUID == null) {
data.habits.add(position, habit)
data.positionTypes.add(position, STANDALONE_HABIT)
} else {
// val parent = data.uuidToHabitGroup[habit.parentUUID]
// val parentIdx = data.habitGroups.indexOf(parent)
// val parentPosition = data.uuidToPosition[habit.parentUUID]!!
// data.subHabits[parentIdx].add(position - parentPosition - 1, habit)
data.positionTypes.add(position, SUB_HABIT)
}
data.incrementPositions(position, data.positionTypes.size - 1)
data.positionToHabit[position] = habit
data.uuidToPosition[uuid] = position
data.uuidToHabit[uuid] = habit
data.scores[uuid] = newData.scores[uuid]!!
data.checkmarks[uuid] = newData.checkmarks[uuid]!!
@ -435,62 +672,23 @@ class HabitCardListCache @Inject constructor(
@Synchronized
private fun performInsert(habitGroup: HabitGroup, position: Int) {
val newPosition = if (position < data.habits.size) {
data.habits.size
} else {
position
}
if (!data.isValidInsert(habitGroup, position)) return
val uuid = habitGroup.uuid
data.habitGroups.add(newPosition - data.habits.size, habitGroup)
data.uuidToHabitGroup[uuid] = habitGroup
data.scores[uuid] = newData.scores[uuid]!!
listener.onItemInserted(newPosition)
}
@Synchronized
private fun performMove(
habit: Habit,
fromPosition: Int,
toPosition: Int
) {
data.habits.removeAt(fromPosition)
val prevIdx = newData.habitGroups.indexOf(habitGroup)
val habitList = newData.subHabits[prevIdx]
var idx = data.habitGroups.indexOf(data.positionToHabitGroup[position])
if (idx < 0) idx = data.habitGroups.size
// Workaround for https://github.com/iSoron/uhabits/issues/968
val checkedToPosition = if (toPosition > data.habits.size) {
logger.error("performMove: $toPosition for habit is strictly higher than ${data.habits.size}")
data.habits.size
} else {
toPosition
}
data.habits.add(checkedToPosition, habit)
listener.onItemMoved(fromPosition, checkedToPosition)
}
private fun performMove(
habitGroup: HabitGroup,
fromPosition: Int,
toPosition: Int
) {
if (fromPosition < data.habits.size || fromPosition > data.habits.size + data.habitGroups.size) {
logger.error("performMove: $fromPosition for habit group is out of bounds")
return
}
data.habitGroups.removeAt(fromPosition - data.habits.size)
// Workaround for https://github.com/iSoron/uhabits/issues/968
val checkedToPosition = if (toPosition < data.habits.size) {
logger.error("performMove: $toPosition for habit group is strictly lower than ${data.habits.size}")
data.habits.size
} else if (toPosition > data.habits.size + data.habitGroups.size) {
logger.error("performMove: $toPosition for habit group is strictly higher than ${data.habits.size + data.habitGroups.size}")
data.habits.size + data.habitGroups.size
} else {
toPosition
data.habitGroups.add(idx, habitGroup)
data.subHabits.add(prevIdx, habitList)
data.scores[uuid] = newData.scores[uuid]!!
for (h in habitList) {
data.scores[h.uuid] = newData.scores[h.uuid]!!
data.checkmarks[h.uuid] = newData.checkmarks[h.uuid]!!
data.notes[h.uuid] = newData.notes[h.uuid]!!
}
data.habitGroups.add(checkedToPosition - data.habits.size, habitGroup)
listener.onItemMoved(fromPosition, checkedToPosition)
data.rebuildPositions()
listener.onItemInserted(position)
}
@Synchronized
@ -500,7 +698,7 @@ class HabitCardListCache @Inject constructor(
val newScore = newData.scores[uuid]!!
if (oldScore != newScore) unchanged = false
if (position < data.habits.size) {
if (data.positionTypes[position] != HABIT_GROUP) {
val oldCheckmarks = data.checkmarks[uuid]
val newCheckmarks = newData.checkmarks[uuid]!!
val oldNoteIndicators = data.notes[uuid]
@ -519,38 +717,45 @@ class HabitCardListCache @Inject constructor(
@Synchronized
private fun processPosition(currentPosition: Int) {
if (currentPosition < newData.habits.size) {
val habit = newData.habits[currentPosition]
val uuid = habit.uuid
val prevPosition = data.habits.indexOf(habit)
val type = newData.positionTypes[currentPosition]
if (type == STANDALONE_HABIT || type == SUB_HABIT) {
val habit = newData.positionToHabit[currentPosition]!!
val uuid = habit.uuid ?: throw NullPointerException()
val prevPosition = data.uuidToPosition[uuid] ?: -1
val newPosition = if (type == STANDALONE_HABIT) {
currentPosition
} else {
val hgr = data.uuidToHabitGroup[habit.parentUUID]
val hgrIdx = data.habitGroups.indexOf(hgr)
newData.subHabits[hgrIdx].indexOf(habit) + data.uuidToPosition[hgr!!.uuid]!! + 1
}
if (prevPosition < 0) {
performInsert(habit, currentPosition)
performInsert(habit, newPosition)
} else {
if (prevPosition != currentPosition) {
performMove(
if (prevPosition != newPosition) {
data.performMove(
habit,
prevPosition,
currentPosition
newPosition
)
}
if (uuid == null) throw NullPointerException()
performUpdate(uuid, currentPosition)
}
} else {
val habitGroup = newData.habitGroups[currentPosition - data.habits.size]
val uuid = habitGroup.uuid
val prevPosition = data.habitGroups.indexOf(habitGroup) + data.habits.size
if (prevPosition < data.habits.size) {
} else if (type == HABIT_GROUP) {
val habitGroup = newData.positionToHabitGroup[currentPosition]!!
val uuid = habitGroup.uuid ?: throw NullPointerException()
val prevPosition = data.uuidToPosition[uuid] ?: -1
if (prevPosition < 0) {
performInsert(habitGroup, currentPosition)
} else {
if (prevPosition != currentPosition) {
performMove(
data.performMove(
habitGroup,
prevPosition,
currentPosition
)
}
if (uuid == null) throw NullPointerException()
performUpdate(uuid, currentPosition)
}
}
@ -558,27 +763,29 @@ class HabitCardListCache @Inject constructor(
@Synchronized
private fun processRemovedHabits() {
val before: Set<String?> = data.uuidToHabit.keys
val after: Set<String?> = newData.uuidToHabit.keys
val before: Set<String?> = (data.uuidToHabit.keys).union(data.uuidToHabitGroup.keys)
val after: Set<String?> = (newData.uuidToHabit.keys).union(newData.uuidToHabitGroup.keys)
val removed: MutableSet<String?> = TreeSet(before)
removed.removeAll(after)
for (uuid in removed) remove(uuid!!)
processRemovedHabitGroups()
for (uuid in removed.sortedBy { uuid -> data.uuidToPosition[uuid] }) remove(uuid!!)
}
}
@Synchronized
private fun processRemovedHabitGroups() {
val before: Set<String?> = data.uuidToHabitGroup.keys
val after: Set<String?> = newData.uuidToHabitGroup.keys
val removed: MutableSet<String?> = TreeSet(before)
removed.removeAll(after)
for (uuid in removed) remove(uuid!!)
}
companion object {
const val STANDALONE_HABIT = 0
const val HABIT_GROUP = 1
const val SUB_HABIT = 2
}
init {
filteredHabits = allHabits
filteredHabitGroups = allHabitGroups
filteredHabits = habits
filteredHabitGroups = habitGroups
filteredSubHabits = LinkedList()
for (hgr in habitGroups) {
val subList = hgr.habitList
filteredSubHabits.add(subList)
}
this.taskRunner = taskRunner
listener = object : Listener {}
data = CacheData()

@ -24,6 +24,7 @@ import org.isoron.uhabits.core.database.Database
import org.isoron.uhabits.core.database.DatabaseOpener
import org.isoron.uhabits.core.database.JdbcDatabase
import org.isoron.uhabits.core.database.MigrationHelper
import org.isoron.uhabits.core.models.HabitGroupList
import org.isoron.uhabits.core.models.HabitList
import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.Timestamp
@ -52,6 +53,7 @@ import java.sql.SQLException
@RunWith(MockitoJUnitRunner::class)
open class BaseUnitTest {
protected open lateinit var habitList: HabitList
protected open lateinit var habitGroupList: HabitGroupList
protected lateinit var fixtures: HabitFixtures
protected lateinit var modelFactory: ModelFactory
protected lateinit var taskRunner: SingleThreadTaskRunner
@ -80,6 +82,7 @@ open class BaseUnitTest {
setStartDayOffset(0, 0)
val memoryModelFactory = MemoryModelFactory()
habitList = spy(memoryModelFactory.buildHabitList())
habitGroupList = spy(memoryModelFactory.buildHabitGroupList())
fixtures = HabitFixtures(memoryModelFactory, habitList)
modelFactory = memoryModelFactory
taskRunner = SingleThreadTaskRunner()

@ -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, mock())
cache = HabitCardListCache(habitList, habitGroupList, commandRunner, taskRunner, mock())
cache.setCheckmarkCount(10)
cache.refreshAllHabits()
cache.onAttached()
@ -82,8 +82,8 @@ class HabitCardListCacheTest : BaseUnitTest() {
val h = habitList.getByPosition(3)
val score = h.scores[today].value
assertThat(cache.getHabitByPosition(3), equalTo(h))
assertThat(cache.getScore(h.id!!), equalTo(score))
val actualCheckmarks = cache.getCheckmarks(h.id!!)
assertThat(cache.getScore(h.uuid!!), equalTo(score))
val actualCheckmarks = cache.getCheckmarks(h.uuid!!)
val expectedCheckmarks = h
.computedEntries

@ -140,6 +140,7 @@ class ListHabitsSelectionMenuBehaviorTest : BaseUnitTest() {
habitList.add(habit3)
behavior = ListHabitsSelectionMenuBehavior(
habitList,
habitGroupList,
screen,
adapter,
commandRunner

Loading…
Cancel
Save