/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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.activities.habits.list.model;
import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.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;
private Task currentFetchTask;
@NonNull
private Listener listener;
@NonNull
private CacheData data;
@NonNull
private HabitList allHabits;
@NonNull
private HabitList filteredHabits;
private final TaskRunner taskRunner;
private final CommandRunner commandRunner;
@Inject
public HabitCardListCache(@NonNull HabitList allHabits,
@NonNull CommandRunner commandRunner,
@NonNull TaskRunner taskRunner)
{
this.allHabits = allHabits;
this.commandRunner = commandRunner;
this.filteredHabits = allHabits;
this.taskRunner = taskRunner;
this.listener = new Listener() {};
data = new CacheData();
}
public void cancelTasks()
{
if (currentFetchTask != null) currentFetchTask.cancel();
}
public 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
* @throws IndexOutOfBoundsException if position is not valid
*/
@NonNull
public Habit getHabitByPosition(int position)
{
return data.habits.get(position);
}
public int getHabitCount()
{
return data.habits.size();
}
public HabitList.Order getOrder()
{
return filteredHabits.getOrder();
}
public int getScore(long habitId)
{
return data.scores.get(habitId);
}
public void onAttached()
{
refreshAllHabits();
commandRunner.addListener(this);
}
@Override
public void onCommandExecuted(@NonNull Command command,
@Nullable Long refreshKey)
{
if (refreshKey == null) refreshAllHabits();
else refreshHabit(refreshKey);
}
public void onDetached()
{
commandRunner.removeListener(this);
}
public void refreshAllHabits()
{
if (currentFetchTask != null) currentFetchTask.cancel();
currentFetchTask = new RefreshTask();
taskRunner.execute(currentFetchTask);
}
public void refreshHabit(long id)
{
taskRunner.execute(new RefreshTask(id));
}
public void remove(@NonNull 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 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 void setCheckmarkCount(int checkmarkCount)
{
this.checkmarkCount = checkmarkCount;
}
public void setFilter(HabitMatcher matcher)
{
filteredHabits = allHabits.getFiltered(matcher);
}
public void setListener(@NonNull Listener listener)
{
this.listener = listener;
}
public void setOrder(HabitList.Order order)
{
allHabits.setOrder(order);
filteredHabits.setOrder(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 HashMap<Long, Habit> id_to_habit;
@NonNull
public List<Habit> habits;
@NonNull
public HashMap<Long, int[]> checkmarks;
@NonNull
public HashMap<Long, Integer> 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 void copyCheckmarksFrom(@NonNull CacheData oldData)
{
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 void copyScoresFrom(@NonNull CacheData oldData)
{
for (Long id : id_to_habit.keySet())
{
if (oldData.scores.containsKey(id))
scores.put(id, oldData.scores.get(id));
else scores.put(id, 0);
}
}
public void fetchHabits()
{
for (Habit h : filteredHabits)
{
habits.add(h);
id_to_habit.put(h.getId(), h);
}
}
}
private class RefreshTask implements Task
{
@NonNull
private CacheData newData;
@Nullable
private Long targetId;
private boolean isCancelled;
private TaskRunner runner;
public RefreshTask()
{
newData = new CacheData();
targetId = null;
isCancelled = false;
}
public RefreshTask(long targetId)
{
newData = new CacheData();
this.targetId = targetId;
}
@Override
public void cancel()
{
isCancelled = true;
}
@Override
public void doInBackground()
{
newData.fetchHabits();
newData.copyScoresFrom(data);
newData.copyCheckmarksFrom(data);
long day = DateUtils.millisecondsInOneDay;
long dateTo = DateUtils.getStartOfDay(DateUtils.getLocalTime());
long dateFrom = dateTo - (checkmarkCount - 1) * day;
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().getTodayValue());
newData.checkmarks.put(id,
habit.getCheckmarks().getValues(dateFrom, dateTo));
runner.publishProgress(this, position);
}
}
@Override
public void onAttached(@NonNull TaskRunner runner)
{
this.runner = runner;
}
@Override
public void onPostExecute()
{
currentFetchTask = null;
listener.onRefreshFinished();
}
@Override
public void onProgressUpdate(int currentPosition)
{
if (currentPosition < 0) processRemovedHabits();
else processPosition(currentPosition);
}
private 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 void performMove(Habit habit, int fromPosition, int toPosition)
{
data.habits.remove(fromPosition);
data.habits.add(toPosition, habit);
listener.onItemMoved(fromPosition, toPosition);
}
private void performUpdate(Long id, int position)
{
Integer oldScore = data.scores.get(id);
int[] oldCheckmarks = data.checkmarks.get(id);
Integer newScore = newData.scores.get(id);
int[] newCheckmarks = newData.checkmarks.get(id);
boolean unchanged = true;
if (!oldScore.equals(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 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)
performUpdate(id, currentPosition);
else performMove(habit, prevPosition, currentPosition);
}
private 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);
}
}
}