mirror of https://github.com/iSoron/uhabits.git
parent
26fb76f95f
commit
e84cc8e8b1
@ -1,468 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
|
||||||
*
|
|
||||||
* This file is part of Loop Habit Tracker.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by the
|
|
||||||
* Free Software Foundation, either version 3 of the License, or (at your
|
|
||||||
* option) any later version.
|
|
||||||
*
|
|
||||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
|
||||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
||||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
||||||
* more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along
|
|
||||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.isoron.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.
|
|
||||||
* <p>
|
|
||||||
* 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.
|
|
||||||
* <p>
|
|
||||||
* 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<Long, Habit> id_to_habit;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public final List<Habit> habits;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public final HashMap<Long, int[]> checkmarks;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public final HashMap<Long, Double> 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<Long> before = data.id_to_habit.keySet();
|
|
||||||
Set<Long> after = newData.id_to_habit.keySet();
|
|
||||||
|
|
||||||
Set<Long> removed = new TreeSet<>(before);
|
|
||||||
removed.removeAll(after);
|
|
||||||
|
|
||||||
for (Long id : removed) remove(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,369 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||||
|
*
|
||||||
|
* This file is part of Loop Habit Tracker.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation, either version 3 of the License, or (at your
|
||||||
|
* option) any later version.
|
||||||
|
*
|
||||||
|
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||||
|
* more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along
|
||||||
|
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.isoron.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<Long?, Habit> = HashMap()
|
||||||
|
val habits: MutableList<Habit>
|
||||||
|
val checkmarks: HashMap<Long?, IntArray>
|
||||||
|
val scores: HashMap<Long?, Double>
|
||||||
|
@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<Int> = 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<Long?> = data.idToHabit.keys
|
||||||
|
val after: Set<Long?> = newData.idToHabit.keys
|
||||||
|
val removed: MutableSet<Long?> = TreeSet(before)
|
||||||
|
removed.removeAll(after)
|
||||||
|
for (id in removed) remove(id!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
filteredHabits = allHabits
|
||||||
|
this.taskRunner = taskRunner
|
||||||
|
listener = object : Listener {}
|
||||||
|
data = CacheData()
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue