diff --git a/app/build.gradle b/app/build.gradle index 75e13f84f..588869201 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -78,6 +78,7 @@ dependencies { compile 'org.apmem.tools:layouts:1.10@aar' compile 'org.jetbrains:annotations-java5:15.0' compile 'com.google.code.gson:gson:2.8.0' + compile 'com.google.code.findbugs:jsr305:2.0.1' provided 'javax.annotation:jsr250-api:1.0' diff --git a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java index c434a4a87..85084c743 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java +++ b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java @@ -21,6 +21,8 @@ package org.isoron.uhabits.models; import org.apache.commons.lang3.builder.*; +import javax.annotation.concurrent.*; + /** * A Checkmark represents the completion status of the habit for a given day. *

@@ -30,6 +32,7 @@ import org.apache.commons.lang3.builder.*; *

* Checkmarks are computed automatically from the list of repetitions. */ +@ThreadSafe public final class Checkmark { /** diff --git a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java index 5e71eb9e5..da9725b92 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -29,19 +29,21 @@ import java.util.*; import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY; import static org.isoron.uhabits.models.Checkmark.CHECKED_IMPLICITLY; - +import javax.annotation.concurrent.*; /** * The collection of {@link Checkmark}s belonging to a habit. */ +@ThreadSafe public abstract class CheckmarkList { - protected Habit habit; + protected final Habit habit; - public ModelObservable observable = new ModelObservable(); + public final ModelObservable observable; public CheckmarkList(Habit habit) { this.habit = habit; + this.observable = new ModelObservable(); } /** @@ -67,7 +69,7 @@ public abstract class CheckmarkList * @return values for the checkmarks in the interval */ @NonNull - public final int[] getAllValues() + public synchronized final int[] getAllValues() { Repetition oldestRep = habit.getRepetitions().getOldest(); if (oldestRep == null) return new int[0]; @@ -100,7 +102,7 @@ public abstract class CheckmarkList * @return checkmark for today */ @Nullable - public final Checkmark getToday() + public synchronized final Checkmark getToday() { computeAll(); return getNewestComputed(); @@ -111,7 +113,7 @@ public abstract class CheckmarkList * * @return value of today's checkmark */ - public final int getTodayValue() + public synchronized final int getTodayValue() { Checkmark today = getToday(); if (today != null) return today.getValue(); @@ -162,9 +164,14 @@ public abstract class CheckmarkList */ public final void writeCSV(Writer out) throws IOException { - computeAll(); + int values[]; + + synchronized (this) + { + computeAll(); + values = getAllValues(); + } - int values[] = getAllValues(); long timestamp = DateUtils.getStartOfToday(); SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); @@ -274,7 +281,7 @@ public abstract class CheckmarkList * repetition of the habit until today. Days that already have a * corresponding checkmark are skipped. */ - protected final void computeAll() + private synchronized void computeAll() { Repetition oldest = habit.getRepetitions().getOldest(); if (oldest == null) return; diff --git a/app/src/main/java/org/isoron/uhabits/models/Frequency.java b/app/src/main/java/org/isoron/uhabits/models/Frequency.java index 5b893b5a1..b21349b18 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Frequency.java +++ b/app/src/main/java/org/isoron/uhabits/models/Frequency.java @@ -21,9 +21,12 @@ package org.isoron.uhabits.models; import org.apache.commons.lang3.builder.*; +import javax.annotation.concurrent.*; + /** * Represents how often is the habit repeated. */ +@ThreadSafe public class Frequency { public static final Frequency DAILY = new Frequency(1, 1); diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index bac4902c7..90b760609 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -26,6 +26,7 @@ import org.apache.commons.lang3.builder.*; import java.util.*; +import javax.annotation.concurrent.*; import javax.inject.*; import static org.isoron.uhabits.models.Checkmark.*; @@ -33,6 +34,7 @@ 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; @@ -89,12 +91,13 @@ public class Habit streaks = factory.buildStreakList(this); scores = factory.buildScoreList(this); repetitions = factory.buildRepetitionList(this); + observable = new ModelObservable(); } /** * Clears the reminder for a habit. */ - public void clearReminder() + public synchronized void clearReminder() { data.reminder = null; observable.notifyListeners(); @@ -105,7 +108,7 @@ public class Habit * * @param model the model whose attributes should be copied from */ - public void copyFrom(@NonNull Habit model) + public synchronized void copyFrom(@NonNull Habit model) { this.data = new HabitData(model.data); observable.notifyListeners(); @@ -115,7 +118,7 @@ public class Habit * List of checkmarks belonging to this habit. */ @NonNull - public CheckmarkList getCheckmarks() + public synchronized CheckmarkList getCheckmarks() { return checkmarks; } @@ -129,56 +132,56 @@ public class Habit * habit.color). */ @NonNull - public Integer getColor() + public synchronized Integer getColor() { return data.color; } - public void setColor(@NonNull Integer color) + public synchronized void setColor(@NonNull Integer color) { data.color = color; } @NonNull - public String getDescription() + public synchronized String getDescription() { return data.description; } - public void setDescription(@NonNull String description) + public synchronized void setDescription(@NonNull String description) { data.description = description; } @NonNull - public Frequency getFrequency() + public synchronized Frequency getFrequency() { return data.frequency; } - public void setFrequency(@NonNull Frequency frequency) + public synchronized void setFrequency(@NonNull Frequency frequency) { data.frequency = frequency; } @Nullable - public Long getId() + public synchronized Long getId() { return id; } - public void setId(@Nullable Long id) + public synchronized void setId(@Nullable Long id) { this.id = id; } @NonNull - public String getName() + public synchronized String getName() { return data.name; } - public void setName(@NonNull String name) + public synchronized void setName(@NonNull String name) { data.name = name; } @@ -199,13 +202,13 @@ public class Habit * @throws IllegalStateException if habit has no reminder */ @NonNull - public Reminder getReminder() + public synchronized Reminder getReminder() { if (data.reminder == null) throw new IllegalStateException(); return data.reminder; } - public void setReminder(@Nullable Reminder reminder) + public synchronized void setReminder(@Nullable Reminder reminder) { data.reminder = reminder; } @@ -228,35 +231,35 @@ public class Habit return streaks; } - public int getTargetType() + public synchronized int getTargetType() { return data.targetType; } - public void setTargetType(int targetType) + public synchronized void setTargetType(int targetType) { if (targetType != AT_LEAST && targetType != AT_MOST) throw new IllegalArgumentException(); data.targetType = targetType; } - public double getTargetValue() + public synchronized double getTargetValue() { return data.targetValue; } - public void setTargetValue(double targetValue) + public synchronized void setTargetValue(double targetValue) { if (targetValue < 0) throw new IllegalArgumentException(); data.targetValue = targetValue; } - public int getType() + public synchronized int getType() { return data.type; } - public void setType(int type) + public synchronized void setType(int type) { if (type != YES_NO_HABIT && type != NUMBER_HABIT) throw new IllegalArgumentException(); @@ -265,12 +268,12 @@ public class Habit } @NonNull - public String getUnit() + public synchronized String getUnit() { return data.unit; } - public void setUnit(@NonNull String unit) + public synchronized void setUnit(@NonNull String unit) { data.unit = unit; } @@ -286,7 +289,7 @@ public class Habit return Uri.parse(s); } - public boolean hasId() + public synchronized boolean hasId() { return getId() != null; } @@ -296,7 +299,7 @@ public class Habit * * @return true if habit has reminder, false otherwise */ - public boolean hasReminder() + public synchronized boolean hasReminder() { return data.reminder != null; } @@ -308,24 +311,24 @@ public class Habit getStreaks().invalidateNewerThan(timestamp); } - public boolean isArchived() + public synchronized boolean isArchived() { return data.archived; } - public void setArchived(boolean archived) + public synchronized void setArchived(boolean archived) { data.archived = archived; } - public boolean isCompletedToday() + public synchronized boolean isCompletedToday() { int todayCheckmark = getCheckmarks().getTodayValue(); if (isNumerical()) return todayCheckmark >= data.targetValue; else return (todayCheckmark != UNCHECKED); } - public boolean isNumerical() + public synchronized boolean isNumerical() { return data.type == NUMBER_HABIT; } diff --git a/app/src/main/java/org/isoron/uhabits/models/HabitList.java b/app/src/main/java/org/isoron/uhabits/models/HabitList.java index 444a5824c..2892a2c30 100644 --- a/app/src/main/java/org/isoron/uhabits/models/HabitList.java +++ b/app/src/main/java/org/isoron/uhabits/models/HabitList.java @@ -28,12 +28,15 @@ 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 { - private ModelObservable observable; + private final ModelObservable observable; @NonNull protected final HabitMatcher filter; diff --git a/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java b/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java index a762b5f8a..6ab80813b 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java +++ b/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java @@ -21,10 +21,13 @@ package org.isoron.uhabits.models; import java.util.*; +import javax.annotation.concurrent.*; + /** * A ModelObservable allows objects to subscribe themselves to it and receive * notifications whenever the model is changed. */ +@ThreadSafe public class ModelObservable { private List listeners; @@ -43,7 +46,7 @@ public class ModelObservable * * @param l the listener to be added. */ - public void addListener(Listener l) + public synchronized void addListener(Listener l) { listeners.add(l); } @@ -53,7 +56,7 @@ public class ModelObservable *

* Only models should call this method. */ - public void notifyListeners() + public synchronized void notifyListeners() { for (Listener l : listeners) l.onModelChange(); } @@ -66,7 +69,7 @@ public class ModelObservable * * @param l the listener to be removed */ - public void removeListener(Listener l) + public synchronized void removeListener(Listener l) { listeners.remove(l); }