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