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

@ -136,11 +136,10 @@ class HabitCardView(
init { init {
scoreRing = RingView(context).apply { scoreRing = RingView(context).apply {
val thickness = dp(3f) val thickness = dp(3f)
val rightMargin = dp(8f).toInt() val margin = dp(8f).toInt()
val ringSize = dp(15f).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 { layoutParams = LinearLayout.LayoutParams(ringSize, ringSize).apply {
setMargins(leftMargin, 0, rightMargin, 0) setMargins(margin, 0, margin, 0)
gravity = Gravity.CENTER gravity = Gravity.CENTER
} }
setThickness(thickness) setThickness(thickness)
@ -268,6 +267,16 @@ class HabitCardView(
} }
scoreRing.apply { scoreRing.apply {
setColor(c) 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 { checkmarkPanel.apply {
color = c color = c

@ -67,6 +67,20 @@ abstract class HabitList : Iterable<Habit> {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
abstract fun add(habit: Habit) 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. * Returns the habit with specified id.
* *

@ -195,7 +195,11 @@ class MemoryHabitGroupList : HabitGroupList {
private fun loadFromParent() { private fun loadFromParent() {
checkNotNull(parent) checkNotNull(parent)
list.clear() 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() resort()
} }

@ -77,6 +77,17 @@ class MemoryHabitList : HabitList {
resort() 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 @Synchronized
override fun getById(id: Long): Habit? { override fun getById(id: Long): Habit? {
for (h in list) { for (h in list) {

@ -64,6 +64,19 @@ class SQLiteHabitList @Inject constructor(private val modelFactory: ModelFactory
observable.notifyListeners() 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 @Synchronized
override fun getById(id: Long): Habit? { override fun getById(id: Long): Habit? {
loadRecords() loadRecords()

@ -53,8 +53,8 @@ import javax.inject.Inject
*/ */
@AppScope @AppScope
class HabitCardListCache @Inject constructor( class HabitCardListCache @Inject constructor(
private val allHabits: HabitList, private val habits: HabitList,
private val allHabitGroups: HabitGroupList, private val habitGroups: HabitGroupList,
private val commandRunner: CommandRunner, private val commandRunner: CommandRunner,
taskRunner: TaskRunner, taskRunner: TaskRunner,
logging: Logging logging: Logging
@ -68,6 +68,7 @@ class HabitCardListCache @Inject constructor(
private val data: CacheData private val data: CacheData
private var filteredHabits: HabitList private var filteredHabits: HabitList
private var filteredHabitGroups: HabitGroupList private var filteredHabitGroups: HabitGroupList
private var filteredSubHabits: MutableList<HabitList>
private val taskRunner: TaskRunner private val taskRunner: TaskRunner
@Synchronized @Synchronized
@ -87,12 +88,17 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
fun hasNoHabit(): Boolean { fun hasNoHabit(): Boolean {
return allHabits.isEmpty return habits.isEmpty
} }
@Synchronized @Synchronized
fun hasNoHabitGroup(): Boolean { 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 @Synchronized
fun getHabitByPosition(position: Int): Habit? { fun getHabitByPosition(position: Int): Habit? {
return if (position < 0 || position >= data.habits.size) { return data.positionToHabit[position]
null
} else {
data.habits[position]
}
} }
/** /**
@ -118,16 +120,21 @@ class HabitCardListCache @Inject constructor(
*/ */
@Synchronized @Synchronized
fun getHabitGroupByPosition(position: Int): HabitGroup? { fun getHabitGroupByPosition(position: Int): HabitGroup? {
return if (position < data.habits.size || position >= data.habits.size + data.habitGroups.size) { return data.positionToHabitGroup[position]
null }
@Synchronized
fun getUUIDByPosition(position: Int): String? {
return if (data.positionTypes[position] == STANDALONE_HABIT || data.positionTypes[position] == SUB_HABIT) {
data.positionToHabit[position]!!.uuid
} else { } else {
data.habitGroups[position - data.habits.size] data.positionToHabitGroup[position]!!.uuid
} }
} }
@get:Synchronized @get:Synchronized
val itemCount: Int val itemCount: Int
get() = habitCount + habitGroupCount get() = habitCount + habitGroupCount + subHabitCount
@get:Synchronized @get:Synchronized
val habitCount: Int val habitCount: Int
@ -137,15 +144,20 @@ class HabitCardListCache @Inject constructor(
val habitGroupCount: Int val habitGroupCount: Int
get() = data.habitGroups.size get() = data.habitGroups.size
@get:Synchronized
val subHabitCount: Int
get() = data.subHabits.sumOf { it.size() }
@get:Synchronized @get:Synchronized
@set:Synchronized @set:Synchronized
var primaryOrder: Order var primaryOrder: Order
get() = filteredHabits.primaryOrder get() = filteredHabits.primaryOrder
set(order) { set(order) {
allHabits.primaryOrder = order habits.primaryOrder = order
habitGroups.primaryOrder = order
filteredHabits.primaryOrder = order filteredHabits.primaryOrder = order
allHabitGroups.primaryOrder = order
filteredHabitGroups.primaryOrder = order filteredHabitGroups.primaryOrder = order
filteredSubHabits.forEach { it.primaryOrder = order }
refreshAllHabits() refreshAllHabits()
} }
@ -154,16 +166,17 @@ class HabitCardListCache @Inject constructor(
var secondaryOrder: Order var secondaryOrder: Order
get() = filteredHabits.secondaryOrder get() = filteredHabits.secondaryOrder
set(order) { set(order) {
allHabits.secondaryOrder = order habits.secondaryOrder = order
habitGroups.secondaryOrder = order
filteredHabits.secondaryOrder = order filteredHabits.secondaryOrder = order
allHabitGroups.secondaryOrder = order
filteredHabitGroups.secondaryOrder = order filteredHabitGroups.secondaryOrder = order
filteredSubHabits.forEach { it.secondaryOrder = order }
refreshAllHabits() refreshAllHabits()
} }
@Synchronized @Synchronized
fun getScore(habitUUID: String): Double { fun getScore(uuid: String): Double {
return data.scores[habitUUID]!! return data.scores[uuid]!!
} }
@Synchronized @Synchronized
@ -201,42 +214,68 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
fun remove(uuid: String) { fun remove(uuid: String) {
val type = data.positionTypes[data.uuidToPosition[uuid]!!]
if (type == STANDALONE_HABIT) {
val h = data.uuidToHabit[uuid] val h = data.uuidToHabit[uuid]
if (h != null) { if (h != null) {
val position = data.habits.indexOf(h) val position = data.habits.indexOf(h)
data.habits.removeAt(position) data.habits.removeAt(position)
data.uuidToHabit.remove(uuid)
data.checkmarks.remove(uuid) data.checkmarks.remove(uuid)
data.notes.remove(uuid) data.notes.remove(uuid)
data.scores.remove(uuid) data.scores.remove(uuid)
data.decrementPositions(position + 1, data.positionTypes.size)
listener.onItemRemoved(position) listener.onItemRemoved(position)
} else { }
} 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] val hgr = data.uuidToHabitGroup[uuid]
if (hgr != null) { if (hgr != null) {
val position = data.habitGroups.indexOf(hgr) val position = data.uuidToPosition[uuid]!!
data.habitGroups.removeAt(position) val hgrIdx = data.habitGroups.indexOf(hgr)
data.uuidToHabitGroup.remove(uuid)
listener.onItemRemoved(position + data.habits.size) 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 @Synchronized
fun reorder(from: Int, to: Int) { fun reorder(from: Int, to: Int) {
if (data.habits.size in (from + 1)..to || data.habits.size in (to + 1)..from) { if (from == to) return
logger.error("reorder: from and to are in different sections") val uuid = if (data.positionTypes[from] == STANDALONE_HABIT) {
return data.positionToHabit[from]!!.uuid
}
if (from < data.habits.size) {
val fromHabit = data.habits[from]
data.habits.removeAt(from)
data.habits.add(to, fromHabit)
} else { } else {
val fromHabitGroup = data.habitGroups[from] data.positionToHabitGroup[from]!!.uuid
data.habitGroups.removeAt(from - data.habits.size) }
data.habitGroups.add(to - data.habits.size, fromHabitGroup) 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 @Synchronized
@ -246,8 +285,11 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
fun setFilter(matcher: HabitMatcher) { fun setFilter(matcher: HabitMatcher) {
filteredHabits = allHabits.getFiltered(matcher) filteredHabits = habits.getFiltered(matcher)
filteredHabitGroups = allHabitGroups.getFiltered(matcher) filteredHabitGroups = habitGroups.getFiltered(matcher)
for (idx in filteredSubHabits.indices) {
filteredSubHabits[idx] = filteredSubHabits[idx].getFiltered(matcher)
}
} }
@Synchronized @Synchronized
@ -272,6 +314,11 @@ class HabitCardListCache @Inject constructor(
val uuidToHabitGroup: HashMap<String?, HabitGroup> = HashMap() val uuidToHabitGroup: HashMap<String?, HabitGroup> = HashMap()
val habits: MutableList<Habit> val habits: MutableList<Habit>
val habitGroups: MutableList<HabitGroup> 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 checkmarks: HashMap<String?, IntArray>
val scores: HashMap<String?, Double> val scores: HashMap<String?, Double>
val notes: HashMap<String?, Array<String>> val notes: HashMap<String?, Array<String>>
@ -327,14 +374,186 @@ class HabitCardListCache @Inject constructor(
for (h in filteredHabits) { for (h in filteredHabits) {
if (h.uuid == null) continue if (h.uuid == null) continue
habits.add(h) habits.add(h)
uuidToHabit[h.uuid] = h
} }
for (hgr in filteredHabitGroups) { for (hgr in filteredHabitGroups) {
if (hgr.uuid == null) continue if (hgr.uuid == null) continue
habitGroups.add(hgr) 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 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 { init {
habits = LinkedList() habits = LinkedList()
habitGroups = LinkedList() habitGroups = LinkedList()
subHabits = LinkedList()
positionTypes = LinkedList()
uuidToPosition = HashMap()
positionToHabit = HashMap()
positionToHabitGroup = HashMap()
checkmarks = HashMap() checkmarks = HashMap()
scores = HashMap() scores = HashMap()
notes = HashMap() notes = HashMap()
@ -374,15 +598,17 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
override fun doInBackground() { override fun doInBackground() {
newData.fetchHabits() newData.fetchHabits()
newData.rebuildPositions()
newData.copyScoresFrom(data) newData.copyScoresFrom(data)
newData.copyCheckmarksFrom(data) newData.copyCheckmarksFrom(data)
newData.copyNoteIndicatorsFrom(data) newData.copyNoteIndicatorsFrom(data)
val today = getTodayWithOffset() val today = getTodayWithOffset()
val dateFrom = today.minus(checkmarkCount - 1) val dateFrom = today.minus(checkmarkCount - 1)
if (runner != null) runner!!.publishProgress(this, -1) if (runner != null) runner!!.publishProgress(this, -1)
for (position in newData.habits.indices) { for ((position, type) in newData.positionTypes.withIndex()) {
if (isCancelled) return if (isCancelled) return
val habit = newData.habits[position] if (type == STANDALONE_HABIT || type == SUB_HABIT) {
val habit = newData.positionToHabit[position]!!
if (targetUUID != null && targetUUID != habit.uuid) continue if (targetUUID != null && targetUUID != habit.uuid) continue
newData.scores[habit.uuid] = habit.scores[today].value newData.scores[habit.uuid] = habit.scores[today].value
val list: MutableList<Int> = ArrayList() val list: MutableList<Int> = ArrayList()
@ -395,14 +621,12 @@ class HabitCardListCache @Inject constructor(
newData.checkmarks[habit.uuid] = ArrayUtils.toPrimitive(entries) newData.checkmarks[habit.uuid] = ArrayUtils.toPrimitive(entries)
newData.notes[habit.uuid] = notes.toTypedArray() newData.notes[habit.uuid] = notes.toTypedArray()
runner!!.publishProgress(this, position) 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)
} }
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 @Synchronized
private fun performInsert(habit: Habit, position: Int) { private fun performInsert(habit: Habit, position: Int) {
if (!data.isValidInsert(habit, position)) return
val uuid = habit.uuid val uuid = habit.uuid
if (habit.parentUUID == null) {
data.habits.add(position, habit) 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.uuidToHabit[uuid] = habit
data.scores[uuid] = newData.scores[uuid]!! data.scores[uuid] = newData.scores[uuid]!!
data.checkmarks[uuid] = newData.checkmarks[uuid]!! data.checkmarks[uuid] = newData.checkmarks[uuid]!!
@ -435,62 +672,23 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
private fun performInsert(habitGroup: HabitGroup, position: Int) { private fun performInsert(habitGroup: HabitGroup, position: Int) {
val newPosition = if (position < data.habits.size) { if (!data.isValidInsert(habitGroup, position)) return
data.habits.size
} else {
position
}
val uuid = habitGroup.uuid val uuid = habitGroup.uuid
data.habitGroups.add(newPosition - data.habits.size, habitGroup) val prevIdx = newData.habitGroups.indexOf(habitGroup)
data.uuidToHabitGroup[uuid] = habitGroup val habitList = newData.subHabits[prevIdx]
data.scores[uuid] = newData.scores[uuid]!! var idx = data.habitGroups.indexOf(data.positionToHabitGroup[position])
listener.onItemInserted(newPosition) if (idx < 0) idx = data.habitGroups.size
}
@Synchronized data.habitGroups.add(idx, habitGroup)
private fun performMove( data.subHabits.add(prevIdx, habitList)
habit: Habit, data.scores[uuid] = newData.scores[uuid]!!
fromPosition: Int, for (h in habitList) {
toPosition: Int data.scores[h.uuid] = newData.scores[h.uuid]!!
) { data.checkmarks[h.uuid] = newData.checkmarks[h.uuid]!!
data.habits.removeAt(fromPosition) data.notes[h.uuid] = newData.notes[h.uuid]!!
// 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.rebuildPositions()
data.habitGroups.add(checkedToPosition - data.habits.size, habitGroup) listener.onItemInserted(position)
listener.onItemMoved(fromPosition, checkedToPosition)
} }
@Synchronized @Synchronized
@ -500,7 +698,7 @@ class HabitCardListCache @Inject constructor(
val newScore = newData.scores[uuid]!! val newScore = newData.scores[uuid]!!
if (oldScore != newScore) unchanged = false if (oldScore != newScore) unchanged = false
if (position < data.habits.size) { if (data.positionTypes[position] != HABIT_GROUP) {
val oldCheckmarks = data.checkmarks[uuid] val oldCheckmarks = data.checkmarks[uuid]
val newCheckmarks = newData.checkmarks[uuid]!! val newCheckmarks = newData.checkmarks[uuid]!!
val oldNoteIndicators = data.notes[uuid] val oldNoteIndicators = data.notes[uuid]
@ -519,38 +717,45 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
private fun processPosition(currentPosition: Int) { private fun processPosition(currentPosition: Int) {
if (currentPosition < newData.habits.size) { val type = newData.positionTypes[currentPosition]
val habit = newData.habits[currentPosition]
val uuid = habit.uuid if (type == STANDALONE_HABIT || type == SUB_HABIT) {
val prevPosition = data.habits.indexOf(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) { if (prevPosition < 0) {
performInsert(habit, currentPosition) performInsert(habit, newPosition)
} else { } else {
if (prevPosition != currentPosition) { if (prevPosition != newPosition) {
performMove( data.performMove(
habit, habit,
prevPosition, prevPosition,
currentPosition newPosition
) )
} }
if (uuid == null) throw NullPointerException()
performUpdate(uuid, currentPosition) performUpdate(uuid, currentPosition)
} }
} else { } else if (type == HABIT_GROUP) {
val habitGroup = newData.habitGroups[currentPosition - data.habits.size] val habitGroup = newData.positionToHabitGroup[currentPosition]!!
val uuid = habitGroup.uuid val uuid = habitGroup.uuid ?: throw NullPointerException()
val prevPosition = data.habitGroups.indexOf(habitGroup) + data.habits.size val prevPosition = data.uuidToPosition[uuid] ?: -1
if (prevPosition < data.habits.size) { if (prevPosition < 0) {
performInsert(habitGroup, currentPosition) performInsert(habitGroup, currentPosition)
} else { } else {
if (prevPosition != currentPosition) { if (prevPosition != currentPosition) {
performMove( data.performMove(
habitGroup, habitGroup,
prevPosition, prevPosition,
currentPosition currentPosition
) )
} }
if (uuid == null) throw NullPointerException()
performUpdate(uuid, currentPosition) performUpdate(uuid, currentPosition)
} }
} }
@ -558,27 +763,29 @@ class HabitCardListCache @Inject constructor(
@Synchronized @Synchronized
private fun processRemovedHabits() { private fun processRemovedHabits() {
val before: Set<String?> = data.uuidToHabit.keys val before: Set<String?> = (data.uuidToHabit.keys).union(data.uuidToHabitGroup.keys)
val after: Set<String?> = newData.uuidToHabit.keys val after: Set<String?> = (newData.uuidToHabit.keys).union(newData.uuidToHabitGroup.keys)
val removed: MutableSet<String?> = TreeSet(before) val removed: MutableSet<String?> = TreeSet(before)
removed.removeAll(after) removed.removeAll(after)
for (uuid in removed) remove(uuid!!) for (uuid in removed.sortedBy { uuid -> data.uuidToPosition[uuid] }) remove(uuid!!)
processRemovedHabitGroups()
} }
@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 { init {
filteredHabits = allHabits filteredHabits = habits
filteredHabitGroups = allHabitGroups filteredHabitGroups = habitGroups
filteredSubHabits = LinkedList()
for (hgr in habitGroups) {
val subList = hgr.habitList
filteredSubHabits.add(subList)
}
this.taskRunner = taskRunner this.taskRunner = taskRunner
listener = object : Listener {} listener = object : Listener {}
data = CacheData() 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.DatabaseOpener
import org.isoron.uhabits.core.database.JdbcDatabase import org.isoron.uhabits.core.database.JdbcDatabase
import org.isoron.uhabits.core.database.MigrationHelper 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.HabitList
import org.isoron.uhabits.core.models.ModelFactory import org.isoron.uhabits.core.models.ModelFactory
import org.isoron.uhabits.core.models.Timestamp import org.isoron.uhabits.core.models.Timestamp
@ -52,6 +53,7 @@ import java.sql.SQLException
@RunWith(MockitoJUnitRunner::class) @RunWith(MockitoJUnitRunner::class)
open class BaseUnitTest { open class BaseUnitTest {
protected open lateinit var habitList: HabitList protected open lateinit var habitList: HabitList
protected open lateinit var habitGroupList: HabitGroupList
protected lateinit var fixtures: HabitFixtures protected lateinit var fixtures: HabitFixtures
protected lateinit var modelFactory: ModelFactory protected lateinit var modelFactory: ModelFactory
protected lateinit var taskRunner: SingleThreadTaskRunner protected lateinit var taskRunner: SingleThreadTaskRunner
@ -80,6 +82,7 @@ open class BaseUnitTest {
setStartDayOffset(0, 0) setStartDayOffset(0, 0)
val memoryModelFactory = MemoryModelFactory() val memoryModelFactory = MemoryModelFactory()
habitList = spy(memoryModelFactory.buildHabitList()) habitList = spy(memoryModelFactory.buildHabitList())
habitGroupList = spy(memoryModelFactory.buildHabitGroupList())
fixtures = HabitFixtures(memoryModelFactory, habitList) fixtures = HabitFixtures(memoryModelFactory, habitList)
modelFactory = memoryModelFactory modelFactory = memoryModelFactory
taskRunner = SingleThreadTaskRunner() taskRunner = SingleThreadTaskRunner()

@ -43,7 +43,7 @@ class HabitCardListCacheTest : BaseUnitTest() {
for (i in 0..9) { for (i in 0..9) {
if (i == 3) habitList.add(fixtures.createLongHabit()) else habitList.add(fixtures.createShortHabit()) 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.setCheckmarkCount(10)
cache.refreshAllHabits() cache.refreshAllHabits()
cache.onAttached() cache.onAttached()
@ -82,8 +82,8 @@ class HabitCardListCacheTest : BaseUnitTest() {
val h = habitList.getByPosition(3) val h = habitList.getByPosition(3)
val score = h.scores[today].value val score = h.scores[today].value
assertThat(cache.getHabitByPosition(3), equalTo(h)) assertThat(cache.getHabitByPosition(3), equalTo(h))
assertThat(cache.getScore(h.id!!), equalTo(score)) assertThat(cache.getScore(h.uuid!!), equalTo(score))
val actualCheckmarks = cache.getCheckmarks(h.id!!) val actualCheckmarks = cache.getCheckmarks(h.uuid!!)
val expectedCheckmarks = h val expectedCheckmarks = h
.computedEntries .computedEntries

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

Loading…
Cancel
Save