Move remaining model classes

This commit is contained in:
2017-05-25 08:54:21 -04:00
parent d23b59ced2
commit 51ca4aa98e
37 changed files with 162 additions and 107 deletions

View File

@@ -0,0 +1,299 @@
/*
* 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.models;
import android.support.annotation.*;
import org.isoron.uhabits.utils.*;
import java.io.*;
import java.text.*;
import java.util.*;
import javax.annotation.concurrent.*;
import static org.isoron.uhabits.models.Checkmark.*;
/**
* The collection of {@link Checkmark}s belonging to a habit.
*/
@ThreadSafe
public abstract class CheckmarkList
{
protected final Habit habit;
public final ModelObservable observable;
public CheckmarkList(Habit habit)
{
this.habit = habit;
this.observable = new ModelObservable();
}
/**
* Adds all the given checkmarks to the list.
* <p>
* This should never be called by the application, since the checkmarks are
* computed automatically from the list of repetitions.
*
* @param checkmarks the checkmarks to be added.
*/
public abstract void add(List<Checkmark> checkmarks);
/**
* Returns the values for all the checkmarks, since the oldest repetition of
* the habit until today.
* <p>
* If there are no repetitions at all, returns an empty array. The values
* are returned in an array containing one integer value for each day since
* the first repetition of the habit until today. The first entry
* corresponds to today, the second entry corresponds to yesterday, and so
* on.
*
* @return values for the checkmarks in the interval
*/
@NonNull
public synchronized final int[] getAllValues()
{
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return new int[0];
Long fromTimestamp = oldestRep.getTimestamp();
Long toTimestamp = DateUtils.getStartOfToday();
return getValues(fromTimestamp, toTimestamp);
}
/**
* Returns the list of checkmarks that fall within the given interval.
* <p>
* There is exactly one checkmark per day in the interval. The endpoints of
* the interval are included. The list is ordered by timestamp (decreasing).
* That is, the first checkmark corresponds to the newest timestamp, and the
* last checkmark corresponds to the oldest timestamp.
*
* @param fromTimestamp timestamp of the beginning of the interval.
* @param toTimestamp timestamp of the end of the interval.
* @return the list of checkmarks within the interval.
*/
@NonNull
public abstract List<Checkmark> getByInterval(long fromTimestamp,
long toTimestamp);
/**
* Returns the checkmark for today.
*
* @return checkmark for today
*/
@Nullable
public synchronized final Checkmark getToday()
{
computeAll();
return getNewestComputed();
}
/**
* Returns the value of today's checkmark.
*
* @return value of today's checkmark
*/
public synchronized final int getTodayValue()
{
Checkmark today = getToday();
if (today != null) return today.getValue();
else return Checkmark.UNCHECKED;
}
/**
* Returns the values of the checkmarks that fall inside a certain interval
* of time.
* <p>
* The values are returned in an array containing one integer value for each
* day of the interval. The first entry corresponds to the most recent day
* in the interval. Each subsequent entry corresponds to one day older than
* the previous entry. The boundaries of the time interval are included.
*
* @param from timestamp for the oldest checkmark
* @param to timestamp for the newest checkmark
* @return values for the checkmarks inside the given interval
*/
public final int[] getValues(long from, long to)
{
if(from > to) return new int[0];
List<Checkmark> checkmarks = getByInterval(from, to);
int values[] = new int[checkmarks.size()];
int i = 0;
for (Checkmark c : checkmarks)
values[i++] = c.getValue();
return values;
}
/**
* Marks as invalid every checkmark that has timestamp either equal or newer
* than a given timestamp. These checkmarks will be recomputed at the next
* time they are queried.
*
* @param timestamp the timestamp
*/
public abstract void invalidateNewerThan(long timestamp);
/**
* Writes the entire list of checkmarks to the given writer, in CSV format.
*
* @param out the writer where the CSV will be output
* @throws IOException in case write operations fail
*/
public final void writeCSV(Writer out) throws IOException
{
int values[];
synchronized (this)
{
computeAll();
values = getAllValues();
}
long timestamp = DateUtils.getStartOfToday();
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
for (int value : values)
{
String date = dateFormat.format(new Date(timestamp));
out.write(String.format("%s,%d\n", date, value));
timestamp -= DateUtils.millisecondsInOneDay;
}
}
/**
* Computes and stores one checkmark for each day that falls inside the
* specified interval of time. Days that already have a corresponding
* checkmark are skipped.
*
* This method assumes the list of computed checkmarks has no holes. That
* is, if there is a checkmark computed at time t1 and another at time t2,
* then every checkmark between t1 and t2 is also computed.
*
* @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval
*/
protected final synchronized void compute(long from, long to)
{
final long day = DateUtils.millisecondsInOneDay;
Checkmark newest = getNewestComputed();
Checkmark oldest = getOldestComputed();
if (newest == null)
{
forceRecompute(from, to);
}
else
{
forceRecompute(from, oldest.getTimestamp() - day);
forceRecompute(newest.getTimestamp() + day, to);
}
}
/**
* Returns oldest checkmark that has already been computed.
*
* @return oldest checkmark already computed
*/
protected abstract Checkmark getOldestComputed();
/**
* Computes and stores one checkmark for each day that falls inside the
* specified interval of time.
*
* This method does not check if the checkmarks have already been
* computed or not. If they have, then duplicate checkmarks will
* be stored, which is a bad thing.
*
* @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval
*/
private synchronized void forceRecompute(long from, long to)
{
if (from > to) return;
final long day = DateUtils.millisecondsInOneDay;
Frequency freq = habit.getFrequency();
long fromExtended = from - (long) (freq.getDenominator()) * day;
List<Repetition> reps =
habit.getRepetitions().getByInterval(fromExtended, to);
final int nDays = (int) ((to - from) / day) + 1;
int nDaysExtended = (int) ((to - fromExtended) / day) + 1;
final int checks[] = new int[nDaysExtended];
for (Repetition rep : reps)
{
int offset = (int) ((rep.getTimestamp() - fromExtended) / day);
checks[nDaysExtended - offset - 1] = rep.getValue();
}
for (int i = 0; i < nDays; i++)
{
int counter = 0;
for (int j = 0; j < freq.getDenominator(); j++)
if (checks[i + j] == CHECKED_EXPLICITLY) counter++;
if (counter >= freq.getNumerator())
if (checks[i] != CHECKED_EXPLICITLY)
checks[i] = CHECKED_IMPLICITLY;
}
List<Checkmark> checkmarks = new LinkedList<>();
for (int i = 0; i < nDays; i++)
{
int value = checks[i];
long timestamp = to - i * day;
checkmarks.add(new Checkmark(timestamp, value));
}
add(checkmarks);
}
/**
* Computes and stores one checkmark for each day, since the first
* repetition of the habit until today. Days that already have a
* corresponding checkmark are skipped.
*/
private synchronized void computeAll()
{
Repetition oldest = habit.getRepetitions().getOldest();
if (oldest == null) return;
Long today = DateUtils.getStartOfToday();
compute(oldest.getTimestamp(), today);
}
/**
* Returns newest checkmark that has already been computed.
*
* @return newest checkmark already computed
*/
protected abstract Checkmark getNewestComputed();
}

