diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt index 8dae13f2d..10eeb4d5b 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/list/views/HabitCardListAdapter.kt @@ -121,7 +121,7 @@ class HabitCardListAdapter @Inject constructor( val score = cache.getScore(habit!!.id!!) val checkmarks = cache.getCheckmarks(habit.id!!) val selected = selected.contains(habit) - listView!!.bindCardView(holder, habit, score, checkmarks, selected) + listView!!.bindCardView(holder, habit, score, checkmarks!!, selected) } override fun onViewAttachedToWindow(holder: HabitCardViewHolder) { diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java deleted file mode 100644 index a0eeebe6d..000000000 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.java +++ /dev/null @@ -1,468 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * 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 . - */ - -package org.isoron.uhabits.core.ui.screens.habits.list; - -import androidx.annotation.*; - -import org.apache.commons.lang3.*; -import org.isoron.uhabits.core.*; -import org.isoron.uhabits.core.commands.*; -import org.isoron.uhabits.core.models.*; -import org.isoron.uhabits.core.tasks.*; -import org.isoron.uhabits.core.utils.*; - -import java.util.*; - -import javax.inject.*; - -/** - * A HabitCardListCache fetches and keeps a cache of all the data necessary to - * render a HabitCardListView. - *

- * This is needed since performing database lookups during scrolling can make - * the ListView very slow. It also registers itself as an observer of the - * models, in order to update itself automatically. - *

- * Note that this class is singleton-scoped, therefore it is shared among all - * activities. - */ -@AppScope -public class HabitCardListCache implements CommandRunner.Listener -{ - private int checkmarkCount; - - @Nullable - private Task currentFetchTask; - - @NonNull - private Listener listener; - - @NonNull - private CacheData data; - - @NonNull - private final HabitList allHabits; - - @NonNull - private HabitList filteredHabits; - - @NonNull - private final TaskRunner taskRunner; - - @NonNull - private final CommandRunner commandRunner; - - @Inject - public HabitCardListCache(@NonNull HabitList allHabits, - @NonNull CommandRunner commandRunner, - @NonNull TaskRunner taskRunner) - { - if (allHabits == null) throw new NullPointerException(); - if (commandRunner == null) throw new NullPointerException(); - if (taskRunner == null) throw new NullPointerException(); - - this.allHabits = allHabits; - this.commandRunner = commandRunner; - this.filteredHabits = allHabits; - this.taskRunner = taskRunner; - - this.listener = new Listener() - { - }; - data = new CacheData(); - } - - public synchronized void cancelTasks() - { - if (currentFetchTask != null) currentFetchTask.cancel(); - } - - public synchronized int[] getCheckmarks(long habitId) - { - return data.checkmarks.get(habitId); - } - - /** - * Returns the habits that occupies a certain position on the list. - * - * @param position the position of the habit - * @return the habit at given position or null if position is invalid - */ - @Nullable - public synchronized Habit getHabitByPosition(int position) - { - if (position < 0 || position >= data.habits.size()) return null; - return data.habits.get(position); - } - - public synchronized int getHabitCount() - { - return data.habits.size(); - } - - public synchronized HabitList.Order getPrimaryOrder() - { - return filteredHabits.getPrimaryOrder(); - } - - public synchronized HabitList.Order getSecondaryOrder() - { - return filteredHabits.getSecondaryOrder(); - } - - public synchronized double getScore(long habitId) - { - return data.scores.get(habitId); - } - - public synchronized void onAttached() - { - refreshAllHabits(); - commandRunner.addListener(this); - } - - @Override - public synchronized void onCommandFinished(@Nullable Command command) - { - if (command instanceof CreateRepetitionCommand) { - Habit h = ((CreateRepetitionCommand) command).getHabit(); - Long id = h.getId(); - if (id != null) refreshHabit(id); - } else { - refreshAllHabits(); - } - } - - public synchronized void onDetached() - { - commandRunner.removeListener(this); - } - - public synchronized void refreshAllHabits() - { - if (currentFetchTask != null) currentFetchTask.cancel(); - currentFetchTask = new RefreshTask(); - taskRunner.execute(currentFetchTask); - } - - public synchronized void refreshHabit(long id) - { - taskRunner.execute(new RefreshTask(id)); - } - - public synchronized void remove(long id) - { - Habit h = data.id_to_habit.get(id); - if (h == null) return; - - int position = data.habits.indexOf(h); - data.habits.remove(position); - data.id_to_habit.remove(id); - data.checkmarks.remove(id); - data.scores.remove(id); - - listener.onItemRemoved(position); - } - - public synchronized void reorder(int from, int to) - { - Habit fromHabit = data.habits.get(from); - data.habits.remove(from); - data.habits.add(to, fromHabit); - listener.onItemMoved(from, to); - } - - public synchronized void setCheckmarkCount(int checkmarkCount) - { - this.checkmarkCount = checkmarkCount; - } - - public synchronized void setFilter(@NonNull HabitMatcher matcher) - { - if (matcher == null) throw new NullPointerException(); - filteredHabits = allHabits.getFiltered(matcher); - } - - public synchronized void setListener(@NonNull Listener listener) - { - if (listener == null) throw new NullPointerException(); - this.listener = listener; - } - - public synchronized void setPrimaryOrder(@NonNull HabitList.Order order) - { - if (order == null) throw new NullPointerException(); - allHabits.setPrimaryOrder(order); - filteredHabits.setPrimaryOrder(order); - refreshAllHabits(); - } - - public synchronized void setSecondaryOrder(@NonNull HabitList.Order order) - { - allHabits.setSecondaryOrder(order); - filteredHabits.setSecondaryOrder(order); - refreshAllHabits(); - } - - - /** - * Interface definition for a callback to be invoked when the data on the - * cache has been modified. - */ - public interface Listener - { - default void onItemChanged(int position) - { - } - - default void onItemInserted(int position) - { - } - - default void onItemMoved(int oldPosition, int newPosition) - { - } - - default void onItemRemoved(int position) - { - } - - default void onRefreshFinished() - { - } - } - - private class CacheData - { - @NonNull - public final HashMap id_to_habit; - - @NonNull - public final List habits; - - @NonNull - public final HashMap checkmarks; - - @NonNull - public final HashMap scores; - - /** - * Creates a new CacheData without any content. - */ - public CacheData() - { - id_to_habit = new HashMap<>(); - habits = new LinkedList<>(); - checkmarks = new HashMap<>(); - scores = new HashMap<>(); - } - - public synchronized void copyCheckmarksFrom(@NonNull CacheData oldData) - { - if (oldData == null) throw new NullPointerException(); - - int[] empty = new int[checkmarkCount]; - - for (Long id : id_to_habit.keySet()) - { - if (oldData.checkmarks.containsKey(id)) - checkmarks.put(id, oldData.checkmarks.get(id)); - else checkmarks.put(id, empty); - } - } - - public synchronized void copyScoresFrom(@NonNull CacheData oldData) - { - if (oldData == null) throw new NullPointerException(); - - for (Long id : id_to_habit.keySet()) - { - if (oldData.scores.containsKey(id)) - scores.put(id, oldData.scores.get(id)); - else scores.put(id, 0.0); - } - } - - public synchronized void fetchHabits() - { - for (Habit h : filteredHabits) - { - if (h.getId() == null) continue; - habits.add(h); - id_to_habit.put(h.getId(), h); - } - } - } - - private class RefreshTask implements Task - { - @NonNull - private final CacheData newData; - - @Nullable - private final Long targetId; - - private boolean isCancelled; - - @Nullable - private TaskRunner runner; - - public RefreshTask() - { - newData = new CacheData(); - targetId = null; - isCancelled = false; - } - - public RefreshTask(long targetId) - { - newData = new CacheData(); - this.targetId = targetId; - } - - @Override - public synchronized void cancel() - { - isCancelled = true; - } - - @Override - public synchronized void doInBackground() - { - newData.fetchHabits(); - newData.copyScoresFrom(data); - newData.copyCheckmarksFrom(data); - - Timestamp today = DateUtils.getTodayWithOffset(); - Timestamp dateFrom = today.minus(checkmarkCount - 1); - - if (runner != null) runner.publishProgress(this, -1); - - for (int position = 0; position < newData.habits.size(); position++) - { - if (isCancelled) return; - - Habit habit = newData.habits.get(position); - Long id = habit.getId(); - if (targetId != null && !targetId.equals(id)) continue; - - newData.scores.put(id, habit.getScores().get(today).getValue()); - Integer[] entries = habit.getComputedEntries() - .getByInterval(dateFrom, today) - .stream() - .map(Entry::getValue) - .toArray(Integer[]::new); - newData.checkmarks.put(id, ArrayUtils.toPrimitive(entries)); - - runner.publishProgress(this, position); - } - } - - @Override - public synchronized void onAttached(@NonNull TaskRunner runner) - { - if (runner == null) throw new NullPointerException(); - this.runner = runner; - } - - @Override - public synchronized void onPostExecute() - { - currentFetchTask = null; - listener.onRefreshFinished(); - } - - @Override - public synchronized void onProgressUpdate(int currentPosition) - { - if (currentPosition < 0) processRemovedHabits(); - else processPosition(currentPosition); - } - - private synchronized void performInsert(Habit habit, int position) - { - Long id = habit.getId(); - data.habits.add(position, habit); - data.id_to_habit.put(id, habit); - data.scores.put(id, newData.scores.get(id)); - data.checkmarks.put(id, newData.checkmarks.get(id)); - listener.onItemInserted(position); - } - - private synchronized void performMove(@NonNull Habit habit, - int fromPosition, - int toPosition) - { - if(habit == null) throw new NullPointerException(); - data.habits.remove(fromPosition); - data.habits.add(toPosition, habit); - listener.onItemMoved(fromPosition, toPosition); - } - - private synchronized void performUpdate(long id, int position) - { - double oldScore = data.scores.get(id); - int[] oldCheckmarks = data.checkmarks.get(id); - - double newScore = newData.scores.get(id); - int[] newCheckmarks = newData.checkmarks.get(id); - - boolean unchanged = true; - if (oldScore != newScore) unchanged = false; - if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false; - if (unchanged) return; - - data.scores.put(id, newScore); - data.checkmarks.put(id, newCheckmarks); - listener.onItemChanged(position); - } - - private synchronized void processPosition(int currentPosition) - { - Habit habit = newData.habits.get(currentPosition); - Long id = habit.getId(); - - int prevPosition = data.habits.indexOf(habit); - - if (prevPosition < 0) - { - performInsert(habit, currentPosition); - } - else - { - if (prevPosition != currentPosition) - performMove(habit, prevPosition, currentPosition); - - performUpdate(id, currentPosition); - } - } - - private synchronized void processRemovedHabits() - { - Set before = data.id_to_habit.keySet(); - Set after = newData.id_to_habit.keySet(); - - Set removed = new TreeSet<>(before); - removed.removeAll(after); - - for (Long id : removed) remove(id); - } - } -} 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 new file mode 100644 index 000000000..2f0fdf17e --- /dev/null +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/list/HabitCardListCache.kt @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * 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 . + */ +package org.isoron.uhabits.core.ui.screens.habits.list + +import org.apache.commons.lang3.ArrayUtils +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.models.Habit +import org.isoron.uhabits.core.models.HabitList +import org.isoron.uhabits.core.models.HabitList.Order +import org.isoron.uhabits.core.models.HabitMatcher +import org.isoron.uhabits.core.tasks.Task +import org.isoron.uhabits.core.tasks.TaskRunner +import org.isoron.uhabits.core.utils.DateUtils.Companion.getTodayWithOffset +import java.util.ArrayList +import java.util.Arrays +import java.util.HashMap +import java.util.LinkedList +import java.util.TreeSet +import javax.inject.Inject + +/** + * A HabitCardListCache fetches and keeps a cache of all the data necessary to + * render a HabitCardListView. + * + * + * This is needed since performing database lookups during scrolling can make + * the ListView very slow. It also registers itself as an observer of the + * models, in order to update itself automatically. + * + * + * Note that this class is singleton-scoped, therefore it is shared among all + * activities. + */ +@AppScope +class HabitCardListCache @Inject constructor( + private val allHabits: HabitList, + private val commandRunner: CommandRunner, + taskRunner: TaskRunner +) : CommandRunner.Listener { + private var checkmarkCount = 0 + private var currentFetchTask: Task? = null + private var listener: Listener + private val data: CacheData + private var filteredHabits: HabitList + private val taskRunner: TaskRunner + @Synchronized + fun cancelTasks() { + currentFetchTask?.cancel() + } + + @Synchronized + fun getCheckmarks(habitId: Long): IntArray { + return data.checkmarks[habitId]!! + } + + /** + * Returns the habits that occupies a certain position on the list. + * + * @param position the position of the habit + * @return the habit at given position or null if position is invalid + */ + @Synchronized + fun getHabitByPosition(position: Int): Habit? { + return if (position < 0 || position >= data.habits.size) null else data.habits[position] + } + + @get:Synchronized + val habitCount: Int + get() = data.habits.size + + @get:Synchronized + @set:Synchronized + var primaryOrder: Order + get() = filteredHabits.primaryOrder + set(order) { + allHabits.primaryOrder = order + filteredHabits.primaryOrder = order + refreshAllHabits() + } + + @get:Synchronized + @set:Synchronized + var secondaryOrder: Order + get() = filteredHabits.secondaryOrder + set(order) { + allHabits.secondaryOrder = order + filteredHabits.secondaryOrder = order + refreshAllHabits() + } + + @Synchronized + fun getScore(habitId: Long): Double { + return data.scores[habitId]!! + } + + @Synchronized + fun onAttached() { + refreshAllHabits() + commandRunner.addListener(this) + } + + @Synchronized + override fun onCommandFinished(command: Command) { + if (command is CreateRepetitionCommand) { + val (_, _, _, id) = command.habit + id?.let { refreshHabit(it) } + } else { + refreshAllHabits() + } + } + + @Synchronized + fun onDetached() { + commandRunner.removeListener(this) + } + + @Synchronized + fun refreshAllHabits() { + if (currentFetchTask != null) currentFetchTask!!.cancel() + currentFetchTask = RefreshTask() + taskRunner.execute(currentFetchTask) + } + + @Synchronized + fun refreshHabit(id: Long) { + taskRunner.execute(RefreshTask(id)) + } + + @Synchronized + fun remove(id: Long) { + val h = data.idToHabit[id] ?: return + val position = data.habits.indexOf(h) + data.habits.removeAt(position) + data.idToHabit.remove(id) + data.checkmarks.remove(id) + data.scores.remove(id) + listener.onItemRemoved(position) + } + + @Synchronized + fun reorder(from: Int, to: Int) { + val fromHabit = data.habits[from] + data.habits.removeAt(from) + data.habits.add(to, fromHabit) + listener.onItemMoved(from, to) + } + + @Synchronized + fun setCheckmarkCount(checkmarkCount: Int) { + this.checkmarkCount = checkmarkCount + } + + @Synchronized + fun setFilter(matcher: HabitMatcher) { + filteredHabits = allHabits.getFiltered(matcher) + } + + @Synchronized + fun setListener(listener: Listener) { + this.listener = listener + } + + /** + * Interface definition for a callback to be invoked when the data on the + * cache has been modified. + */ + interface Listener { + fun onItemChanged(position: Int) {} + fun onItemInserted(position: Int) {} + fun onItemMoved(oldPosition: Int, newPosition: Int) {} + fun onItemRemoved(position: Int) {} + fun onRefreshFinished() {} + } + + private inner class CacheData { + val idToHabit: HashMap = HashMap() + val habits: MutableList + val checkmarks: HashMap + val scores: HashMap + @Synchronized + fun copyCheckmarksFrom(oldData: CacheData) { + val empty = IntArray(checkmarkCount) + for (id in idToHabit.keys) { + if (oldData.checkmarks.containsKey(id)) checkmarks[id] = + oldData.checkmarks[id]!! else checkmarks[id] = empty + } + } + + @Synchronized + fun copyScoresFrom(oldData: CacheData) { + for (id in idToHabit.keys) { + if (oldData.scores.containsKey(id)) scores[id] = + oldData.scores[id]!! else scores[id] = 0.0 + } + } + + @Synchronized + fun fetchHabits() { + for (h in filteredHabits) { + if (h.id == null) continue + habits.add(h) + idToHabit[h.id] = h + } + } + + /** + * Creates a new CacheData without any content. + */ + init { + habits = LinkedList() + checkmarks = HashMap() + scores = HashMap() + } + } + + private inner class RefreshTask : Task { + private val newData: CacheData + private val targetId: Long? + private var isCancelled = false + private var runner: TaskRunner? = null + + constructor() { + newData = CacheData() + targetId = null + isCancelled = false + } + + constructor(targetId: Long) { + newData = CacheData() + this.targetId = targetId + } + + @Synchronized + override fun cancel() { + isCancelled = true + } + + @Synchronized + override fun doInBackground() { + newData.fetchHabits() + newData.copyScoresFrom(data) + newData.copyCheckmarksFrom(data) + val today = getTodayWithOffset() + val dateFrom = today.minus(checkmarkCount - 1) + if (runner != null) runner!!.publishProgress(this, -1) + for (position in newData.habits.indices) { + if (isCancelled) return + val (_, _, _, id, _, _, _, _, _, _, _, _, _, _, computedEntries, _, scores) = newData.habits[position] + if (targetId != null && targetId != id) continue + newData.scores[id] = scores[today].value + val list: MutableList = ArrayList() + for ( + (_, value) in computedEntries + .getByInterval(dateFrom, today) + ) { + list.add(value) + } + val entries = list.toTypedArray() + newData.checkmarks[id] = ArrayUtils.toPrimitive(entries) + runner!!.publishProgress(this, position) + } + } + + @Synchronized + override fun onAttached(runner: TaskRunner) { + this.runner = runner + } + + @Synchronized + override fun onPostExecute() { + currentFetchTask = null + listener.onRefreshFinished() + } + + @Synchronized + override fun onProgressUpdate(currentPosition: Int) { + if (currentPosition < 0) processRemovedHabits() else processPosition(currentPosition) + } + + @Synchronized + private fun performInsert(habit: Habit, position: Int) { + val id = habit.id + data.habits.add(position, habit) + data.idToHabit[id] = habit + data.scores[id] = newData.scores[id]!! + data.checkmarks[id] = newData.checkmarks[id]!! + listener.onItemInserted(position) + } + + @Synchronized + private fun performMove( + habit: Habit, + fromPosition: Int, + toPosition: Int + ) { + data.habits.removeAt(fromPosition) + data.habits.add(toPosition, habit) + listener.onItemMoved(fromPosition, toPosition) + } + + @Synchronized + private fun performUpdate(id: Long, position: Int) { + val oldScore = data.scores[id]!! + val oldCheckmarks = data.checkmarks[id] + val newScore = newData.scores[id]!! + val newCheckmarks = newData.checkmarks[id]!! + var unchanged = true + if (oldScore != newScore) unchanged = false + if (!Arrays.equals(oldCheckmarks, newCheckmarks)) unchanged = false + if (unchanged) return + data.scores[id] = newScore + data.checkmarks[id] = newCheckmarks + listener.onItemChanged(position) + } + + @Synchronized + private fun processPosition(currentPosition: Int) { + val habit = newData.habits[currentPosition] + val id = habit.id + val prevPosition = data.habits.indexOf(habit) + if (prevPosition < 0) { + performInsert(habit, currentPosition) + } else { + if (prevPosition != currentPosition) performMove( + habit, + prevPosition, + currentPosition + ) + if (id == null) throw NullPointerException() + performUpdate(id, currentPosition) + } + } + + @Synchronized + private fun processRemovedHabits() { + val before: Set = data.idToHabit.keys + val after: Set = newData.idToHabit.keys + val removed: MutableSet = TreeSet(before) + removed.removeAll(after) + for (id in removed) remove(id!!) + } + } + + init { + filteredHabits = allHabits + this.taskRunner = taskRunner + listener = object : Listener {} + data = CacheData() + } +}