View File

@@ -0,0 +1,450 @@
/*
* 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.models;
import android.support.annotation.*;
import org.apache.commons.lang3.builder.*;
import java.util.*;
import javax.annotation.concurrent.*;
import javax.inject.*;
import static org.isoron.uhabits.models.Checkmark.*;
/**
* The thing that the user wants to track.
*/
@ThreadSafe
public class Habit
{
public static final int AT_LEAST = 0;
public static final int AT_MOST = 1;
public static final String HABIT_URI_FORMAT =
"content://org.isoron.uhabits/habit/%d";
public static final int NUMBER_HABIT = 1;
public static final int YES_NO_HABIT = 0;
@Nullable
public Long id;
@NonNull
private HabitData data;
@NonNull
private StreakList streaks;
@NonNull
private ScoreList scores;
@NonNull
private RepetitionList repetitions;
@NonNull
private CheckmarkList checkmarks;
private ModelObservable observable = new ModelObservable();
/**
* Constructs a habit with default data.
* <p>
* The habit is not archived, not highlighted, has no reminders and is
* placed in the last position of the list of habits.
*/
@Inject
Habit(@NonNull ModelFactory factory)
{
this.data = new HabitData();
checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this);
repetitions = factory.buildRepetitionList(this);
}
Habit(@NonNull ModelFactory factory, @NonNull HabitData data)
{
this.data = new HabitData(data);
checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this);
repetitions = factory.buildRepetitionList(this);
observable = new ModelObservable();
}
/**
* Clears the reminder for a habit.
*/
public synchronized void clearReminder()
{
data.reminder = null;
observable.notifyListeners();
}
/**
* Copies all the attributes of the specified habit into this habit
*
* @param model the model whose attributes should be copied from
*/
public synchronized void copyFrom(@NonNull Habit model)
{
this.data = new HabitData(model.data);
observable.notifyListeners();
}
/**
* List of checkmarks belonging to this habit.
*/
@NonNull
public synchronized CheckmarkList getCheckmarks()
{
return checkmarks;
}
/**
* Color of the habit.
* <p>
* This number is not an android.graphics.Color, but an index to the
* activity color palette, which changes according to the theme. To convert
* this color into an android.graphics.Color, use ColorHelper.getColor(context,
* habit.color).
*/
@NonNull
public synchronized Integer getColor()
{
return data.color;
}
public synchronized void setColor(@NonNull Integer color)
{
data.color = color;
}
@NonNull
public synchronized String getDescription()
{
return data.description;
}
public synchronized void setDescription(@NonNull String description)
{
data.description = description;
}
@NonNull
public synchronized Frequency getFrequency()
{
return data.frequency;
}
public synchronized void setFrequency(@NonNull Frequency frequency)
{
data.frequency = frequency;
}
@Nullable
public synchronized Long getId()
{
return id;
}
public synchronized void setId(@Nullable Long id)
{
this.id = id;
}
@NonNull
public synchronized String getName()
{
return data.name;
}
public synchronized void setName(@NonNull String name)
{
data.name = name;
}
public ModelObservable getObservable()
{
return observable;
}
/**
* Returns the reminder for this habit.
* <p>
* Before calling this method, you should call {@link #hasReminder()} to
* verify that a reminder does exist, otherwise an exception will be
* thrown.
*
* @return the reminder for this habit
* @throws IllegalStateException if habit has no reminder
*/
@NonNull
public synchronized Reminder getReminder()
{
if (data.reminder == null) throw new IllegalStateException();
return data.reminder;
}
public synchronized void setReminder(@Nullable Reminder reminder)
{
data.reminder = reminder;
}
@NonNull
public RepetitionList getRepetitions()
{
return repetitions;
}
@NonNull
public ScoreList getScores()
{
return scores;
}
@NonNull
public StreakList getStreaks()
{
return streaks;
}
public synchronized int getTargetType()
{
return data.targetType;
}
public synchronized void setTargetType(int targetType)
{
if (targetType != AT_LEAST && targetType != AT_MOST)
throw new IllegalArgumentException();
data.targetType = targetType;
}
public synchronized double getTargetValue()
{
return data.targetValue;
}
public synchronized void setTargetValue(double targetValue)
{
if (targetValue < 0) throw new IllegalArgumentException();
data.targetValue = targetValue;
}
public synchronized int getType()
{
return data.type;
}
public synchronized void setType(int type)
{
if (type != YES_NO_HABIT && type != NUMBER_HABIT)
throw new IllegalArgumentException();
data.type = type;
}
@NonNull
public synchronized String getUnit()
{
return data.unit;
}
public synchronized void setUnit(@NonNull String unit)
{
data.unit = unit;
}
/**
* Returns the public URI that identifies this habit
*
* @return the URI
*/
public String getUriString()
{
return String.format(Locale.US, HABIT_URI_FORMAT, getId());
}
public synchronized boolean hasId()
{
return getId() != null;
}
/**
* Returns whether the habit has a reminder.
*
* @return true if habit has reminder, false otherwise
*/
public synchronized boolean hasReminder()
{
return data.reminder != null;
}
public void invalidateNewerThan(long timestamp)
{
getScores().invalidateNewerThan(timestamp);
getCheckmarks().invalidateNewerThan(timestamp);
getStreaks().invalidateNewerThan(timestamp);
}
public synchronized boolean isArchived()
{
return data.archived;
}
public synchronized void setArchived(boolean archived)
{
data.archived = archived;
}
public synchronized boolean isCompletedToday()
{
int todayCheckmark = getCheckmarks().getTodayValue();
if (isNumerical()) return todayCheckmark >= data.targetValue;
else return (todayCheckmark != UNCHECKED);
}
public synchronized boolean isNumerical()
{
return data.type == NUMBER_HABIT;
}
public HabitData getData()
{
return new HabitData(data);
}
public static class HabitData
{
@NonNull
public String name;
@NonNull
public String description;
@NonNull
public Frequency frequency;
public int color;
public boolean archived;
public int targetType;
public double targetValue;
public int type;
@NonNull
public String unit;
@Nullable
public Reminder reminder;
public HabitData()
{
this.color = 5;
this.archived = false;
this.frequency = new Frequency(3, 7);
this.type = YES_NO_HABIT;
this.name = "";
this.description = "";
this.targetType = AT_LEAST;
this.targetValue = 100;
this.unit = "";
}
public HabitData(@NonNull HabitData model)
{
this.name = model.name;
this.description = model.description;
this.frequency = model.frequency;
this.color = model.color;
this.archived = model.archived;
this.targetType = model.targetType;
this.targetValue = model.targetValue;
this.type = model.type;
this.unit = model.unit;
this.reminder = model.reminder;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("name", name)
.append("description", description)
.append("frequency", frequency)
.append("color", color)
.append("archived", archived)
.append("targetType", targetType)
.append("targetValue", targetValue)
.append("type", type)
.append("unit", unit)
.append("reminder", reminder)
.toString();
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HabitData habitData = (HabitData) o;
return new EqualsBuilder()
.append(color, habitData.color)
.append(archived, habitData.archived)
.append(targetType, habitData.targetType)
.append(targetValue, habitData.targetValue)
.append(type, habitData.type)
.append(name, habitData.name)
.append(description, habitData.description)
.append(frequency, habitData.frequency)
.append(unit, habitData.unit)
.append(reminder, habitData.reminder)
.isEquals();
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(name)
.append(description)
.append(frequency)
.append(color)
.append(archived)
.append(targetType)
.append(targetValue)
.append(type)
.append(unit)
.append(reminder)
.toHashCode();
}
}
}

View File

@@ -0,0 +1,249 @@
/*
* 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.models;
import android.support.annotation.*;
import com.opencsv.*;
import org.isoron.uhabits.utils.*;
import java.io.*;
import java.util.*;
import javax.annotation.concurrent.*;
/**
* An ordered collection of {@link Habit}s.
*/
@ThreadSafe
public abstract class HabitList implements Iterable<Habit>
{
private final ModelObservable observable;
@NonNull
protected final HabitMatcher filter;
/**
* Creates a new HabitList.
* <p>
* Depending on the implementation, this list can either be empty or be
* populated by some pre-existing habits, for example, from a certain
* database.
*/
public HabitList()
{
observable = new ModelObservable();
filter = new HabitMatcherBuilder().setArchivedAllowed(true).build();
}
protected HabitList(@NonNull HabitMatcher filter)
{
observable = new ModelObservable();
this.filter = filter;
}
/**
* Inserts a new habit in the list.
* <p>
* 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.
*/
public abstract void add(@NonNull Habit habit)
throws IllegalArgumentException;
/**
* Returns the habit with specified id.
*
* @param id the id of the habit
* @return the habit, or null if none exist
*/
@Nullable
public abstract Habit getById(long id);
/**
* Returns the habit that occupies a certain position.
*
* @param position the position of the desired habit
* @return the habit at that position
* @throws IndexOutOfBoundsException when the position is invalid
*/
@NonNull
public abstract Habit getByPosition(int position);
/**
* Returns the list of habits that match a given condition.
*
* @param matcher the matcher that checks the condition
* @return the list of matching habits
*/
@NonNull
public abstract HabitList getFiltered(HabitMatcher matcher);
public ModelObservable getObservable()
{
return observable;
}
public abstract Order getOrder();
/**
* Changes the order of the elements on the list.
*
* @param order the new order criterea
*/
public abstract void setOrder(@NonNull Order order);
/**
* Returns the index of the given habit in the list, or -1 if the list does
* not contain the habit.
*
* @param h the habit
* @return the index of the habit, or -1 if not in the list
*/
public abstract int indexOf(@NonNull Habit h);
public boolean isEmpty()
{
return size() == 0;
}
/**
* Removes the given habit from the list.
* <p>
* If the given habit is not in the list, does nothing.
*
* @param h the habit to be removed.
*/
public abstract void remove(@NonNull Habit h);
/**
* Removes all the habits from the list.
*/
public void removeAll()
{
List<Habit> copy = new LinkedList<>();
for (Habit h : this) copy.add(h);
for (Habit h : copy) remove(h);
}
/**
* Changes the position of a habit in the list.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
public abstract void reorder(Habit from, Habit to);
public void repair()
{
for (Habit h : this)
{
h.getCheckmarks().invalidateNewerThan(0);
h.getStreaks().invalidateNewerThan(0);
h.getScores().invalidateNewerThan(0);
}
}
/**
* Returns the number of habits in this list.
*
* @return number of habits
*/
public abstract int size();
/**
* Notifies the list that a certain list of habits has been modified.
* <p>
* Depending on the implementation, this operation might trigger a write to
* disk, or do nothing at all. To make sure that the habits get persisted,
* this operation must be called.
*
* @param habits the list of habits that have been modified.
*/
public abstract void update(List<Habit> habits);
/**
* Notifies the list that a certain habit has been modified.
* <p>
* See {@link #update(List)} for more details.
*
* @param habit the habit that has been modified.
*/
public void update(@NonNull Habit habit)
{
update(Collections.singletonList(habit));
}
/**
* Writes the list of habits to the given writer, in CSV format. There is
* one line for each habit, containing the fields name, description,
* frequency numerator, frequency denominator and color. The color is
* written in HTML format (#000000).
*
* @param out the writer that will receive the result
* @throws IOException if write operations fail
*/
public void writeCSV(@NonNull Writer out) throws IOException
{
String header[] = {
"Position",
"Name",
"Description",
"NumRepetitions",
"Interval",
"Color"
};
CSVWriter csv = new CSVWriter(out);
csv.writeNext(header, false);
for (Habit habit : this)
{
Frequency freq = habit.getFrequency();
String[] cols = {
String.format("%03d", indexOf(habit) + 1),
habit.getName(),
habit.getDescription(),
Integer.toString(freq.getNumerator()),
Integer.toString(freq.getDenominator()),
ColorConstants.CSV_PALETTE[habit.getColor()]
};
csv.writeNext(cols, false);
}
csv.close();
}
public enum Order
{
BY_NAME,
BY_COLOR,
BY_SCORE,
BY_POSITION
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.models;
import android.support.annotation.*;
import java.util.*;
public class HabitMatcher
{
public static final HabitMatcher WITH_ALARM = new HabitMatcherBuilder()
.setArchivedAllowed(true)
.setReminderRequired(true)
.build();
private final boolean archivedAllowed;
private final boolean reminderRequired;
private final boolean completedAllowed;
private final List<Integer> allowedColors;
public HabitMatcher(boolean allowArchived,
boolean reminderRequired,
boolean completedAllowed,
@NonNull List<Integer> allowedColors)
{
this.archivedAllowed = allowArchived;
this.reminderRequired = reminderRequired;
this.completedAllowed = completedAllowed;
this.allowedColors = allowedColors;
}
public List<Integer> getAllowedColors()
{
return allowedColors;
}
public boolean isArchivedAllowed()
{
return archivedAllowed;
}
public boolean isCompletedAllowed()
{
return completedAllowed;
}
public boolean isReminderRequired()
{
return reminderRequired;
}
public boolean matches(Habit habit)
{
if (!isArchivedAllowed() && habit.isArchived()) return false;
if (isReminderRequired() && !habit.hasReminder()) return false;
if (!isCompletedAllowed() && habit.isCompletedToday()) return false;
if (!allowedColors.contains(habit.getColor())) return false;
return true;
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.models;
import org.isoron.uhabits.utils.*;
import java.util.*;
public class HabitMatcherBuilder
{
private boolean archivedAllowed = false;
private boolean reminderRequired = false;
private boolean completedAllowed = true;
private List<Integer> allowedColors = allColors();
private static List<Integer> allColors()
{
List<Integer> colors = new ArrayList<>();
for(int i = 0; i < ColorConstants.CSV_PALETTE.length; i++)
colors.add(i);
return colors;
}
public HabitMatcher build()
{
return new HabitMatcher(archivedAllowed, reminderRequired,
completedAllowed, allowedColors);
}
public HabitMatcherBuilder setArchivedAllowed(boolean archivedAllowed)
{
this.archivedAllowed = archivedAllowed;
return this;
}
public HabitMatcherBuilder setAllowedColors(List<Integer> allowedColors)
{
this.allowedColors = allowedColors;
return this;
}
public HabitMatcherBuilder setCompletedAllowed(boolean completedAllowed)
{
this.completedAllowed = completedAllowed;
return this;
}
public HabitMatcherBuilder setReminderRequired(boolean reminderRequired)
{
this.reminderRequired = reminderRequired;
return this;
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.models;
public class HabitNotFoundException extends RuntimeException {
public HabitNotFoundException() {
super();
}
public HabitNotFoundException(String message) {
super(message);
}
public HabitNotFoundException(Throwable cause) {
super(cause);
}
public HabitNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.models;
/**
* Interface implemented by factories that provide concrete implementations of
* the core model classes.
*/
public interface ModelFactory
{
CheckmarkList buildCheckmarkList(Habit habit);
default Habit buildHabit()
{
return new Habit(this);
}
default Habit buildHabit(Habit.HabitData data)
{
return new Habit(this, data);
}
HabitList buildHabitList();
RepetitionList buildRepetitionList(Habit habit);
ScoreList buildScoreList(Habit habit);
StreakList buildStreakList(Habit habit);
}

View File

@@ -0,0 +1,210 @@
/*
* 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.models;
import android.support.annotation.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
/**
* The collection of {@link Repetition}s belonging to a habit.
*/
public abstract class RepetitionList
{
@NonNull
protected final Habit habit;
@NonNull
protected final ModelObservable observable;
public RepetitionList(@NonNull Habit habit)
{
this.habit = habit;
this.observable = new ModelObservable();
}
/**
* Adds a repetition to the list.
* <p>
* Any implementation of this method must call observable.notifyListeners()
* after the repetition has been added.
*
* @param repetition the repetition to be added.
*/
public abstract void add(Repetition repetition);
/**
* Returns true if the list contains a repetition that has the given
* timestamp.
*
* @param timestamp the timestamp to find.
* @return true if list contains repetition with given timestamp, false
* otherwise.
*/
public boolean containsTimestamp(long timestamp)
{
return (getByTimestamp(timestamp) != null);
}
/**
* Returns the list of repetitions that happened within the given time
* interval.
* <p>
* The list is sorted by timestamp in increasing order. That is, the first
* element corresponds to oldest timestamp, while the last element
* corresponds to the newest. The endpoints of the interval are included.
*
* @param fromTimestamp timestamp of the beginning of the interval
* @param toTimestamp timestamp of the end of the interval
* @return list of repetitions within given time interval
*/
// TODO: Change order timestamp desc
public abstract List<Repetition> getByInterval(long fromTimestamp,
long toTimestamp);
/**
* Returns the repetition that has the given timestamp, or null if none
* exists.
*
* @param timestamp the repetition timestamp.
* @return the repetition that has the given timestamp.
*/
@Nullable
public abstract Repetition getByTimestamp(long timestamp);
@NonNull
public ModelObservable getObservable()
{
return observable;
}
/**
* Returns the oldest repetition in the list.
* <p>
* If the list is empty, returns null. Repetitions in the future are
* discarded.
*
* @return oldest repetition in the list, or null if list is empty.
*/
@Nullable
public abstract Repetition getOldest();
@Nullable
/**
* Returns the newest repetition in the list.
* <p>
* If the list is empty, returns null. Repetitions in the past are
* discarded.
*
* @return newest repetition in the list, or null if list is empty.
*/
public abstract Repetition getNewest();
/**
* Returns the total number of repetitions for each month, from the first
* repetition until today, grouped by day of week.
* <p>
* The repetitions are returned in a HashMap. The key is the timestamp for
* the first day of the month, at midnight (00:00). The value is an integer
* array with 7 entries. The first entry contains the total number of
* repetitions during the specified month that occurred on a Saturday. The
* second entry corresponds to Sunday, and so on. If there are no
* repetitions during a certain month, the value is null.
*
* @return total number of repetitions by month versus day of week
*/
@NonNull
public HashMap<Long, Integer[]> getWeekdayFrequency()
{
List<Repetition> reps = getByInterval(0, DateUtils.getStartOfToday());
HashMap<Long, Integer[]> map = new HashMap<>();
for (Repetition r : reps)
{
Calendar date = DateUtils.getCalendar(r.getTimestamp());
int weekday = DateUtils.getWeekday(r.getTimestamp());
date.set(Calendar.DAY_OF_MONTH, 1);
long timestamp = date.getTimeInMillis();
Integer[] list = map.get(timestamp);
if (list == null)
{
list = new Integer[7];
Arrays.fill(list, 0);
map.put(timestamp, list);
}
list[weekday]++;
}
return map;
}
/**
* Removes a given repetition from the list.
* <p>
* If the list does not contain the repetition, it is unchanged.
* <p>
* Any implementation of this method must call observable.notifyListeners()
* after the repetition has been added.
*
* @param repetition the repetition to be removed
*/
public abstract void remove(@NonNull Repetition repetition);
/**
* Adds or remove a repetition at a certain timestamp.
* <p>
* If there exists a repetition on the list with the given timestamp, the
* method removes this repetition from the list and returns it. If there are
* no repetitions with the given timestamp, creates and adds one to the
* list, then returns it.
*
* @param timestamp the timestamp for the timestamp that should be added or
* removed.
* @return the repetition that has been added or removed.
*/
@NonNull
public Repetition toggleTimestamp(long timestamp)
{
timestamp = DateUtils.getStartOfDay(timestamp);
Repetition rep = getByTimestamp(timestamp);
if (rep != null) remove(rep);
else
{
rep = new Repetition(timestamp, Checkmark.CHECKED_EXPLICITLY);
add(rep);
}
habit.invalidateNewerThan(timestamp);
return rep;
}
/**
* Returns the number of all repetitions
*
* @return number of all repetitions
*/
@NonNull
public abstract long getTotalCount();
}

View File

@@ -0,0 +1,337 @@
/*
* 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.models;
import android.support.annotation.*;
import org.isoron.uhabits.utils.*;
import java.io.*;
import java.text.*;
import java.util.*;
public abstract class ScoreList implements Iterable<Score>
{
protected final Habit habit;
protected ModelObservable observable;
protected Double todayValue = null;
/**
* Creates a new ScoreList for the given habit.
* <p>
* The list is populated automatically according to the repetitions that the
* habit has.
*
* @param habit the habit to which the scores belong.
*/
public ScoreList(Habit habit)
{
this.habit = habit;
observable = new ModelObservable();
}
/**
* Adds the given scores to the list.
* <p>
* This method should not be called by the application, since the scores are
* computed automatically from the list of repetitions.
*
* @param scores the scores to add.
*/
public abstract void add(List<Score> scores);
public ModelObservable getObservable()
{
return observable;
}
/**
* Returns the value of the score for today.
*
* @return value of today's score
*/
public double getTodayValue()
{
if(todayValue == null) todayValue = getValue(DateUtils.getStartOfToday());
return todayValue;
}
/**
* Returns the value of the score for a given day.
* <p>
* If the timestamp given happens before the first repetition of the habit
* then returns zero.
*
* @param timestamp the timestamp of a day
* @return score value for that day
*/
public final double getValue(long timestamp)
{
compute(timestamp, timestamp);
Score s = getComputedByTimestamp(timestamp);
if(s == null) throw new IllegalStateException();
return s.getValue();
}
/**
* Returns the list of scores that fall within the given interval.
* <p>
* There is exactly one score per day in the interval. The endpoints of
* the interval are included. The list is ordered by timestamp (decreasing).
* That is, the first score corresponds to the newest timestamp, and the
* last score corresponds to the oldest timestamp.
*
* @param fromTimestamp timestamp of the beginning of the interval.
* @param toTimestamp timestamp of the end of the interval.
* @return the list of scores within the interval.
*/
@NonNull
public abstract List<Score> getByInterval(long fromTimestamp,
long toTimestamp);
/**
* Returns the values of the scores that fall inside a certain interval
* of time.
* <p>
* The values are returned in an array containing one integer value for each
* day of the interval. The first entry corresponds to the most recent day
* in the interval. Each subsequent entry corresponds to one day older than
* the previous entry. The boundaries of the time interval are included.
*
* @param from timestamp for the oldest score
* @param to timestamp for the newest score
* @return values for the scores inside the given interval
*/
public final double[] getValues(long from, long to)
{
List<Score> scores = getByInterval(from, to);
double[] values = new double[scores.size()];
for(int i = 0; i < values.length; i++)
values[i] = scores.get(i).getValue();
return values;
}
public List<Score> groupBy(DateUtils.TruncateField field)
{
computeAll();
HashMap<Long, ArrayList<Double>> groups = getGroupedValues(field);
List<Score> scores = groupsToAvgScores(groups);
Collections.sort(scores, (s1, s2) -> s2.compareNewer(s1));
return scores;
}
/**
* Marks all scores that have timestamp equal to or newer than the given
* timestamp as invalid. Any following getValue calls will trigger the
* scores to be recomputed.
*
* @param timestamp the oldest timestamp that should be invalidated
*/
public abstract void invalidateNewerThan(long timestamp);
@Override
public Iterator<Score> iterator()
{
return toList().iterator();
}
/**
* Returns a Java list of scores, containing one score for each day, from
* the first repetition of the habit until today.
* <p>
* The scores are sorted by decreasing timestamp. The first score
* corresponds to today.
*
* @return list of scores
*/
public abstract List<Score> toList();
public void writeCSV(Writer out) throws IOException
{
computeAll();
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
for (Score s : this)
{
String timestamp = dateFormat.format(s.getTimestamp());
String score =
String.format("%.4f", s.getValue());
out.write(String.format("%s,%s\n", timestamp, score));
}
}
/**
* Computes and stores one score for each day inside the given interval.
* <p>
* Scores that have already been computed are skipped, therefore there is no
* harm in calling this function more times, or with larger intervals, than
* strictly needed. The endpoints of the interval are included.
* <p>
* This method assumes the list of computed scores has no holes. That is, if
* there is a score computed at time t1 and another at time t2, then every
* score between t1 and t2 is also computed.
*
* @param from timestamp of the beginning of the interval
* @param to timestamp of the end of the time interval
*/
protected synchronized void compute(long from, long to)
{
final long day = DateUtils.millisecondsInOneDay;
Score newest = getNewestComputed();
Score oldest = getOldestComputed();
if (newest == null)
{
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep != null)
from = Math.min(from, oldestRep.getTimestamp());
forceRecompute(from, to, 0);
}
else
{
if (oldest == null) throw new IllegalStateException();
forceRecompute(from, oldest.getTimestamp() - day, 0);
forceRecompute(newest.getTimestamp() + day, to,
newest.getValue());
}
}
/**
* Computes and saves the scores that are missing since the first repetition
* of the habit.
*/
protected void computeAll()
{
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return;
long today = DateUtils.getStartOfToday();
compute(oldestRep.getTimestamp(), today);
}
/**
* Returns the score that has the given timestamp, if it has already been
* computed. If that score has not been computed yet, returns null.
*
* @param timestamp the timestamp of the score
* @return the score with given timestamp, or null not yet computed.
*/
@Nullable
protected abstract Score getComputedByTimestamp(long timestamp);
/**
* Returns the most recent score that has already been computed. If no score
* has been computed yet, returns null.
*/
@Nullable
protected abstract Score getNewestComputed();
/**
* Returns oldest score already computed. If no score has been computed yet,
* returns null.
*/
@Nullable
protected abstract Score getOldestComputed();
/**
* Computes and stores one score for each day inside the given interval.
* <p>
* This function does not check if the scores have already been computed. If
* they have, then it stores duplicate scores, which is a bad thing.
*
* @param from timestamp of the beginning of the interval
* @param to timestamp of the end of the interval
* @param previousValue value of the score on the day immediately before the
* interval begins
*/
private void forceRecompute(long from, long to, double previousValue)
{
if(from > to) return;
final long day = DateUtils.millisecondsInOneDay;
final double freq = habit.getFrequency().toDouble();
final int checkmarkValues[] = habit.getCheckmarks().getValues(from, to);
List<Score> scores = new LinkedList<>();
for (int i = 0; i < checkmarkValues.length; i++)
{
double value = checkmarkValues[checkmarkValues.length - i - 1];
if(habit.isNumerical())
{
value /= 1000;
value /= habit.getTargetValue();
value = Math.min(1, value);
}
if(!habit.isNumerical() && value > 0)
value = 1;
previousValue = Score.compute(freq, previousValue, value);
scores.add(new Score(from + day * i, previousValue));
}
add(scores);
}
@NonNull
private HashMap<Long, ArrayList<Double>> getGroupedValues(DateUtils.TruncateField field)
{
HashMap<Long, ArrayList<Double>> groups = new HashMap<>();
for (Score s : this)
{
long groupTimestamp = DateUtils.truncate(field, s.getTimestamp());
if (!groups.containsKey(groupTimestamp))
groups.put(groupTimestamp, new ArrayList<>());
groups.get(groupTimestamp).add(s.getValue());
}
return groups;
}
@NonNull
private List<Score> groupsToAvgScores(HashMap<Long, ArrayList<Double>> groups)
{
List<Score> scores = new LinkedList<>();
for (Long timestamp : groups.keySet())
{
double meanValue = 0.0;
ArrayList<Double> groupValues = groups.get(timestamp);
for (Double v : groupValues) meanValue += v;
meanValue /= groupValues.size();
scores.add(new Score(timestamp, meanValue));
}
return scores;
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.models;
import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.utils.*;
public final class Streak
{
private final long start;
private final long end;
public Streak(long start, long end)
{
this.start = start;
this.end = end;
}
public int compareLonger(Streak other)
{
if (this.getLength() != other.getLength())
return Long.signum(this.getLength() - other.getLength());
return Long.signum(this.getEnd() - other.getEnd());
}
public int compareNewer(Streak other)
{
return Long.signum(this.getEnd() - other.getEnd());
}
public long getEnd()
{
return end;
}
public long getLength()
{
return (end - start) / DateUtils.millisecondsInOneDay + 1;
}
public long getStart()
{
return start;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("start", start)
.append("end", end)
.toString();
}
}

View File

@@ -0,0 +1,157 @@
/*
* 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.models;
import android.support.annotation.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
/**
* The collection of {@link Streak}s that belong to a habit.
* <p>
* This list is populated automatically from the list of repetitions.
*/
public abstract class StreakList
{
protected final Habit habit;
protected ModelObservable observable;
protected StreakList(Habit habit)
{
this.habit = habit;
observable = new ModelObservable();
}
public abstract List<Streak> getAll();
@NonNull
public List<Streak> getBest(int limit)
{
List<Streak> streaks = getAll();
Collections.sort(streaks, (s1, s2) -> s2.compareLonger(s1));
streaks = streaks.subList(0, Math.min(streaks.size(), limit));
Collections.sort(streaks, (s1, s2) -> s2.compareNewer(s1));
return streaks;
}
@Nullable
public abstract Streak getNewestComputed();
@NonNull
public ModelObservable getObservable()
{
return observable;
}
public abstract void invalidateNewerThan(long timestamp);
public synchronized void rebuild()
{
long today = DateUtils.getStartOfToday();
Long beginning = findBeginning();
if (beginning == null || beginning > today) return;
int checks[] = habit.getCheckmarks().getValues(beginning, today);
List<Streak> streaks = checkmarksToStreaks(beginning, checks);
removeNewestComputed();
add(streaks);
}
/**
* Converts a list of checkmark values to a list of streaks.
*
* @param beginning the timestamp corresponding to the first checkmark
* value.
* @param checks the checkmarks values, ordered by decreasing timestamp.
* @return the list of streaks.
*/
@NonNull
protected List<Streak> checkmarksToStreaks(long beginning, int[] checks)
{
ArrayList<Long> transitions = getTransitions(beginning, checks);
List<Streak> streaks = new LinkedList<>();
for (int i = 0; i < transitions.size(); i += 2)
{
long start = transitions.get(i);
long end = transitions.get(i + 1);
streaks.add(new Streak(start, end));
}
return streaks;
}
/**
* Finds the place where we should start when recomputing the streaks.
*
* @return
*/
@Nullable
protected Long findBeginning()
{
Streak newestStreak = getNewestComputed();
if (newestStreak != null) return newestStreak.getStart();
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep != null) return oldestRep.getTimestamp();
return null;
}
/**
* Returns the timestamps where there was a transition from performing a
* habit to not performing a habit, and vice-versa.
*
* @param beginning the timestamp for the first checkmark
* @param checks the checkmarks, ordered by decresing timestamp
* @return the list of transitions
*/
@NonNull
protected ArrayList<Long> getTransitions(long beginning, int[] checks)
{
long day = DateUtils.millisecondsInOneDay;
long current = beginning;
ArrayList<Long> list = new ArrayList<>();
list.add(current);
for (int i = 1; i < checks.length; i++)
{
current += day;
int j = checks.length - i - 1;
if ((checks[j + 1] == 0 && checks[j] > 0)) list.add(current);
if ((checks[j + 1] > 0 && checks[j] == 0)) list.add(current - day);
}
if (list.size() % 2 == 1) list.add(current);
return list;
}
protected abstract void add(@NonNull List<Streak> streaks);
protected abstract void removeNewestComputed();
}

View File

@@ -0,0 +1,88 @@
/*
* 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.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.models.*;
import java.util.*;
/**
* In-memory implementation of {@link CheckmarkList}.
*/
public class MemoryCheckmarkList extends CheckmarkList
{
LinkedList<Checkmark> list;
public MemoryCheckmarkList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public void add(List<Checkmark> checkmarks)
{
list.addAll(checkmarks);
Collections.sort(list, (c1, c2) -> c2.compareNewer(c1));
}
@NonNull
@Override
public List<Checkmark> getByInterval(long fromTimestamp, long toTimestamp)
{
compute(fromTimestamp, toTimestamp);
List<Checkmark> filtered = new LinkedList<>();
for (Checkmark c : list)
if (c.getTimestamp() >= fromTimestamp &&
c.getTimestamp() <= toTimestamp) filtered.add(c);
return filtered;
}
@Override
public void invalidateNewerThan(long timestamp)
{
LinkedList<Checkmark> invalid = new LinkedList<>();
for (Checkmark c : list)
if (c.getTimestamp() >= timestamp) invalid.add(c);
list.removeAll(invalid);
}
@Override
protected Checkmark getOldestComputed()
{
if(list.isEmpty()) return null;
return list.getLast();
}
@Override
protected Checkmark getNewestComputed()
{
if(list.isEmpty()) return null;
return list.getFirst();
}
}

View File

@@ -0,0 +1,181 @@
/*
* 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.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.models.*;
import java.util.*;
import static org.isoron.uhabits.models.HabitList.Order.*;
/**
* In-memory implementation of {@link HabitList}.
*/
public class MemoryHabitList extends HabitList
{
@NonNull
private LinkedList<Habit> list;
private Comparator<Habit> comparator = null;
@NonNull
private Order order;
public MemoryHabitList()
{
super();
list = new LinkedList<>();
order = Order.BY_POSITION;
}
protected MemoryHabitList(@NonNull HabitMatcher matcher)
{
super(matcher);
list = new LinkedList<>();
order = Order.BY_POSITION;
}
@Override
public void add(@NonNull Habit habit) throws IllegalArgumentException
{
if (list.contains(habit))
throw new IllegalArgumentException("habit already added");
Long id = habit.getId();
if (id != null && getById(id) != null)
throw new RuntimeException("duplicate id");
if (id == null) habit.setId((long) list.size());
list.addLast(habit);
resort();
}
@Override
public Habit getById(long id)
{
for (Habit h : list)
{
if (h.getId() == null) continue;
if (h.getId() == id) return h;
}
return null;
}
@NonNull
@Override
public Habit getByPosition(int position)
{
return list.get(position);
}
@NonNull
@Override
public HabitList getFiltered(HabitMatcher matcher)
{
MemoryHabitList habits = new MemoryHabitList(matcher);
habits.comparator = comparator;
for (Habit h : this) if (matcher.matches(h)) habits.add(h);
return habits;
}
@Override
public Order getOrder()
{
return order;
}
@Override
public int indexOf(@NonNull Habit h)
{
return list.indexOf(h);
}
@Override
public Iterator<Habit> iterator()
{
return Collections.unmodifiableCollection(list).iterator();
}
@Override
public void remove(@NonNull Habit habit)
{
list.remove(habit);
}
@Override
public void reorder(Habit from, Habit to)
{
int toPos = indexOf(to);
list.remove(from);
list.add(toPos, from);
}
@Override
public void setOrder(@NonNull Order order)
{
this.order = order;
this.comparator = getComparatorByOrder(order);
resort();
}
@Override
public int size()
{
return list.size();
}
@Override
public void update(List<Habit> habits)
{
// NOP
}
private Comparator<Habit> getComparatorByOrder(Order order)
{
Comparator<Habit> nameComparator =
(h1, h2) -> h1.getName().compareTo(h2.getName());
Comparator<Habit> colorComparator = (h1, h2) -> {
Integer c1 = h1.getColor();
Integer c2 = h2.getColor();
if (c1.equals(c2)) return nameComparator.compare(h1, h2);
else return c1.compareTo(c2);
};
Comparator<Habit> scoreComparator = (h1, h2) -> {
double s1 = h1.getScores().getTodayValue();
double s2 = h2.getScores().getTodayValue();
return Double.compare(s2, s1);
};
if (order == BY_POSITION) return null;
if (order == BY_NAME) return nameComparator;
if (order == BY_COLOR) return colorComparator;
if (order == BY_SCORE) return scoreComparator;
throw new IllegalStateException();
}
private void resort()
{
if (comparator != null) Collections.sort(list, comparator);
}
}

View File

@@ -0,0 +1,73 @@
/*
* 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.models.memory;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import dagger.*;
@Module
public class MemoryModelFactory implements ModelFactory
{
@Provides
@AppScope
public static HabitList provideHabitList()
{
return new MemoryHabitList();
}
@Provides
@AppScope
public static ModelFactory provideModelFactory()
{
return new MemoryModelFactory();
}
@Override
public CheckmarkList buildCheckmarkList(Habit habit)
{
return new MemoryCheckmarkList(habit);
}
@Override
public HabitList buildHabitList()
{
return new MemoryHabitList();
}
@Override
public RepetitionList buildRepetitionList(Habit habit)
{
return new MemoryRepetitionList(habit);
}
@Override
public ScoreList buildScoreList(Habit habit)
{
return new MemoryScoreList(habit);
}
@Override
public StreakList buildStreakList(Habit habit)
{
return new MemoryStreakList(habit);
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.models.*;
import java.util.*;
/**
* In-memory implementation of {@link RepetitionList}.
*/
public class MemoryRepetitionList extends RepetitionList
{
LinkedList<Repetition> list;
public MemoryRepetitionList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public void add(Repetition repetition)
{
list.add(repetition);
observable.notifyListeners();
}
@Override
public List<Repetition> getByInterval(long fromTimestamp, long toTimestamp)
{
LinkedList<Repetition> filtered = new LinkedList<>();
for (Repetition r : list)
{
long t = r.getTimestamp();
if (t >= fromTimestamp && t <= toTimestamp) filtered.add(r);
}
Collections.sort(filtered,
(r1, r2) -> (int) (r1.getTimestamp() - r2.getTimestamp()));
return filtered;
}
@Nullable
@Override
public Repetition getByTimestamp(long timestamp)
{
for (Repetition r : list)
if (r.getTimestamp() == timestamp) return r;
return null;
}
@Nullable
@Override
public Repetition getOldest()
{
long oldestTime = Long.MAX_VALUE;
Repetition oldestRep = null;
for (Repetition rep : list)
{
if (rep.getTimestamp() < oldestTime)
{
oldestRep = rep;
oldestTime = rep.getTimestamp();
}
}
return oldestRep;
}
@Nullable
@Override
public Repetition getNewest()
{
long newestTime = -1;
Repetition newestRep = null;
for (Repetition rep : list)
{
if (rep.getTimestamp() > newestTime)
{
newestRep = rep;
newestTime = rep.getTimestamp();
}
}
return newestRep;
}
@Override
public void remove(@NonNull Repetition repetition)
{
list.remove(repetition);
observable.notifyListeners();
}
@Override
public long getTotalCount()
{
return list.size();
}
}

View File

@@ -0,0 +1,108 @@
/*
* 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.models.memory;
import android.support.annotation.*;
import org.isoron.uhabits.models.*;
import java.util.*;
public class MemoryScoreList extends ScoreList
{
LinkedList<Score> list;
public MemoryScoreList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public void add(List<Score> scores)
{
list.addAll(scores);
Collections.sort(list,
(s1, s2) -> Long.signum(s2.getTimestamp() - s1.getTimestamp()));
}
@NonNull
@Override
public List<Score> getByInterval(long fromTimestamp, long toTimestamp)
{
compute(fromTimestamp, toTimestamp);
List<Score> filtered = new LinkedList<>();
for (Score s : list)
if (s.getTimestamp() >= fromTimestamp &&
s.getTimestamp() <= toTimestamp) filtered.add(s);
return filtered;
}
@Nullable
@Override
public Score getComputedByTimestamp(long timestamp)
{
for (Score s : list)
if (s.getTimestamp() == timestamp) return s;
return null;
}
@Override
public void invalidateNewerThan(long timestamp)
{
List<Score> discard = new LinkedList<>();
for (Score s : list)
if (s.getTimestamp() >= timestamp) discard.add(s);
list.removeAll(discard);
todayValue = null;
getObservable().notifyListeners();
}
@Override
@NonNull
public List<Score> toList()
{
computeAll();
return new LinkedList<>(list);
}
@Nullable
@Override
protected Score getNewestComputed()
{
if (list.isEmpty()) return null;
return list.getFirst();
}
@Nullable
@Override
protected Score getOldestComputed()
{
if (list.isEmpty()) return null;
return list.getLast();
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.models.memory;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
public class MemoryStreakList extends StreakList
{
LinkedList<Streak> list;
public MemoryStreakList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public Streak getNewestComputed()
{
Streak newest = null;
for (Streak s : list)
if (newest == null || s.getEnd() > newest.getEnd()) newest = s;
return newest;
}
@Override
public void invalidateNewerThan(long timestamp)
{
LinkedList<Streak> discard = new LinkedList<>();
for (Streak s : list)
if (s.getEnd() >= timestamp - DateUtils.millisecondsInOneDay)
discard.add(s);
list.removeAll(discard);
observable.notifyListeners();
}
@Override
protected void add(List<Streak> streaks)
{
list.addAll(streaks);
Collections.sort(list, (s1, s2) -> s2.compareNewer(s1));
}
@Override
protected void removeNewestComputed()
{
Streak newest = getNewestComputed();
if (newest != null) list.remove(newest);
}
@Override
public List<Streak> getAll()
{
rebuild();
return new LinkedList<>(list);
}
}

View File

@@ -0,0 +1,23 @@
/*
* 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/>.
*/
/**
* Provides in-memory implementation of core models.
*/
package org.isoron.uhabits.models.memory;

View File

@@ -0,0 +1,24 @@
/*
* 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 Licenses along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides core models classes, such as {@link org.isoron.uhabits.models.Habit}
* and {@link org.isoron.uhabits.models.Repetition}.
*/
package org.isoron.uhabits.models;

View File

@@ -0,0 +1,20 @@
package org.isoron.uhabits.utils;
public class ColorConstants
{
public static String[] CSV_PALETTE = {
"#D32F2F", // 0 red
"#E64A19", // 1 orange
"#F9A825", // 2 yellow
"#AFB42B", // 3 light green
"#388E3C", // 4 dark green
"#00897B", // 5 teal
"#00ACC1", // 6 cyan
"#039BE5", // 7 blue
"#5E35B1", // 8 deep purple
"#8E24AA", // 9 purple
"#D81B60", // 10 pink
"#303030", // 11 dark grey
"#aaaaaa" // 12 light grey
};
}

View File

@@ -0,0 +1,47 @@
/*
* 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.utils;
import android.support.annotation.*;
import java.text.*;
import java.util.*;
public class DateFormats
{
@NonNull
public static SimpleDateFormat fromSkeleton(@NonNull String skeleton,
@NonNull Locale locale)
{
SimpleDateFormat df = new SimpleDateFormat(skeleton, locale);
df.setTimeZone(TimeZone.getTimeZone("UTC"));
return df;
}
public static SimpleDateFormat getBackupDateFormat()
{
return fromSkeleton("yyyy-MM-dd HHmmss", Locale.US);
}
public static SimpleDateFormat getCSVDateFormat()
{
return fromSkeleton("yyyy-MM-dd", Locale.US);
}
}

View File

@@ -0,0 +1,247 @@
/*
* 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.utils;
import java.util.*;
import static java.util.Calendar.*;
public abstract class DateUtils
{
private static Long fixedLocalTime = null;
private static TimeZone fixedTimeZone = null;
/**
* Number of milliseconds in one day.
*/
public static long millisecondsInOneDay = 24 * 60 * 60 * 1000;
public static long applyTimezone(long localTimestamp)
{
TimeZone tz = getTimezone();
long now = new Date(localTimestamp).getTime();
return now - tz.getOffset(now);
}
public static String formatHeaderDate(GregorianCalendar day)
{
Locale locale = Locale.getDefault();
String dayOfMonth = Integer.toString(day.get(DAY_OF_MONTH));
String dayOfWeek = day.getDisplayName(DAY_OF_WEEK, SHORT, locale);
return dayOfWeek + "\n" + dayOfMonth;
}
public static GregorianCalendar getCalendar(long timestamp)
{
GregorianCalendar day =
new GregorianCalendar(TimeZone.getTimeZone("GMT"));
day.setTimeInMillis(timestamp);
return day;
}
public static String[] getDayNames(int format)
{
String[] wdays = new String[7];
Calendar day = new GregorianCalendar();
day.set(DAY_OF_WEEK, Calendar.SATURDAY);
for (int i = 0; i < wdays.length; i++)
{
wdays[i] =
day.getDisplayName(DAY_OF_WEEK, format, Locale.getDefault());
day.add(DAY_OF_MONTH, 1);
}
return wdays;
}
public static long getLocalTime()
{
if (fixedLocalTime != null) return fixedLocalTime;
TimeZone tz = getTimezone();
long now = new Date().getTime();
return now + tz.getOffset(now);
}
/**
* @return array with weekday names starting according to locale settings,
* e.g. [Mo,Di,Mi,Do,Fr,Sa,So] in Europe
*/
public static String[] getLocaleDayNames(int format)
{
String[] days = new String[7];
Calendar calendar = new GregorianCalendar();
calendar.set(DAY_OF_WEEK, calendar.getFirstDayOfWeek());
for (int i = 0; i < days.length; i++)
{
days[i] = calendar.getDisplayName(DAY_OF_WEEK, format,
Locale.getDefault());
calendar.add(DAY_OF_MONTH, 1);
}
return days;
}
/**
* @return array with week days numbers starting according to locale
* settings, e.g. [2,3,4,5,6,7,1] in Europe
*/
public static Integer[] getLocaleWeekdayList()
{
Integer[] dayNumbers = new Integer[7];
Calendar calendar = new GregorianCalendar();
calendar.set(DAY_OF_WEEK, calendar.getFirstDayOfWeek());
for (int i = 0; i < dayNumbers.length; i++)
{
dayNumbers[i] = calendar.get(DAY_OF_WEEK);
calendar.add(DAY_OF_MONTH, 1);
}
return dayNumbers;
}
public static String[] getLongDayNames()
{
return getDayNames(GregorianCalendar.LONG);
}
public static String[] getShortDayNames()
{
return getDayNames(SHORT);
}
public static long getStartOfDay(long timestamp)
{
return (timestamp / millisecondsInOneDay) * millisecondsInOneDay;
}
public static long getStartOfToday()
{
return getStartOfDay(DateUtils.getLocalTime());
}
public static long millisecondsUntilTomorrow()
{
return getStartOfToday() + millisecondsInOneDay - getLocalTime();
}
public static GregorianCalendar getStartOfTodayCalendar()
{
return getCalendar(getStartOfToday());
}
public static TimeZone getTimezone()
{
if(fixedTimeZone != null) return fixedTimeZone;
return TimeZone.getDefault();
}
public static void setFixedTimeZone(TimeZone tz)
{
fixedTimeZone = tz;
}
public static int getWeekday(long timestamp)
{
GregorianCalendar day = getCalendar(timestamp);
return javaWeekdayToLoopWeekday(day.get(DAY_OF_WEEK));
}
/**
* Throughout the code, it is assumed that the weekdays are numbered from 0
* (Saturday) to 6 (Friday). In the Java Calendar they are numbered from 1
* (Sunday) to 7 (Saturday). This function converts from Java to our
* internal representation.
*
* @return weekday number in the internal interpretation
*/
public static int javaWeekdayToLoopWeekday(int number)
{
return number % 7;
}
public static long removeTimezone(long timestamp)
{
TimeZone tz = getTimezone();
long now = new Date(timestamp).getTime();
return now + tz.getOffset(now);
}
public static void setFixedLocalTime(Long timestamp)
{
fixedLocalTime = timestamp;
}
public static Long truncate(TruncateField field, long timestamp)
{
GregorianCalendar cal = DateUtils.getCalendar(timestamp);
switch (field)
{
case MONTH:
cal.set(DAY_OF_MONTH, 1);
return cal.getTimeInMillis();
case WEEK_NUMBER:
int firstWeekday = cal.getFirstDayOfWeek();
int weekday = cal.get(DAY_OF_WEEK);
int delta = weekday - firstWeekday;
if (delta < 0) delta += 7;
cal.add(Calendar.DAY_OF_YEAR, -delta);
return cal.getTimeInMillis();
case QUARTER:
int quarter = cal.get(Calendar.MONTH) / 3;
cal.set(DAY_OF_MONTH, 1);
cal.set(Calendar.MONTH, quarter * 3);
return cal.getTimeInMillis();
case YEAR:
cal.set(Calendar.MONTH, Calendar.JANUARY);
cal.set(DAY_OF_MONTH, 1);
return cal.getTimeInMillis();
default:
throw new IllegalArgumentException();
}
}
public enum TruncateField
{
MONTH, WEEK_NUMBER, YEAR, QUARTER
}
/**
* Gets the number of days between two timestamps (exclusively).
*
* @param t1 the first timestamp to use in milliseconds
* @param t2 the second timestamp to use in milliseconds
* @return the number of days between the two timestamps
*/
public static int getDaysBetween(long t1, long t2)
{
Date d1 = new Date(t1);
Date d2 = new Date(t2);
return (int) (Math.abs((d2.getTime() - d1.getTime()) / millisecondsInOneDay));
}
}