feat: Decouple the checkmark state from the source of the input

pull/643/head
Christoph Hennemann 5 years ago
parent 87dda92d37
commit 5403ca3ef4

@ -56,7 +56,7 @@ public class HistoryChartTest extends BaseViewTest
today = new Timestamp(DateUtils.getStartOfToday()); today = new Timestamp(DateUtils.getStartOfToday());
chart = new HistoryChart(targetContext); chart = new HistoryChart(targetContext);
chart.setCheckmarks(habit.getCheckmarks().getAllValues()); chart.setCheckmarkStates(habit.getCheckmarks().getAllValues());
chart.setColor(PaletteUtils.getAndroidTestColor(habit.getColor())); chart.setColor(PaletteUtils.getAndroidTestColor(habit.getColor()));
measureView(chart, dpToPixels(400), dpToPixels(200)); measureView(chart, dpToPixels(400), dpToPixels(200));
@ -92,7 +92,7 @@ public class HistoryChartTest extends BaseViewTest
public void tapDate_withEmptyHabit() public void tapDate_withEmptyHabit()
{ {
chart.setIsEditable(true); chart.setIsEditable(true);
chart.setCheckmarks(new int[]{}); chart.setCheckmarkStates(new int[]{});
chart.tap(dpToPixels(340), dpToPixels(40)); chart.tap(dpToPixels(340), dpToPixels(40));
verify(controller).onToggleCheckmark(today, Checkmark.YES_MANUAL); verify(controller).onToggleCheckmark(today, Checkmark.YES_MANUAL);
verifyNoMoreInteractions(controller); verifyNoMoreInteractions(controller);

@ -164,12 +164,12 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
private class RefreshTask implements Task private class RefreshTask implements Task
{ {
public int[] checkmarks; public CheckmarkState[] checkmarks;
@Override @Override
public void doInBackground() public void doInBackground()
{ {
checkmarks = habit.getCheckmarks().getAllValues(); checkmarks = habit.getCheckmarks().getAllStates();
} }
@Override @Override
@ -180,7 +180,7 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
int color = PaletteUtils.getColor(getContext(), habit.getColor()); int color = PaletteUtils.getColor(getContext(), habit.getColor());
historyChart.setColor(color); historyChart.setColor(color);
historyChart.setCheckmarks(checkmarks); historyChart.setCheckmarkStates(checkmarks);
historyChart.setNumerical(habit.isNumerical()); historyChart.setNumerical(habit.isNumerical());
historyChart.setTarget(habit.getTargetValue() / habit.getFrequency().getDenominator()); historyChart.setTarget(habit.getTargetValue() / habit.getFrequency().getDenominator());
} }

@ -123,7 +123,7 @@ public class BarChart extends ScrollableChart
for (int i = 1; i < 100; i++) for (int i = 1; i < 100; i++)
{ {
int value = random.nextInt(1000); int value = random.nextInt(1000);
checkmarks.add(new Checkmark(today.minus(i), value)); checkmarks.add(new Checkmark(today.minus(i), value, false));
} }
setCheckmarks(checkmarks); setCheckmarks(checkmarks);

@ -42,7 +42,7 @@ import static org.isoron.uhabits.core.models.Checkmark.*;
public class HistoryChart extends ScrollableChart public class HistoryChart extends ScrollableChart
{ {
private int[] checkmarks; private CheckmarkState[] checkmarks;
private double target; private double target;
@ -149,15 +149,15 @@ public class HistoryChart extends ScrollableChart
if (timestamp == null) return false; if (timestamp == null) return false;
Timestamp today = DateUtils.getToday(); Timestamp today = DateUtils.getToday();
int newValue = YES_MANUAL; int newValue = YES;
int offset = timestamp.daysUntil(today); int offset = timestamp.daysUntil(today);
if (offset < checkmarks.length) if (offset < checkmarks.length)
{ {
newValue = Repetition.nextToggleValue(checkmarks[offset]); newValue = Repetition.nextToggleValue(checkmarks[offset].getValue());
checkmarks[offset] = newValue; checkmarks[offset] = new CheckmarkState(newValue, true);
} }
controller.onToggleCheckmark(timestamp, newValue); controller.onToggleCheckmark(timestamp, newValue, true);
postInvalidate(); postInvalidate();
return true; return true;
@ -166,22 +166,28 @@ public class HistoryChart extends ScrollableChart
public void populateWithRandomData() public void populateWithRandomData()
{ {
Random random = new Random(); Random random = new Random();
checkmarks = new int[100]; checkmarks = new CheckmarkState[100];
for (int i = 0; i < 100; i++) for (int i = 0; i < 100; i++)
if (random.nextFloat() < 0.3) checkmarks[i] = 2; if (random.nextFloat() < 0.3)
{
checkmarks[i] = new CheckmarkState(2, random.nextBoolean());
}
for (int i = 0; i < 100 - 7; i++) for (int i = 0; i < 100 - 7; i++)
{ {
int count = 0; int count = 0;
for (int j = 0; j < 7; j++) for (int j = 0; j < 7; j++)
if (checkmarks[i + j] != 0) count++; if (checkmarks[i + j].getValue() != 0) count++;
if (count >= 3) checkmarks[i] = Math.max(checkmarks[i], 1); if (count >= 3)
{
checkmarks[i] = new CheckmarkState(Math.max(checkmarks[i].getValue(), 1), random.nextBoolean());
}
} }
} }
public void setCheckmarks(int[] checkmarks) public void setCheckmarkStates(CheckmarkState[] checkmarks)
{ {
this.checkmarks = checkmarks; this.checkmarks = checkmarks;
postInvalidate(); postInvalidate();
@ -372,7 +378,8 @@ public class HistoryChart extends ScrollableChart
int checkmarkOffset) int checkmarkOffset)
{ {
int checkmark = 0; int value = NO;
boolean manualInput = false;
if (checkmarkOffset >= checkmarks.length) if (checkmarkOffset >= checkmarks.length)
{ {
pSquareBg.setColor(colors[0]); pSquareBg.setColor(colors[0]);
@ -380,13 +387,14 @@ public class HistoryChart extends ScrollableChart
} }
else else
{ {
checkmark = checkmarks[checkmarkOffset]; value = checkmarks[checkmarkOffset].getValue();
if(checkmark == 0) manualInput = checkmarks[checkmarkOffset].isManualInput();
if(value == NO)
{ {
pSquareBg.setColor(colors[0]); pSquareBg.setColor(colors[0]);
pSquareFg.setColor(textColors[1]); pSquareFg.setColor(textColors[1]);
} }
else if ((isNumerical && (checkmark / 1000f >= target) || (!isNumerical && checkmark == YES_MANUAL))) else if ((isNumerical && (value / 1000f >= target) || (!isNumerical && value == YES && manualInput)))
{ {
pSquareBg.setColor(colors[2]); pSquareBg.setColor(colors[2]);
pSquareFg.setColor(textColors[2]); pSquareFg.setColor(textColors[2]);
@ -401,7 +409,7 @@ public class HistoryChart extends ScrollableChart
float round = dpToPixels(getContext(), 2); float round = dpToPixels(getContext(), 2);
canvas.drawRoundRect(location, round, round, pSquareBg); canvas.drawRoundRect(location, round, round, pSquareBg);
if (!isNumerical && checkmark == SKIP) if (!isNumerical && value == SKIP)
{ {
pSquareBg.setColor(backgroundColor); pSquareBg.setColor(backgroundColor);
pSquareBg.setStrokeWidth(columnWidth * 0.025f); pSquareBg.setStrokeWidth(columnWidth * 0.025f);
@ -439,7 +447,7 @@ public class HistoryChart extends ScrollableChart
private void init() private void init()
{ {
isEditable = false; isEditable = false;
checkmarks = new int[0]; checkmarks = new CheckmarkState[0];
controller = new Controller() {}; controller = new Controller() {};
target = 2; target = 2;
@ -545,6 +553,6 @@ public class HistoryChart extends ScrollableChart
public interface Controller public interface Controller
{ {
default void onToggleCheckmark(Timestamp timestamp, int value) {} default void onToggleCheckmark(Timestamp timestamp, int value, boolean manualInput) {}
} }
} }

@ -46,13 +46,13 @@ class CheckmarkButtonView(
invalidate() invalidate()
} }
var value: Int = 0 var value: CheckmarkState = CheckmarkState(0, false)
set(value) { set(value) {
field = value field = value
invalidate() invalidate()
} }
var onToggle: (Int) -> Unit = {} var onToggle: (CheckmarkState) -> Unit = {}
private var drawer = Drawer() private var drawer = Drawer()
init { init {
@ -62,7 +62,7 @@ class CheckmarkButtonView(
} }
fun performToggle() { fun performToggle() {
value = Repetition.nextToggleValue(value) value = CheckmarkState(Repetition.nextToggleValue(value.value), true)
onToggle(value) onToggle(value)
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
invalidate() invalidate()
@ -102,12 +102,11 @@ class CheckmarkButtonView(
} }
fun draw(canvas: Canvas) { fun draw(canvas: Canvas) {
paint.color = when (value) { paint.color = when (value.isManualInput) {
YES_MANUAL -> color true -> color
SKIP -> color false -> lowContrastColor
else -> lowContrastColor
} }
val id = when (value) { val id = when (value.value) {
SKIP -> R.string.fa_skipped SKIP -> R.string.fa_skipped
NO -> R.string.fa_times NO -> R.string.fa_times
else -> R.string.fa_check else -> R.string.fa_check

@ -34,7 +34,7 @@ class CheckmarkPanelView(
@Provided private val buttonFactory: CheckmarkButtonViewFactory @Provided private val buttonFactory: CheckmarkButtonViewFactory
) : ButtonPanelView<CheckmarkButtonView>(context, preferences) { ) : ButtonPanelView<CheckmarkButtonView>(context, preferences) {
var values = IntArray(0) var values: Array<CheckmarkState> = emptyArray()
set(values) { set(values) {
field = values field = values
setupButtons() setupButtons()
@ -46,7 +46,7 @@ class CheckmarkPanelView(
setupButtons() setupButtons()
} }
var onToggle: (Timestamp, Int) -> Unit = {_, _ ->} var onToggle: (Timestamp, Int, Boolean) -> Unit = {_, _, _ ->}
set(value) { set(value) {
field = value field = value
setupButtons() setupButtons()
@ -62,10 +62,10 @@ class CheckmarkPanelView(
val timestamp = today.minus(index + dataOffset) val timestamp = today.minus(index + dataOffset)
button.value = when { button.value = when {
index + dataOffset < values.size -> values[index + dataOffset] index + dataOffset < values.size -> values[index + dataOffset]
else -> NO else -> CheckmarkState(NO, false)
} }
button.color = color button.color = color
button.onToggle = { value -> onToggle(timestamp, value) } button.onToggle = { state -> onToggle(timestamp, state.value, state.isManualInput) }
} }
} }
} }

@ -21,7 +21,6 @@ package org.isoron.uhabits.activities.habits.list.views;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.widget.*;
import android.view.*; import android.view.*;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -181,10 +180,10 @@ public class HabitCardListAdapter
Habit habit = cache.getHabitByPosition(position); Habit habit = cache.getHabitByPosition(position);
double score = cache.getScore(habit.getId()); double score = cache.getScore(habit.getId());
int checkmarks[] = cache.getCheckmarks(habit.getId()); CheckmarkState[] checkmarkStates = cache.getCheckmarkStates(habit.getId());
boolean selected = this.selected.contains(habit); boolean selected = this.selected.contains(habit);
listView.bindCardView(holder, habit, score, checkmarks, selected); listView.bindCardView(holder, habit, score, checkmarkStates, selected);
} }
@Override @Override

@ -21,7 +21,6 @@ package org.isoron.uhabits.activities.habits.list.views
import android.content.* import android.content.*
import android.os.* import android.os.*
import androidx.appcompat.widget.*
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.ItemTouchHelper.* import androidx.recyclerview.widget.ItemTouchHelper.*
import android.view.* import android.view.*
@ -69,12 +68,12 @@ class HabitCardListView(
fun bindCardView(holder: HabitCardViewHolder, fun bindCardView(holder: HabitCardViewHolder,
habit: Habit, habit: Habit,
score: Double, score: Double,
checkmarks: IntArray, checkmarkStates: Array<CheckmarkState>,
selected: Boolean): View { selected: Boolean): View {
val cardView = holder.itemView as HabitCardView val cardView = holder.itemView as HabitCardView
cardView.habit = habit cardView.habit = habit
cardView.isSelected = selected cardView.isSelected = selected
cardView.values = checkmarks cardView.values = checkmarkStates
cardView.buttonCount = checkmarkCount cardView.buttonCount = checkmarkCount
cardView.dataOffset = dataOffset cardView.dataOffset = dataOffset
cardView.score = score cardView.score = score

@ -86,7 +86,7 @@ class HabitCardView(
get() = checkmarkPanel.values get() = checkmarkPanel.values
set(values) { set(values) {
checkmarkPanel.values = values checkmarkPanel.values = values
numberPanel.values = values.map { it / 1000.0 }.toDoubleArray() numberPanel.values = values.map { it.value / 1000.0 }.toDoubleArray()
} }
var threshold: Double var threshold: Double
@ -121,9 +121,9 @@ class HabitCardView(
} }
checkmarkPanel = checkmarkPanelFactory.create().apply { checkmarkPanel = checkmarkPanelFactory.create().apply {
onToggle = { timestamp, value -> onToggle = { timestamp, value, manualInput ->
triggerRipple(timestamp) triggerRipple(timestamp)
habit?.let { behavior.onToggle(it, timestamp, value) } habit?.let { behavior.onToggle(it, timestamp, value, manualInput) }
} }
} }

@ -94,9 +94,9 @@ public class ShowHabitScreen extends BaseScreen
} }
@Override @Override
public void onToggleCheckmark(Timestamp timestamp, int value) public void onToggleCheckmark(Timestamp timestamp, int value, boolean manualInput)
{ {
behavior.get().onToggleCheckmark(timestamp, value); behavior.get().onToggleCheckmark(timestamp, value, manualInput);
} }
@Override @Override

@ -119,9 +119,9 @@ public class HistoryCard extends HabitCard
public void doInBackground() public void doInBackground()
{ {
if (isCanceled()) return; if (isCanceled()) return;
int[] checkmarks = habit.getCheckmarks().getAllValues(); CheckmarkState[] checkmarkStates = habit.getCheckmarks().getAllStates();
if(prefs != null) chart.setFirstWeekday(prefs.getFirstWeekday()); if(prefs != null) chart.setFirstWeekday(prefs.getFirstWeekday());
chart.setCheckmarks(checkmarks); chart.setCheckmarkStates(checkmarkStates);
} }
@Override @Override

@ -23,6 +23,7 @@ import android.app.*
import android.content.* import android.content.*
import android.view.* import android.view.*
import org.isoron.uhabits.core.models.* import org.isoron.uhabits.core.models.*
import org.isoron.uhabits.core.models.Checkmark.NO
import org.isoron.uhabits.utils.* import org.isoron.uhabits.utils.*
import org.isoron.uhabits.widgets.views.* import org.isoron.uhabits.widgets.views.*
@ -46,12 +47,13 @@ open class CheckmarkWidget(
setActiveColor(PaletteUtils.getColor(context, habit.color)) setActiveColor(PaletteUtils.getColor(context, habit.color))
setName(habit.name) setName(habit.name)
setCheckmarkValue(habit.checkmarks.todayValue)
val state = habit.checkmarks.todayState
if (habit.isNumerical) { if (habit.isNumerical) {
setNumerical(true) setNumerical(true)
setCheckmarkState(getNumericalCheckmarkState()) setCheckmarkState(getNumericalCheckmarkState(state))
} else { } else {
setCheckmarkState(habit.checkmarks.todayValue) setCheckmarkState(state)
} }
setPercentage(habit.scores.todayValue.toFloat()) setPercentage(habit.scores.todayValue.toFloat())
refresh() refresh()
@ -65,11 +67,11 @@ open class CheckmarkWidget(
override fun getDefaultHeight() = 125 override fun getDefaultHeight() = 125
override fun getDefaultWidth() = 125 override fun getDefaultWidth() = 125
private fun getNumericalCheckmarkState(): Int { private fun getNumericalCheckmarkState(state: CheckmarkState): CheckmarkState {
return if (habit.isCompletedToday) { return if (habit.isCompletedToday) {
Checkmark.YES_MANUAL CheckmarkState(state.value, true)
} else { } else {
Checkmark.NO CheckmarkState(state.value, false)
} }
} }

@ -45,7 +45,7 @@ class HistoryWidget(
(widgetView.dataView as HistoryChart).apply { (widgetView.dataView as HistoryChart).apply {
setFirstWeekday(firstWeekday) setFirstWeekday(firstWeekday)
setColor(PaletteUtils.getColor(context, habit.color)) setColor(PaletteUtils.getColor(context, habit.color))
setCheckmarks(habit.checkmarks.allValues) setCheckmarkStates(habit.checkmarks.allStates)
setNumerical(habit.isNumerical) setNumerical(habit.isNumerical)
setTarget(habit.targetValue / habit.frequency.denominator) setTarget(habit.targetValue / habit.frequency.denominator)
} }

@ -19,6 +19,7 @@
package org.isoron.uhabits.widgets.views; package org.isoron.uhabits.widgets.views;
import android.content.*; import android.content.*;
import android.util.*; import android.util.*;
import android.widget.*; import android.widget.*;
@ -35,6 +36,9 @@ import org.isoron.uhabits.utils.*;
import static org.isoron.androidbase.utils.InterfaceUtils.getDimension; import static org.isoron.androidbase.utils.InterfaceUtils.getDimension;
public class CheckmarkWidgetView extends HabitWidgetView { public class CheckmarkWidgetView extends HabitWidgetView {
protected int activeColor; protected int activeColor;
@ -49,74 +53,76 @@ public class CheckmarkWidgetView extends HabitWidgetView {
protected int checkmarkValue; protected int checkmarkValue;
protected int checkmarkState; protected CheckmarkState checkmarkState;
protected boolean isNumerical; protected boolean isNumerical;
public CheckmarkWidgetView(Context context)
{ public CheckmarkWidgetView(Context context) {
super(context); super(context);
init(); init();
} }
public CheckmarkWidgetView(Context context, AttributeSet attrs)
{ public CheckmarkWidgetView(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
init(); init();
} }
public void refresh()
{ public void refresh() {
if (backgroundPaint == null || frame == null || ring == null) return; if (backgroundPaint == null || frame == null || ring == null) {
return;
}
StyledResources res = new StyledResources(getContext()); StyledResources res = new StyledResources(getContext());
int bgColor; int bgColor;
int fgColor; int fgColor;
switch (checkmarkState) { if (checkmarkState.isManualInput()) {
case Checkmark.YES_MANUAL: bgColor = activeColor;
case Checkmark.SKIP: fgColor = res.getColor(R.attr.highContrastReverseTextColor);
bgColor = activeColor; setShadowAlpha(0x4f);
fgColor = res.getColor(R.attr.highContrastReverseTextColor); backgroundPaint.setColor(bgColor);
setShadowAlpha(0x4f); frame.setBackgroundDrawable(background);
backgroundPaint.setColor(bgColor); } else {
frame.setBackgroundDrawable(background); bgColor = res.getColor(R.attr.cardBgColor);
break; fgColor = res.getColor(R.attr.mediumContrastTextColor);
setShadowAlpha(0x00);
case Checkmark.YES_AUTO:
case Checkmark.NO:
default:
getResources().getString(R.string.fa_times);
bgColor = res.getColor(R.attr.cardBgColor);
fgColor = res.getColor(R.attr.mediumContrastTextColor);
setShadowAlpha(0x00);
break;
} }
ring.setPercentage(percentage); ring.setPercentage(percentage);
ring.setColor(fgColor); ring.setColor(fgColor);
ring.setBackgroundColor(bgColor); ring.setBackgroundColor(bgColor);
ring.setText(getText()); ring.setText(
getText());
label.setText(name); label.setText(name);
label.setTextColor(fgColor); label.setTextColor(fgColor);
requestLayout(); requestLayout();
postInvalidate(); postInvalidate();
} }
public void setCheckmarkState(int checkmarkState)
{ public void setCheckmarkState(CheckmarkState checkmarkState) {
this.checkmarkState = checkmarkState; this.checkmarkState = checkmarkState;
} }
protected String getText()
{ protected String getText() {
if (isNumerical) return NumberButtonViewKt.toShortString(checkmarkValue / 1000.0); if (isNumerical) {
switch (checkmarkState) { return NumberButtonViewKt.toShortString(checkmarkState.getValue() / 1000.0);
case Checkmark.YES_MANUAL: }
case Checkmark.YES_AUTO: switch (checkmarkState.getValue()) {
case Checkmark.YES:
return getResources().getString(R.string.fa_check); return getResources().getString(R.string.fa_check);
case Checkmark.SKIP: case Checkmark.SKIP:
return getResources().getString(R.string.fa_skipped); return getResources().getString(R.string.fa_skipped);
@ -126,41 +132,36 @@ public class CheckmarkWidgetView extends HabitWidgetView {
} }
} }
public void setActiveColor(int activeColor)
{ public void setActiveColor(int activeColor) {
this.activeColor = activeColor; this.activeColor = activeColor;
} }
public void setCheckmarkValue(int checkmarkValue)
{
this.checkmarkValue = checkmarkValue;
}
public void setName(@NonNull String name) public void setName(@NonNull String name) {
{
this.name = name; this.name = name;
} }
public void setPercentage(float percentage)
{ public void setPercentage(float percentage) {
this.percentage = percentage; this.percentage = percentage;
} }
public void setNumerical(boolean isNumerical)
{ public void setNumerical(boolean isNumerical) {
this.isNumerical = isNumerical; this.isNumerical = isNumerical;
} }
@Override @Override
@NonNull @NonNull
protected Integer getInnerLayoutId() protected Integer getInnerLayoutId() {
{
return R.layout.widget_checkmark; return R.layout.widget_checkmark;
} }
@Override @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
{
int width = MeasureSpec.getSize(widthMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec);
@ -171,10 +172,11 @@ public class CheckmarkWidgetView extends HabitWidgetView {
w *= scale; w *= scale;
h *= scale; h *= scale;
if (h < getDimension(getContext(), R.dimen.checkmarkWidget_heightBreakpoint)) if (h < getDimension(getContext(), R.dimen.checkmarkWidget_heightBreakpoint)) {
ring.setVisibility(GONE); ring.setVisibility(GONE);
else } else {
ring.setVisibility(VISIBLE); ring.setVisibility(VISIBLE);
}
widthMeasureSpec = widthMeasureSpec =
MeasureSpec.makeMeasureSpec((int) w, MeasureSpec.EXACTLY); MeasureSpec.makeMeasureSpec((int) w, MeasureSpec.EXACTLY);
@ -192,19 +194,20 @@ public class CheckmarkWidgetView extends HabitWidgetView {
super.onMeasure(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} }
private void init()
{ private void init() {
ring = (RingView) findViewById(R.id.scoreRing); ring = (RingView) findViewById(R.id.scoreRing);
label = (TextView) findViewById(R.id.label); label = (TextView) findViewById(R.id.label);
if (ring != null) ring.setIsTransparencyEnabled(true); if (ring != null) {
ring.setIsTransparencyEnabled(true);
}
if (isInEditMode()) if (isInEditMode()) {
{
percentage = 0.75f; percentage = 0.75f;
name = "Wake up early"; name = "Wake up early";
activeColor = PaletteUtils.getAndroidTestColor(6); activeColor = PaletteUtils.getAndroidTestColor(6);
checkmarkValue = Checkmark.YES_MANUAL; checkmarkState = new CheckmarkState(Checkmark.YES, true);
refresh(); refresh();
} }
} }

@ -61,8 +61,9 @@ public class WidgetControllerTest extends BaseAndroidJVMTest
public void testOnAddRepetition_whenChecked() throws Exception public void testOnAddRepetition_whenChecked() throws Exception
{ {
habit.getRepetitions().toggle(today); habit.getRepetitions().toggle(today);
int todayValue = habit.getCheckmarks().getTodayValue(); CheckmarkState todayValue = habit.getCheckmarks().getTodayState();
assertThat(todayValue, equalTo(YES_MANUAL)); assertThat(todayValue.getValue(), equalTo(YES));
assertThat(todayValue.isManualInput(), equalTo(true));
controller.onAddRepetition(habit, today); controller.onAddRepetition(habit, today);
verifyZeroInteractions(commandRunner); verifyZeroInteractions(commandRunner);
} }
@ -81,8 +82,9 @@ public class WidgetControllerTest extends BaseAndroidJVMTest
public void testOnRemoveRepetition_whenChecked() throws Exception public void testOnRemoveRepetition_whenChecked() throws Exception
{ {
habit.getRepetitions().toggle(today); habit.getRepetitions().toggle(today);
int todayValue = habit.getCheckmarks().getTodayValue(); CheckmarkState todayValue = habit.getCheckmarks().getTodayState();
assertThat(todayValue, equalTo(YES_MANUAL)); assertThat(todayValue.getValue(), equalTo(YES));
assertThat(todayValue.isManualInput(), equalTo(true));
controller.onRemoveRepetition(habit, today); controller.onRemoveRepetition(habit, today);
verify(commandRunner).execute(any(), isNull()); verify(commandRunner).execute(any(), isNull());
} }
@ -102,4 +104,4 @@ public class WidgetControllerTest extends BaseAndroidJVMTest
controller.onToggleRepetition(habit, today); controller.onToggleRepetition(habit, today);
verify(commandRunner).execute(any(), isNull()); verify(commandRunner).execute(any(), isNull());
} }
} }

@ -22,5 +22,5 @@ package org.isoron.uhabits.core;
public class Config public class Config
{ {
public static final String DATABASE_FILENAME = "uhabits.db"; public static final String DATABASE_FILENAME = "uhabits.db";
public static int DATABASE_VERSION = 23; public static int DATABASE_VERSION = 24;
} }

@ -36,6 +36,8 @@ public class CreateRepetitionCommand extends Command
final int value; final int value;
final boolean manualInput;
@Nullable @Nullable
Repetition previousRep; Repetition previousRep;
@ -45,12 +47,14 @@ public class CreateRepetitionCommand extends Command
public CreateRepetitionCommand(@NonNull HabitList habitList, public CreateRepetitionCommand(@NonNull HabitList habitList,
@NonNull Habit habit, @NonNull Habit habit,
Timestamp timestamp, Timestamp timestamp,
int value) int value,
boolean manualInput)
{ {
this.habitList = habitList; this.habitList = habitList;
this.timestamp = timestamp; this.timestamp = timestamp;
this.habit = habit; this.habit = habit;
this.value = value; this.value = value;
this.manualInput = manualInput;
} }
@Override @Override
@ -63,7 +67,7 @@ public class CreateRepetitionCommand extends Command
if (value > 0) if (value > 0)
{ {
newRep = new Repetition(timestamp, value); newRep = new Repetition(timestamp, value, manualInput);
reps.add(newRep); reps.add(newRep);
} }
@ -106,6 +110,8 @@ public class CreateRepetitionCommand extends Command
public int value; public int value;
public boolean manualInput;
public Record(CreateRepetitionCommand command) public Record(CreateRepetitionCommand command)
{ {
id = command.getId(); id = command.getId();
@ -115,6 +121,7 @@ public class CreateRepetitionCommand extends Command
this.habit = habitId; this.habit = habitId;
this.repTimestamp = command.timestamp.getUnixTime(); this.repTimestamp = command.timestamp.getUnixTime();
this.value = command.value; this.value = command.value;
this.manualInput = command.manualInput;
} }
public CreateRepetitionCommand toCommand(@NonNull HabitList habitList) public CreateRepetitionCommand toCommand(@NonNull HabitList habitList)
@ -123,10 +130,9 @@ public class CreateRepetitionCommand extends Command
if(h == null) throw new HabitNotFoundException(); if(h == null) throw new HabitNotFoundException();
CreateRepetitionCommand command; CreateRepetitionCommand command;
command = new CreateRepetitionCommand( command = new CreateRepetitionCommand(habitList, h, new Timestamp(repTimestamp), value, manualInput);
habitList, h, new Timestamp(repTimestamp), value);
command.setId(id); command.setId(id);
return command; return command;
} }
} }
} }

@ -109,7 +109,7 @@ public class LoopDBImporter extends AbstractImporter
habitRecord.id.toString()); habitRecord.id.toString());
for (RepetitionRecord r : reps) for (RepetitionRecord r : reps)
h.getRepetitions().toggle(new Timestamp(r.timestamp), r.value); h.getRepetitions().toggle(new Timestamp(r.timestamp), r.value, r.isManualInput());
} }
} }
} }

@ -19,6 +19,7 @@
package org.isoron.uhabits.core.models; package org.isoron.uhabits.core.models;
import androidx.annotation.NonNull;
import org.apache.commons.lang3.builder.*; import org.apache.commons.lang3.builder.*;
import javax.annotation.concurrent.*; import javax.annotation.concurrent.*;
@ -40,18 +41,12 @@ public final class Checkmark
/** /**
* Indicates that there was an explicit skip at the timestamp. * Indicates that there was an explicit skip at the timestamp.
*/ */
public static final int SKIP = 3; public static final int SKIP = 2;
/** /**
* Indicates that there was a repetition at the timestamp. * Indicates that there was a repetition at the timestamp.
*/ */
public static final int YES_MANUAL = 2; public static final int YES = 1;
/**
* Indicates that there was no repetition at the timestamp, but one was not
* expected in any case, due to the frequency of the habit.
*/
public static final int YES_AUTO = 1;
/** /**
* Indicates that there was no repetition at the timestamp, even though a * Indicates that there was no repetition at the timestamp, even though a
@ -62,19 +57,15 @@ public final class Checkmark
private final Timestamp timestamp; private final Timestamp timestamp;
/** /**
* The value of the checkmark. * The state of the checkmark.
* <p>
* For boolean habits, this equals either NO, YES_AUTO, YES_MANUAL or SKIP.
* <p>
* For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is stored as 1500.
*/ */
private final int value; @NonNull
private final CheckmarkState state;
public Checkmark(Timestamp timestamp, int value) public Checkmark(Timestamp timestamp, int value, boolean manualInput)
{ {
this.timestamp = timestamp; this.timestamp = timestamp;
this.value = value; this.state = new CheckmarkState(value, manualInput);
} }
@Override @Override
@ -88,7 +79,7 @@ public final class Checkmark
return new EqualsBuilder() return new EqualsBuilder()
.append(timestamp, checkmark.timestamp) .append(timestamp, checkmark.timestamp)
.append(value, checkmark.value) .append(getState(), checkmark.getState())
.isEquals(); .isEquals();
} }
@ -97,9 +88,19 @@ public final class Checkmark
return timestamp; return timestamp;
} }
public CheckmarkState getState()
{
return state;
}
public int getValue() public int getValue()
{ {
return value; return state.getValue();
}
public boolean isManualInput()
{
return state.isManualInput();
} }
@Override @Override
@ -107,7 +108,7 @@ public final class Checkmark
{ {
return new HashCodeBuilder(17, 37) return new HashCodeBuilder(17, 37)
.append(timestamp) .append(timestamp)
.append(value) .append(getState())
.toHashCode(); .toHashCode();
} }
@ -116,7 +117,8 @@ public final class Checkmark
{ {
return new ToStringBuilder(this, defaultToStringStyle()) return new ToStringBuilder(this, defaultToStringStyle())
.append("timestamp", timestamp) .append("timestamp", timestamp)
.append("value", value) .append("value", getValue())
.append("manualInput", isManualInput())
.toString(); .toString();
} }
} }

@ -19,8 +19,10 @@
package org.isoron.uhabits.core.models; package org.isoron.uhabits.core.models;
import androidx.annotation.*; import androidx.annotation.*;
import java.util.stream.Collectors;
import org.apache.commons.lang3.builder.*; import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.core.utils.*; import org.isoron.uhabits.core.utils.*;
@ -33,58 +35,67 @@ import javax.annotation.concurrent.*;
import static org.isoron.uhabits.core.models.Checkmark.*; import static org.isoron.uhabits.core.models.Checkmark.*;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle; import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/** /**
* The collection of {@link Checkmark}s belonging to a habit. * The collection of {@link Checkmark}s belonging to a habit.
*/ */
@ThreadSafe @ThreadSafe
public abstract class CheckmarkList public abstract class CheckmarkList {
{
protected final Habit habit; protected final Habit habit;
public final ModelObservable observable; public final ModelObservable observable;
public CheckmarkList(Habit habit)
{ public CheckmarkList(Habit habit) {
this.habit = habit; this.habit = habit;
this.observable = new ModelObservable(); this.observable = new ModelObservable();
} }
@NonNull @NonNull
static List<Checkmark> buildCheckmarksFromIntervals(Repetition[] reps, static List<Checkmark> buildCheckmarksFromIntervals(
ArrayList<Interval> intervals) Repetition[] reps,
{ ArrayList<Interval> intervals
if (reps.length == 0) throw new IllegalArgumentException(); ) {
if (reps.length == 0) {
throw new IllegalArgumentException();
}
Timestamp today = DateUtils.getToday(); Timestamp today = DateUtils.getToday();
Timestamp begin = reps[0].getTimestamp(); Timestamp begin = reps[0].getTimestamp();
if (intervals.size() > 0) begin = Timestamp.oldest(begin, intervals.get(0).begin); if (intervals.size() > 0) {
begin = Timestamp.oldest(begin, intervals.get(0).begin);
}
int nDays = begin.daysUntil(today) + 1; int nDays = begin.daysUntil(today) + 1;
List<Checkmark> checkmarks = new ArrayList<>(nDays); List<Checkmark> checkmarks = new ArrayList<>(nDays);
for (int i = 0; i < nDays; i++) for (int i = 0; i < nDays; i++) {
checkmarks.add(new Checkmark(today.minus(i), NO)); checkmarks.add(new Checkmark(today.minus(i), NO, false));
}
for (Interval interval : intervals) for (Interval interval : intervals) {
{ for (int i = 0; i < interval.length(); i++) {
for (int i = 0; i < interval.length(); i++)
{
Timestamp date = interval.begin.plus(i); Timestamp date = interval.begin.plus(i);
int offset = date.daysUntil(today); int offset = date.daysUntil(today);
if (offset < 0) continue; if (offset < 0) {
checkmarks.set(offset, new Checkmark(date, YES_AUTO)); continue;
}
checkmarks.set(offset, new Checkmark(date, YES, false));
} }
} }
for (Repetition rep : reps) for (Repetition rep : reps) {
{
Timestamp date = rep.getTimestamp(); Timestamp date = rep.getTimestamp();
int offset = date.daysUntil(today); int offset = date.daysUntil(today);
checkmarks.set(offset, new Checkmark(date, rep.getValue())); checkmarks.set(offset, new Checkmark(date, rep.getValue(), rep.isManualInput()));
} }
return checkmarks; return checkmarks;
} }
/** /**
* For non-daily habits, some groups of repetitions generate many * For non-daily habits, some groups of repetitions generate many
* checkmarks. For example, for weekly habits, each repetition generates * checkmarks. For example, for weekly habits, each repetition generates
@ -97,25 +108,29 @@ public abstract class CheckmarkList
* away in the future as possible. * away in the future as possible.
*/ */
@NonNull @NonNull
static ArrayList<Interval> buildIntervals(@NonNull Frequency freq, static ArrayList<Interval> buildIntervals(
@NonNull Repetition[] reps) @NonNull Frequency freq,
{ @NonNull Repetition[] reps
) {
ArrayList<Repetition> filteredReps = new ArrayList<>(); ArrayList<Repetition> filteredReps = new ArrayList<>();
for (Repetition r : reps) for (Repetition r : reps) {
if (r.getValue() == YES_MANUAL) if (r.getValue() == YES && r.isManualInput()) {
filteredReps.add(r); filteredReps.add(r);
}
}
int num = freq.getNumerator(); int num = freq.getNumerator();
int den = freq.getDenominator(); int den = freq.getDenominator();
ArrayList<Interval> intervals = new ArrayList<>(); ArrayList<Interval> intervals = new ArrayList<>();
for (int i = 0; i < filteredReps.size() - num + 1; i++) for (int i = 0; i < filteredReps.size() - num + 1; i++) {
{
Repetition first = filteredReps.get(i); Repetition first = filteredReps.get(i);
Repetition last = filteredReps.get(i + num - 1); Repetition last = filteredReps.get(i + num - 1);
long distance = first.getTimestamp().daysUntil(last.getTimestamp()); long distance = first.getTimestamp().daysUntil(last.getTimestamp());
if (distance >= den) continue; if (distance >= den) {
continue;
}
Timestamp begin = first.getTimestamp(); Timestamp begin = first.getTimestamp();
Timestamp center = last.getTimestamp(); Timestamp center = last.getTimestamp();
@ -126,32 +141,33 @@ public abstract class CheckmarkList
return intervals; return intervals;
} }
/** /**
* Starting from the second newest interval, this function tries to slide the * Starting from the second newest interval, this function tries to slide the
* intervals backwards into the past, so that gaps are eliminated and * intervals backwards into the past, so that gaps are eliminated and
* streaks are maximized. * streaks are maximized.
*/ */
static void snapIntervalsTogether(@NonNull ArrayList<Interval> intervals) static void snapIntervalsTogether(@NonNull ArrayList<Interval> intervals) {
{
int n = intervals.size(); int n = intervals.size();
for (int i = n - 2; i >= 0; i--) for (int i = n - 2; i >= 0; i--) {
{
Interval curr = intervals.get(i); Interval curr = intervals.get(i);
Interval next = intervals.get(i + 1); Interval next = intervals.get(i + 1);
int gapNextToCurrent = next.begin.daysUntil(curr.end); int gapNextToCurrent = next.begin.daysUntil(curr.end);
int gapCenterToEnd = curr.center.daysUntil(curr.end); int gapCenterToEnd = curr.center.daysUntil(curr.end);
if (gapNextToCurrent >= 0) if (gapNextToCurrent >= 0) {
{
int shift = Math.min(gapCenterToEnd, gapNextToCurrent + 1); int shift = Math.min(gapCenterToEnd, gapNextToCurrent + 1);
intervals.set(i, new Interval(curr.begin.minus(shift), intervals.set(i, new Interval(
curr.center, curr.begin.minus(shift),
curr.end.minus(shift))); curr.center,
curr.end.minus(shift)
));
} }
} }
} }
/** /**
* Adds all the given checkmarks to the list. * Adds all the given checkmarks to the list.
* <p> * <p>
@ -162,6 +178,7 @@ public abstract class CheckmarkList
*/ */
public abstract void add(List<Checkmark> checkmarks); public abstract void add(List<Checkmark> checkmarks);
/** /**
* Returns the values for all the checkmarks, since the oldest repetition of * Returns the values for all the checkmarks, since the oldest repetition of
* the habit until today. * the habit until today.
@ -175,10 +192,11 @@ public abstract class CheckmarkList
* @return values for the checkmarks in the interval * @return values for the checkmarks in the interval
*/ */
@NonNull @NonNull
public synchronized final int[] getAllValues() public synchronized final int[] getAllValues() {
{
Repetition oldestRep = habit.getRepetitions().getOldest(); Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return new int[0]; if (oldestRep == null) {
return new int[0];
}
Timestamp fromTimestamp = oldestRep.getTimestamp(); Timestamp fromTimestamp = oldestRep.getTimestamp();
Timestamp toTimestamp = DateUtils.getToday(); Timestamp toTimestamp = DateUtils.getToday();
@ -186,6 +204,33 @@ public abstract class CheckmarkList
return getValues(fromTimestamp, toTimestamp); return getValues(fromTimestamp, toTimestamp);
} }
/**
* 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 CheckmarkState[] getAllStates() {
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) {
return new CheckmarkState[0];
}
Timestamp fromTimestamp = oldestRep.getTimestamp();
Timestamp toTimestamp = DateUtils.getToday();
return getCheckmarkStates(fromTimestamp, toTimestamp);
}
/** /**
* Returns the list of checkmarks that fall within the given interval. * Returns the list of checkmarks that fall within the given interval.
* <p> * <p>
@ -196,11 +241,15 @@ public abstract class CheckmarkList
* *
* @param fromTimestamp timestamp of the beginning of the interval. * @param fromTimestamp timestamp of the beginning of the interval.
* @param toTimestamp timestamp of the end of the interval. * @param toTimestamp timestamp of the end of the interval.
*
* @return the list of checkmarks within the interval. * @return the list of checkmarks within the interval.
*/ */
@NonNull @NonNull
public abstract List<Checkmark> getByInterval(Timestamp fromTimestamp, public abstract List<Checkmark> getByInterval(
Timestamp toTimestamp); Timestamp fromTimestamp,
Timestamp toTimestamp
);
/** /**
* Returns the checkmark for today. * Returns the checkmark for today.
@ -208,54 +257,72 @@ public abstract class CheckmarkList
* @return checkmark for today * @return checkmark for today
*/ */
@Nullable @Nullable
public synchronized final Checkmark getToday() public synchronized final Checkmark getToday() {
{
compute(); compute();
Timestamp today = DateUtils.getToday(); Timestamp today = DateUtils.getToday();
return getByInterval(today, today).get(0); return getByInterval(today, today).get(0);
} }
/** /**
* Returns the value of today's checkmark. * Returns the value of today's checkmark.
* *
* @return value of today's checkmark * @return value of today's checkmark
*/ */
public synchronized int getTodayValue() public synchronized int getTodayValue() {
{
Checkmark today = getToday(); Checkmark today = getToday();
if (today != null) return today.getValue(); if (today != null) {
else return NO; return today.getValue();
} else {
return NO;
}
} }
public synchronized int getThisWeekValue(int firstWeekday)
{ /**
* Returns the value of today's checkmark.
*
* @return value of today's checkmark
*/
public synchronized CheckmarkState getTodayState() {
Checkmark today = getToday();
if (today != null) {
return new CheckmarkState(today.getValue(), today.isManualInput());
} else {
return new CheckmarkState(NO, false);
}
}
public synchronized int getThisWeekValue(int firstWeekday) {
return getThisIntervalValue(DateUtils.TruncateField.WEEK_NUMBER, firstWeekday); return getThisIntervalValue(DateUtils.TruncateField.WEEK_NUMBER, firstWeekday);
} }
public synchronized int getThisMonthValue()
{ public synchronized int getThisMonthValue() {
return getThisIntervalValue(DateUtils.TruncateField.MONTH, Calendar.SATURDAY); return getThisIntervalValue(DateUtils.TruncateField.MONTH, Calendar.SATURDAY);
} }
public synchronized int getThisQuarterValue() public synchronized int getThisQuarterValue() {
{
return getThisIntervalValue(DateUtils.TruncateField.QUARTER, Calendar.SATURDAY); return getThisIntervalValue(DateUtils.TruncateField.QUARTER, Calendar.SATURDAY);
} }
public synchronized int getThisYearValue() public synchronized int getThisYearValue() {
{
return getThisIntervalValue(DateUtils.TruncateField.YEAR, Calendar.SATURDAY); return getThisIntervalValue(DateUtils.TruncateField.YEAR, Calendar.SATURDAY);
} }
private int getThisIntervalValue(DateUtils.TruncateField truncateField, int firstWeekday)
{ private int getThisIntervalValue(DateUtils.TruncateField truncateField, int firstWeekday) {
List<Checkmark> groups = habit.getCheckmarks().groupBy(truncateField, firstWeekday, 1); List<Checkmark> groups = habit.getCheckmarks().groupBy(truncateField, firstWeekday, 1);
if (groups.isEmpty()) return 0; if (groups.isEmpty()) {
return 0;
}
return groups.get(0).getValue(); return groups.get(0).getValue();
} }
/** /**
* Returns the values of the checkmarks that fall inside a certain interval * Returns the values of the checkmarks that fall inside a certain interval
* of time. * of time.
@ -267,22 +334,54 @@ public abstract class CheckmarkList
* *
* @param from timestamp for the oldest checkmark * @param from timestamp for the oldest checkmark
* @param to timestamp for the newest checkmark * @param to timestamp for the newest checkmark
*
* @return values for the checkmarks inside the given interval * @return values for the checkmarks inside the given interval
*/ */
public final int[] getValues(Timestamp from, Timestamp to) public final int[] getValues(Timestamp from, Timestamp to) {
{ if (from.isNewerThan(to)) {
if (from.isNewerThan(to)) return new int[0]; return new int[0];
}
List<Checkmark> checkmarks = getByInterval(from, to); List<Checkmark> checkmarks = getByInterval(from, to);
int values[] = new int[checkmarks.size()]; int values[] = new int[checkmarks.size()];
int i = 0; int i = 0;
for (Checkmark c : checkmarks) for (Checkmark c : checkmarks) {
values[i++] = c.getValue(); values[i++] = c.getValue();
}
return values; return values;
} }
/**
* Returns the checkmarkStates that fall inside a certain interval
* of time.
* <p>
* The values are returned in an array containing the checkmark 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 the checkmarks inside the given interval
*/
public final CheckmarkState[] getCheckmarkStates(Timestamp from, Timestamp to) {
if (from.isNewerThan(to)) {
return new CheckmarkState[0];
}
final List<Checkmark> checkmarks = getByInterval(from, to);
final CheckmarkState[] checkmarkStates = new CheckmarkState[checkmarks.size()];
for (int i = 0; i < checkmarks.size(); i++) {
checkmarkStates[i] = checkmarks.get(i).getState();
}
return checkmarkStates;
}
/** /**
* Marks as invalid every checkmark that has timestamp either equal or newer * Marks as invalid every checkmark that has timestamp either equal or newer
* than a given timestamp. These checkmarks will be recomputed at the next * than a given timestamp. These checkmarks will be recomputed at the next
@ -292,47 +391,50 @@ public abstract class CheckmarkList
*/ */
public abstract void invalidateNewerThan(Timestamp timestamp); public abstract void invalidateNewerThan(Timestamp timestamp);
/** /**
* Writes the entire list of checkmarks to the given writer, in CSV format. * Writes the entire list of checkmarks to the given writer, in CSV format.
* *
* @param out the writer where the CSV will be output * @param out the writer where the CSV will be output
*
* @throws IOException in case write operations fail * @throws IOException in case write operations fail
*/ */
public final void writeCSV(Writer out) throws IOException public final void writeCSV(Writer out) throws IOException {
{ CheckmarkState[] states;
int values[];
synchronized (this) synchronized (this) {
{
compute(); compute();
values = getAllValues(); states = getAllStates();
} }
Timestamp timestamp = DateUtils.getToday(); Timestamp timestamp = DateUtils.getToday();
SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat(); SimpleDateFormat dateFormat = DateFormats.getCSVDateFormat();
for (int value : values) for (CheckmarkState state : states) {
{
String date = dateFormat.format(timestamp.toJavaDate()); String date = dateFormat.format(timestamp.toJavaDate());
out.write(String.format("%s,%d\n", date, value)); out.write(String.format("%s,%d,%d\n", date, state.getValue(), state.isManualInput() ? 1 : 0));
timestamp = timestamp.minus(1); timestamp = timestamp.minus(1);
} }
} }
/** /**
* Computes and stores one checkmark for each day, from the first habit * Computes and stores one checkmark for each day, from the first habit
* repetition to today. If this list is already computed, does nothing. * repetition to today. If this list is already computed, does nothing.
*/ */
protected final synchronized void compute() protected final synchronized void compute() {
{
final Timestamp today = DateUtils.getToday(); final Timestamp today = DateUtils.getToday();
Checkmark newest = getNewestComputed(); Checkmark newest = getNewestComputed();
if (newest != null && newest.getTimestamp().equals(today)) return; if (newest != null && newest.getTimestamp().equals(today)) {
return;
}
invalidateNewerThan(Timestamp.ZERO); invalidateNewerThan(Timestamp.ZERO);
Repetition oldestRep = habit.getRepetitions().getOldest(); Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return; if (oldestRep == null) {
return;
}
final Timestamp from = oldestRep.getTimestamp(); final Timestamp from = oldestRep.getTimestamp();
Repetition reps[] = habit Repetition reps[] = habit
@ -340,10 +442,14 @@ public abstract class CheckmarkList
.getByInterval(from, today) .getByInterval(from, today)
.toArray(new Repetition[0]); .toArray(new Repetition[0]);
if (habit.isNumerical()) computeNumerical(reps); if (habit.isNumerical()) {
else computeYesNo(reps); computeNumerical(reps);
} else {
computeYesNo(reps);
}
} }
/** /**
* Returns newest checkmark that has already been computed. * Returns newest checkmark that has already been computed.
* *
@ -360,66 +466,76 @@ public abstract class CheckmarkList
@Nullable @Nullable
protected abstract Checkmark getOldestComputed(); protected abstract Checkmark getOldestComputed();
private void computeNumerical(Repetition[] reps)
{ private void computeNumerical(Repetition[] reps) {
if (reps.length == 0) return; if (reps.length == 0) {
return;
}
Timestamp today = DateUtils.getToday(); Timestamp today = DateUtils.getToday();
Timestamp begin = reps[0].getTimestamp(); Timestamp begin = reps[0].getTimestamp();
int nDays = begin.daysUntil(today) + 1; int nDays = begin.daysUntil(today) + 1;
List<Checkmark> checkmarks = new ArrayList<>(nDays); List<Checkmark> checkmarks = new ArrayList<>(nDays);
for (int i = 0; i < nDays; i++) for (int i = 0; i < nDays; i++) {
checkmarks.add(new Checkmark(today.minus(i), 0)); checkmarks.add(new Checkmark(today.minus(i), 0, false));
}
for (Repetition rep : reps) for (Repetition rep : reps) {
{
int offset = rep.getTimestamp().daysUntil(today); int offset = rep.getTimestamp().daysUntil(today);
checkmarks.set(offset, new Checkmark(rep.getTimestamp(), rep.getValue())); checkmarks.set(offset, new Checkmark(rep.getTimestamp(), rep.getValue(), rep.isManualInput()));
} }
add(checkmarks); add(checkmarks);
} }
private void computeYesNo(Repetition[] reps)
{ private void computeYesNo(Repetition[] reps) {
ArrayList<Interval> intervals; ArrayList<Interval> intervals;
intervals = buildIntervals(habit.getFrequency(), reps); intervals = buildIntervals(habit.getFrequency(), reps);
snapIntervalsTogether(intervals); snapIntervalsTogether(intervals);
add(buildCheckmarksFromIntervals(reps, intervals)); add(buildCheckmarksFromIntervals(reps, intervals));
} }
public List<Checkmark> getAll() { public List<Checkmark> getAll() {
Repetition oldest = habit.getRepetitions().getOldest(); Repetition oldest = habit.getRepetitions().getOldest();
if(oldest == null) return new ArrayList<>(); if (oldest == null) {
return new ArrayList<>();
}
return getByInterval(oldest.getTimestamp(), DateUtils.getToday()); return getByInterval(oldest.getTimestamp(), DateUtils.getToday());
} }
static final class Interval
{ static final class Interval {
final Timestamp begin; final Timestamp begin;
final Timestamp center; final Timestamp center;
final Timestamp end; final Timestamp end;
Interval(Timestamp begin, Timestamp center, Timestamp end)
{ Interval(Timestamp begin, Timestamp center, Timestamp end) {
this.begin = begin; this.begin = begin;
this.center = center; this.center = center;
this.end = end; this.end = end;
} }
public int length() { public int length() {
return begin.daysUntil(end) + 1; return begin.daysUntil(end) + 1;
} }
@Override @Override
public boolean equals(Object o) public boolean equals(Object o) {
{ if (this == o) {
if (this == o) return true; return true;
}
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) {
return false;
}
Interval interval = (Interval) o; Interval interval = (Interval) o;
@ -430,9 +546,9 @@ public abstract class CheckmarkList
.isEquals(); .isEquals();
} }
@Override @Override
public int hashCode() public int hashCode() {
{
return new HashCodeBuilder(17, 37) return new HashCodeBuilder(17, 37)
.append(begin) .append(begin)
.append(center) .append(center)
@ -440,9 +556,9 @@ public abstract class CheckmarkList
.toHashCode(); .toHashCode();
} }
@Override @Override
public String toString() public String toString() {
{
return new ToStringBuilder(this, defaultToStringStyle()) return new ToStringBuilder(this, defaultToStringStyle())
.append("begin", begin) .append("begin", begin)
.append("center", center) .append("center", center)
@ -451,44 +567,47 @@ public abstract class CheckmarkList
} }
} }
@NonNull @NonNull
public List<Checkmark> groupBy(DateUtils.TruncateField field, int firstWeekday) public List<Checkmark> groupBy(DateUtils.TruncateField field, int firstWeekday) {
{
return groupBy(field, firstWeekday, 0); return groupBy(field, firstWeekday, 0);
} }
@NonNull @NonNull
public List<Checkmark> groupBy(DateUtils.TruncateField field, public List<Checkmark> groupBy(
int firstWeekday, DateUtils.TruncateField field,
int maxGroups) int firstWeekday,
{ int maxGroups
) {
List<Checkmark> checks = getAll(); List<Checkmark> checks = getAll();
int count = 0; int count = 0;
Timestamp[] truncatedTimestamps = new Timestamp[checks.size()]; Timestamp[] truncatedTimestamps = new Timestamp[checks.size()];
int[] values = new int[checks.size()]; int[] values = new int[checks.size()];
boolean[] manualInputs = new boolean[checks.size()];
for (Checkmark rep : checks) for (Checkmark rep : checks) {
{
Timestamp tt = rep.getTimestamp().truncate(field, firstWeekday); Timestamp tt = rep.getTimestamp().truncate(field, firstWeekday);
if (count == 0 || !truncatedTimestamps[count - 1].equals(tt)) if (count == 0 || !truncatedTimestamps[count - 1].equals(tt)) {
{ if (maxGroups > 0 && count >= maxGroups) {
if (maxGroups > 0 && count >= maxGroups) break; break;
}
truncatedTimestamps[count++] = tt; truncatedTimestamps[count++] = tt;
} }
if(habit.isNumerical()) if (habit.isNumerical()) {
values[count - 1] += rep.getValue(); values[count - 1] += rep.getValue();
else if(rep.getValue() == Checkmark.YES_MANUAL) } else if (rep.getValue() == Checkmark.YES && rep.isManualInput()) {
values[count - 1] += 1000; values[count - 1] += 1000;
}
manualInputs[count - 1] = rep.isManualInput();
} }
ArrayList<Checkmark> groupedCheckmarks = new ArrayList<>(); ArrayList<Checkmark> groupedCheckmarks = new ArrayList<>();
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++) {
{ Checkmark rep = new Checkmark(truncatedTimestamps[i], values[i], manualInputs[i]);
Checkmark rep = new Checkmark(truncatedTimestamps[i], values[i]);
groupedCheckmarks.add(rep); groupedCheckmarks.add(rep);
} }

@ -0,0 +1,103 @@
/*
* 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.core.models;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import static org.isoron.uhabits.core.utils.StringUtils.defaultToStringStyle;
/**
* A CheckmarkState represents the completion status of the habit.
*/
@ThreadSafe
public final class CheckmarkState
{
/**
* The value of the checkmark.
* <p>
* For boolean habits, this equals either NO, YES or SKIP.
* <p>
* For numerical habits, this number is stored in thousandths. That
* is, if the user enters value 1.50 on the app, it is stored as 1500.
*/
private final int value;
/**
* The indicator whether the checkmark value was added manually or computed by the algorithm
*/
private final boolean manualInput;
public CheckmarkState(int value, boolean manualInput)
{
this.value = value;
this.manualInput = manualInput;
}
@Override
public boolean equals(Object o)
{
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CheckmarkState checkmark = (CheckmarkState) o;
return new EqualsBuilder()
.append(value, checkmark.value)
.append(manualInput, checkmark.manualInput)
.isEquals();
}
public int getValue()
{
return value;
}
public boolean isManualInput()
{
return manualInput;
}
@Override
public int hashCode()
{
return new HashCodeBuilder(17, 37)
.append(value)
.append(manualInput)
.toHashCode();
}
@Override
public String toString()
{
return new ToStringBuilder(this, defaultToStringStyle())
.append("value", value)
.append("manualInput", manualInput)
.toString();
}
}

@ -19,6 +19,7 @@
package org.isoron.uhabits.core.models; package org.isoron.uhabits.core.models;
import androidx.annotation.NonNull;
import org.apache.commons.lang3.builder.*; import org.apache.commons.lang3.builder.*;
import static org.isoron.uhabits.core.models.Checkmark.*; import static org.isoron.uhabits.core.models.Checkmark.*;
@ -34,13 +35,10 @@ public final class Repetition
private final Timestamp timestamp; private final Timestamp timestamp;
/** /**
* The value of the repetition. * The state of the repetition.
*
* For boolean habits, this equals YES_MANUAL if performed or SKIP if skipped.
* For numerical habits, this number is stored in thousandths. That is, if the user enters
* value 1.50 on the app, it is here stored as 1500.
*/ */
private final int value; @NonNull
private final CheckmarkState state;
/** /**
* Creates a new repetition with given parameters. * Creates a new repetition with given parameters.
@ -50,19 +48,18 @@ public final class Repetition
* *
* @param timestamp the time this repetition occurred. * @param timestamp the time this repetition occurred.
*/ */
public Repetition(Timestamp timestamp, int value) public Repetition(Timestamp timestamp, int value, boolean manualInput)
{ {
this.timestamp = timestamp; this.timestamp = timestamp;
this.value = value; this.state = new CheckmarkState(value, manualInput);
} }
public static int nextToggleValue(int value) public static int nextToggleValue(int value)
{ {
switch(value) { switch(value) {
case NO: case NO:
case YES_AUTO: return YES;
return YES_MANUAL; case YES:
case YES_MANUAL:
return SKIP; return SKIP;
default: default:
case SKIP: case SKIP:
@ -81,7 +78,8 @@ public final class Repetition
return new EqualsBuilder() return new EqualsBuilder()
.append(timestamp, that.timestamp) .append(timestamp, that.timestamp)
.append(value, that.value) .append(getValue(), that.getValue())
.append(isManualInput(), that.isManualInput())
.isEquals(); .isEquals();
} }
@ -90,17 +88,30 @@ public final class Repetition
return timestamp; return timestamp;
} }
public CheckmarkState getState()
{
return state;
}
public int getValue() public int getValue()
{ {
return value; return state.getValue();
} }
public boolean isManualInput()
{
return state.isManualInput();
}
@Override @Override
public int hashCode() public int hashCode()
{ {
return new HashCodeBuilder(17, 37) return new HashCodeBuilder(17, 37)
.append(timestamp) .append(timestamp)
.append(value) .append(getValue())
.append(isManualInput())
.toHashCode(); .toHashCode();
} }
@ -109,7 +120,8 @@ public final class Repetition
{ {
return new ToStringBuilder(this, defaultToStringStyle()) return new ToStringBuilder(this, defaultToStringStyle())
.append("timestamp", timestamp) .append("timestamp", timestamp)
.append("value", value) .append("value", getValue())
.append("manualInput", isManualInput())
.toString(); .toString();
} }
} }

@ -140,7 +140,7 @@ public abstract class RepetitionList
for (Repetition r : reps) for (Repetition r : reps)
{ {
if (!habit.isNumerical() && r.getValue() != Checkmark.YES_MANUAL) if (!habit.isNumerical() && !(r.getValue() == Checkmark.YES && r.isManualInput()))
continue; continue;
Calendar date = r.getTimestamp().toCalendar(); Calendar date = r.getTimestamp().toCalendar();
@ -197,7 +197,7 @@ public abstract class RepetitionList
if (rep != null) remove(rep); if (rep != null) remove(rep);
else else
{ {
rep = new Repetition(timestamp, Checkmark.YES_MANUAL); rep = new Repetition(timestamp, Checkmark.YES, true);
add(rep); add(rep);
} }
@ -207,11 +207,11 @@ public abstract class RepetitionList
public abstract long getTotalCount(); public abstract long getTotalCount();
public void toggle(Timestamp timestamp, int value) public void toggle(Timestamp timestamp, int value, boolean manualInput)
{ {
Repetition rep = getByTimestamp(timestamp); Repetition rep = getByTimestamp(timestamp);
if (rep != null) remove(rep); if (rep != null) remove(rep);
add(new Repetition(timestamp, value)); add(new Repetition(timestamp, value, manualInput));
habit.invalidateNewerThan(timestamp); habit.invalidateNewerThan(timestamp);
} }

@ -68,7 +68,7 @@ public class MemoryCheckmarkList extends CheckmarkList
{ {
Timestamp t = to.minus(i); Timestamp t = to.minus(i);
if(t.isNewerThan(newestComputed) || t.isOlderThan(oldestComputed)) if(t.isNewerThan(newestComputed) || t.isOlderThan(oldestComputed))
filtered.add(new Checkmark(t, Checkmark.NO)); filtered.add(new Checkmark(t, Checkmark.NO, false));
else else
filtered.add(list.get(t.daysUntil(newestComputed))); filtered.add(list.get(t.daysUntil(newestComputed)));
} }

@ -123,7 +123,7 @@ public class MemoryRepetitionList extends RepetitionList
{ {
int count = 0; int count = 0;
for (Repetition rep : list) for (Repetition rep : list)
if (rep.getValue() == Checkmark.YES_MANUAL) if (rep.getValue() == Checkmark.YES && rep.isManualInput())
count++; count++;
return count; return count;
} }

@ -41,17 +41,34 @@ public class RepetitionRecord
@Column @Column
public Integer value; public Integer value;
@Column
public Integer manualInput;
@Column @Column
public Long id; public Long id;
public boolean isManualInput() {
return manualInput == 1;
}
public void copyFrom(Repetition repetition) public void copyFrom(Repetition repetition)
{ {
timestamp = repetition.getTimestamp().getUnixTime(); timestamp = repetition.getTimestamp().getUnixTime();
value = repetition.getValue(); value = repetition.getValue();
manualInput = convertToInt(repetition);
} }
private int convertToInt(final Repetition repetition) {
if(repetition.isManualInput()) {
return 1;
} else {
return 0;
}
}
public Repetition toRepetition() public Repetition toRepetition()
{ {
return new Repetition(new Timestamp(timestamp), value); return new Repetition(new Timestamp(timestamp), value, isManualInput());
} }
} }

@ -87,7 +87,7 @@ public class HabitFixtures
for (int i = 0; i < times.length; i++) for (int i = 0; i < times.length; i++)
{ {
Timestamp timestamp = today.minus(times[i]); Timestamp timestamp = today.minus(times[i]);
habit.getRepetitions().add(new Repetition(timestamp, values[i])); habit.getRepetitions().add(new Repetition(timestamp, values[i], true));
} }
return habit; return habit;
@ -123,7 +123,7 @@ public class HabitFixtures
for (int i = 0; i < times.length; i++) for (int i = 0; i < times.length; i++)
{ {
Timestamp timestamp = reference.minus(times[i]); Timestamp timestamp = reference.minus(times[i]);
habit.getRepetitions().add(new Repetition(timestamp, values[i])); habit.getRepetitions().add(new Repetition(timestamp, values[i], true));
} }
return habit; return habit;

@ -93,9 +93,9 @@ public class HabitCardListCache implements CommandRunner.Listener
if (currentFetchTask != null) currentFetchTask.cancel(); if (currentFetchTask != null) currentFetchTask.cancel();
} }
public synchronized int[] getCheckmarks(long habitId) public synchronized CheckmarkState[] getCheckmarkStates(long habitId)
{ {
return data.checkmarks.get(habitId); return data.checkmarkStates.get(habitId);
} }
/** /**
@ -165,7 +165,7 @@ public class HabitCardListCache implements CommandRunner.Listener
int position = data.habits.indexOf(h); int position = data.habits.indexOf(h);
data.habits.remove(position); data.habits.remove(position);
data.id_to_habit.remove(id); data.id_to_habit.remove(id);
data.checkmarks.remove(id); data.checkmarkStates.remove(id);
data.scores.remove(id); data.scores.remove(id);
listener.onItemRemoved(position); listener.onItemRemoved(position);
@ -240,7 +240,7 @@ public class HabitCardListCache implements CommandRunner.Listener
public final List<Habit> habits; public final List<Habit> habits;
@NonNull @NonNull
public final HashMap<Long, int[]> checkmarks; public final HashMap<Long, CheckmarkState[]> checkmarkStates;
@NonNull @NonNull
public final HashMap<Long, Double> scores; public final HashMap<Long, Double> scores;
@ -252,7 +252,7 @@ public class HabitCardListCache implements CommandRunner.Listener
{ {
id_to_habit = new HashMap<>(); id_to_habit = new HashMap<>();
habits = new LinkedList<>(); habits = new LinkedList<>();
checkmarks = new HashMap<>(); checkmarkStates = new HashMap<>();
scores = new HashMap<>(); scores = new HashMap<>();
} }
@ -260,13 +260,13 @@ public class HabitCardListCache implements CommandRunner.Listener
{ {
if (oldData == null) throw new NullPointerException(); if (oldData == null) throw new NullPointerException();
int[] empty = new int[checkmarkCount]; CheckmarkState[] empty = new CheckmarkState[checkmarkCount];
for (Long id : id_to_habit.keySet()) for (Long id : id_to_habit.keySet())
{ {
if (oldData.checkmarks.containsKey(id)) if (oldData.checkmarkStates.containsKey(id))
checkmarks.put(id, oldData.checkmarks.get(id)); checkmarkStates.put(id, oldData.checkmarkStates.get(id));
else checkmarks.put(id, empty); else checkmarkStates.put(id, empty);
} }
} }
@ -346,9 +346,9 @@ public class HabitCardListCache implements CommandRunner.Listener
if (targetId != null && !targetId.equals(id)) continue; if (targetId != null && !targetId.equals(id)) continue;
newData.scores.put(id, habit.getScores().getTodayValue()); newData.scores.put(id, habit.getScores().getTodayValue());
newData.checkmarks.put( newData.checkmarkStates.put(
id, id,
habit.getCheckmarks().getValues(dateFrom, dateTo)); habit.getCheckmarks().getCheckmarkStates(dateFrom, dateTo));
runner.publishProgress(this, position); runner.publishProgress(this, position);
} }
@ -381,7 +381,7 @@ public class HabitCardListCache implements CommandRunner.Listener
data.habits.add(position, habit); data.habits.add(position, habit);
data.id_to_habit.put(id, habit); data.id_to_habit.put(id, habit);
data.scores.put(id, newData.scores.get(id)); data.scores.put(id, newData.scores.get(id));
data.checkmarks.put(id, newData.checkmarks.get(id)); data.checkmarkStates.put(id, newData.checkmarkStates.get(id));
listener.onItemInserted(position); listener.onItemInserted(position);
} }
@ -398,10 +398,10 @@ public class HabitCardListCache implements CommandRunner.Listener
private synchronized void performUpdate(long id, int position) private synchronized void performUpdate(long id, int position)
{ {
double oldScore = data.scores.get(id); double oldScore = data.scores.get(id);
int[] oldCheckmarks = data.checkmarks.get(id); CheckmarkState[] oldCheckmarks = data.checkmarkStates.get(id);
double newScore = newData.scores.get(id); double newScore = newData.scores.get(id);
int[] newCheckmarks = newData.checkmarks.get(id); CheckmarkState[] newCheckmarks = newData.checkmarkStates.get(id);
boolean unchanged = true; boolean unchanged = true;
if (oldScore != newScore) unchanged = false; if (oldScore != newScore) unchanged = false;
@ -409,7 +409,7 @@ public class HabitCardListCache implements CommandRunner.Listener
if (unchanged) return; if (unchanged) return;
data.scores.put(id, newScore); data.scores.put(id, newScore);
data.checkmarks.put(id, newCheckmarks); data.checkmarkStates.put(id, newCheckmarks);
listener.onItemChanged(position); listener.onItemChanged(position);
} }

@ -87,7 +87,7 @@ public class ListHabitsBehavior
{ {
newValue = Math.round(newValue * 1000); newValue = Math.round(newValue * 1000);
commandRunner.execute( commandRunner.execute(
new CreateRepetitionCommand(habitList, habit, timestamp, (int) newValue), new CreateRepetitionCommand(habitList, habit, timestamp, (int) newValue, true),
habit.getId()); habit.getId());
}); });
} }
@ -149,10 +149,10 @@ public class ListHabitsBehavior
if (prefs.isFirstRun()) onFirstRun(); if (prefs.isFirstRun()) onFirstRun();
} }
public void onToggle(@NonNull Habit habit, Timestamp timestamp, int value) public void onToggle(@NonNull Habit habit, Timestamp timestamp, int value, boolean manualInput)
{ {
commandRunner.execute( commandRunner.execute(
new CreateRepetitionCommand(habitList, habit, timestamp, value), new CreateRepetitionCommand(habitList, habit, timestamp, value, manualInput),
habit.getId()); habit.getId());
} }

@ -57,7 +57,7 @@ public class ShowHabitBehavior
screen.showEditHistoryScreen(); screen.showEditHistoryScreen();
} }
public void onToggleCheckmark(Timestamp timestamp, int value) public void onToggleCheckmark(Timestamp timestamp, int value, boolean manualInput)
{ {
if (habit.isNumerical()) { if (habit.isNumerical()) {
CheckmarkList checkmarks = habit.getCheckmarks(); CheckmarkList checkmarks = habit.getCheckmarks();
@ -67,12 +67,12 @@ public class ShowHabitBehavior
{ {
newValue = Math.round(newValue * 1000); newValue = Math.round(newValue * 1000);
commandRunner.execute( commandRunner.execute(
new CreateRepetitionCommand(habitList, habit, timestamp, (int) newValue), new CreateRepetitionCommand(habitList, habit, timestamp, (int) newValue, manualInput),
habit.getId()); habit.getId());
}); });
} else { } else {
commandRunner.execute( commandRunner.execute(
new CreateRepetitionCommand(habitList, habit, timestamp, value), null); new CreateRepetitionCommand(habitList, habit, timestamp, value, manualInput), null);
} }
} }

@ -110,11 +110,11 @@ public class ShowHabitMenuBehavior
if (i % 7 == 0) strength = max(0, min(100, strength + 10 * random.nextGaussian())); if (i % 7 == 0) strength = max(0, min(100, strength + 10 * random.nextGaussian()));
if (random.nextInt(100) > strength) continue; if (random.nextInt(100) > strength) continue;
int value = Checkmark.YES_MANUAL; int value = Checkmark.YES;
if (habit.isNumerical()) if (habit.isNumerical())
value = (int) (1000 + 250 * random.nextGaussian() * strength / 100) * 1000; value = (int) (1000 + 250 * random.nextGaussian() * strength / 100) * 1000;
habit.getRepetitions().add(new Repetition(DateUtils.getToday().minus(i), value)); habit.getRepetitions().add(new Repetition(DateUtils.getToday().minus(i), value, true));
} }
habit.invalidateNewerThan(Timestamp.ZERO); habit.invalidateNewerThan(Timestamp.ZERO);

@ -52,7 +52,7 @@ public class WidgetBehavior
notificationTray.cancel(habit); notificationTray.cancel(habit);
Repetition rep = habit.getRepetitions().getByTimestamp(timestamp); Repetition rep = habit.getRepetitions().getByTimestamp(timestamp);
if (rep != null) return; if (rep != null) return;
performToggle(habit, timestamp, Checkmark.YES_MANUAL); performToggle(habit, timestamp, Checkmark.YES);
} }
public void onRemoveRepetition(@NonNull Habit habit, Timestamp timestamp) public void onRemoveRepetition(@NonNull Habit habit, Timestamp timestamp)
@ -66,29 +66,34 @@ public class WidgetBehavior
public void onToggleRepetition(@NonNull Habit habit, Timestamp timestamp) public void onToggleRepetition(@NonNull Habit habit, Timestamp timestamp)
{ {
Repetition previous = habit.getRepetitions().getByTimestamp(timestamp); Repetition previous = habit.getRepetitions().getByTimestamp(timestamp);
if(previous == null) performToggle(habit, timestamp, Checkmark.YES_MANUAL); if(previous == null) performToggle(habit, timestamp, Checkmark.YES);
else performToggle(habit, timestamp, Repetition.nextToggleValue(previous.getValue())); else performToggle(habit, timestamp, Repetition.nextToggleValue(previous.getValue()));
} }
private void performToggle(@NonNull Habit habit, Timestamp timestamp, int value) private void performToggle(@NonNull Habit habit, Timestamp timestamp, int value)
{ {
final boolean manualInput = true;
commandRunner.execute( commandRunner.execute(
new CreateRepetitionCommand(habitList, habit, timestamp, value), new CreateRepetitionCommand(habitList, habit, timestamp, value, manualInput),
habit.getId()); habit.getId());
} }
public void setNumericValue(@NonNull Habit habit, Timestamp timestamp, int newValue) { public void setNumericValue(@NonNull Habit habit, Timestamp timestamp, int newValue)
{
final boolean manualInput = true;
commandRunner.execute( commandRunner.execute(
new CreateRepetitionCommand(habitList, habit, timestamp, newValue), new CreateRepetitionCommand(habitList, habit, timestamp, newValue, manualInput),
habit.getId()); habit.getId());
} }
public void onIncrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) { public void onIncrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount)
{
int currentValue = habit.getCheckmarks().getValues(timestamp, timestamp)[0]; int currentValue = habit.getCheckmarks().getValues(timestamp, timestamp)[0];
setNumericValue(habit, timestamp, currentValue + amount); setNumericValue(habit, timestamp, currentValue + amount);
} }
public void onDecrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount) { public void onDecrement(@NotNull Habit habit, @NotNull Timestamp timestamp, int amount)
{
int currentValue = habit.getCheckmarks().getValues(timestamp, timestamp)[0]; int currentValue = habit.getCheckmarks().getValues(timestamp, timestamp)[0];
setNumericValue(habit, timestamp, currentValue - amount); setNumericValue(habit, timestamp, currentValue - amount);
} }

@ -0,0 +1,4 @@
alter table Repetitions add column manualInput INTEGER not null default 0;
update repetitions set manualInput=1 where value>1 and (select type from habits where id=repetitions.habit)=0; -- yes/no habit
update repetitions set manualInput=1 where value>0 and (select type from habits where id=repetitions.habit)=1; -- numerical habit

@ -38,6 +38,7 @@ import java.io.*;
import java.sql.*; import java.sql.*;
import java.util.*; import java.util.*;
import static org.isoron.uhabits.core.Config.DATABASE_VERSION;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
@ -125,7 +126,7 @@ public class BaseUnitTest
DriverManager.getConnection("jdbc:sqlite::memory:")); DriverManager.getConnection("jdbc:sqlite::memory:"));
db.execute("pragma user_version=8;"); db.execute("pragma user_version=8;");
MigrationHelper helper = new MigrationHelper(db); MigrationHelper helper = new MigrationHelper(db);
helper.migrateTo(23); helper.migrateTo(DATABASE_VERSION);
return db; return db;
} }
catch (SQLException e) catch (SQLException e)

@ -96,13 +96,14 @@ public class CommandParserTest extends BaseUnitTest
public void testDecodeCreateRepCommand() throws JSONException public void testDecodeCreateRepCommand() throws JSONException
{ {
CreateRepetitionCommand original, decoded; CreateRepetitionCommand original, decoded;
original = new CreateRepetitionCommand(habitList, habit, Timestamp.ZERO.plus(100), 5); original = new CreateRepetitionCommand(habitList, habit, Timestamp.ZERO.plus(100), 5, true);
decoded = (CreateRepetitionCommand) parser.parse(original.toJson()); decoded = (CreateRepetitionCommand) parser.parse(original.toJson());
MatcherAssert.assertThat(decoded.getId(), equalTo(original.getId())); MatcherAssert.assertThat(decoded.getId(), equalTo(original.getId()));
MatcherAssert.assertThat(decoded.timestamp, equalTo(original MatcherAssert.assertThat(decoded.timestamp, equalTo(original
.timestamp)); .timestamp));
MatcherAssert.assertThat(decoded.value, equalTo(original.value)); MatcherAssert.assertThat(decoded.value, equalTo(original.value));
MatcherAssert.assertThat(decoded.manualInput, equalTo(original.manualInput));
MatcherAssert.assertThat(decoded.habit, equalTo(original.habit)); MatcherAssert.assertThat(decoded.habit, equalTo(original.habit));
} }
@ -146,4 +147,4 @@ public class CommandParserTest extends BaseUnitTest
MatcherAssert.assertThat(decoded.getId(), equalTo(original.getId())); MatcherAssert.assertThat(decoded.getId(), equalTo(original.getId()));
MatcherAssert.assertThat(decoded.selected, equalTo(original.selected)); MatcherAssert.assertThat(decoded.selected, equalTo(original.selected));
} }
} }

@ -47,7 +47,7 @@ public class CreateRepetitionCommandTest extends BaseUnitTest
habitList.add(habit); habitList.add(habit);
today = DateUtils.getToday(); today = DateUtils.getToday();
command = new CreateRepetitionCommand(habitList, habit, today, 100); command = new CreateRepetitionCommand(habitList, habit, today, 100, true);
} }
@Test @Test
@ -57,17 +57,20 @@ public class CreateRepetitionCommandTest extends BaseUnitTest
Repetition rep = reps.getByTimestamp(today); Repetition rep = reps.getByTimestamp(today);
assertNotNull(rep); assertNotNull(rep);
assertEquals(YES_MANUAL, rep.getValue()); assertEquals(YES, rep.getValue());
assertTrue(rep.isManualInput());
command.execute(); command.execute();
rep = reps.getByTimestamp(today); rep = reps.getByTimestamp(today);
assertNotNull(rep); assertNotNull(rep);
assertEquals(100, rep.getValue()); assertEquals(100, rep.getValue());
assertTrue(rep.isManualInput());
command.undo(); command.undo();
rep = reps.getByTimestamp(today); rep = reps.getByTimestamp(today);
assertNotNull(rep); assertNotNull(rep);
assertEquals(YES_MANUAL, rep.getValue()); assertEquals(YES, rep.getValue());
assertTrue(rep.isManualInput());
} }
@Test @Test

@ -68,10 +68,10 @@ public class CheckmarkListTest extends BaseUnitTest
public void test_buildCheckmarksFromIntervals_1() throws Exception public void test_buildCheckmarksFromIntervals_1() throws Exception
{ {
Repetition reps[] = new Repetition[]{ Repetition reps[] = new Repetition[]{
new Repetition(day(10), YES_MANUAL), new Repetition(day(10), YES, true),
new Repetition(day(5), YES_MANUAL), new Repetition(day(5), YES, true),
new Repetition(day(2), YES_MANUAL), new Repetition(day(2), YES, true),
new Repetition(day(1), YES_MANUAL), new Repetition(day(1), YES, true),
}; };
ArrayList<CheckmarkList.Interval> intervals = new ArrayList<>(); ArrayList<CheckmarkList.Interval> intervals = new ArrayList<>();
@ -80,17 +80,17 @@ public class CheckmarkListTest extends BaseUnitTest
intervals.add(new CheckmarkList.Interval(day(2), day(2), day(1))); intervals.add(new CheckmarkList.Interval(day(2), day(2), day(1)));
List<Checkmark> expected = new ArrayList<>(); List<Checkmark> expected = new ArrayList<>();
expected.add(new Checkmark(day(0), NO)); expected.add(new Checkmark(day(0), NO, false));
expected.add(new Checkmark(day(1), YES_MANUAL)); expected.add(new Checkmark(day(1), YES, true));
expected.add(new Checkmark(day(2), YES_MANUAL)); expected.add(new Checkmark(day(2), YES, true));
expected.add(new Checkmark(day(3), NO)); expected.add(new Checkmark(day(3), NO, false));
expected.add(new Checkmark(day(4), YES_AUTO)); expected.add(new Checkmark(day(4), YES, false));
expected.add(new Checkmark(day(5), YES_MANUAL)); expected.add(new Checkmark(day(5), YES, true));
expected.add(new Checkmark(day(6), YES_AUTO)); expected.add(new Checkmark(day(6), YES, false));
expected.add(new Checkmark(day(7), NO)); expected.add(new Checkmark(day(7), NO, false));
expected.add(new Checkmark(day(8), YES_AUTO)); expected.add(new Checkmark(day(8), YES, false));
expected.add(new Checkmark(day(9), YES_AUTO)); expected.add(new Checkmark(day(9), YES, false));
expected.add(new Checkmark(day(10), YES_MANUAL)); expected.add(new Checkmark(day(10), YES, true));
List<Checkmark> actual = List<Checkmark> actual =
CheckmarkList.buildCheckmarksFromIntervals(reps, intervals); CheckmarkList.buildCheckmarksFromIntervals(reps, intervals);
@ -101,14 +101,14 @@ public class CheckmarkListTest extends BaseUnitTest
public void test_buildCheckmarksFromIntervals_2() throws Exception public void test_buildCheckmarksFromIntervals_2() throws Exception
{ {
Repetition reps[] = new Repetition[]{ Repetition reps[] = new Repetition[]{
new Repetition(day(0), YES_MANUAL), new Repetition(day(0), YES, true),
}; };
ArrayList<CheckmarkList.Interval> intervals = new ArrayList<>(); ArrayList<CheckmarkList.Interval> intervals = new ArrayList<>();
intervals.add(new CheckmarkList.Interval(day(0), day(0), day(-10))); intervals.add(new CheckmarkList.Interval(day(0), day(0), day(-10)));
List<Checkmark> expected = new ArrayList<>(); List<Checkmark> expected = new ArrayList<>();
expected.add(new Checkmark(day(0), YES_MANUAL)); expected.add(new Checkmark(day(0), YES, true));
List<Checkmark> actual = List<Checkmark> actual =
CheckmarkList.buildCheckmarksFromIntervals(reps, intervals); CheckmarkList.buildCheckmarksFromIntervals(reps, intervals);
@ -119,9 +119,9 @@ public class CheckmarkListTest extends BaseUnitTest
public void test_buildIntervals_1() throws Exception public void test_buildIntervals_1() throws Exception
{ {
Repetition reps[] = new Repetition[]{ Repetition reps[] = new Repetition[]{
new Repetition(day(23), YES_MANUAL), new Repetition(day(23), YES, true),
new Repetition(day(18), YES_MANUAL), new Repetition(day(18), YES, true),
new Repetition(day(8), YES_MANUAL), new Repetition(day(8), YES, true),
}; };
ArrayList<CheckmarkList.Interval> expected = new ArrayList<>(); ArrayList<CheckmarkList.Interval> expected = new ArrayList<>();
@ -138,9 +138,9 @@ public class CheckmarkListTest extends BaseUnitTest
public void test_buildIntervals_2() throws Exception public void test_buildIntervals_2() throws Exception
{ {
Repetition reps[] = new Repetition[]{ Repetition reps[] = new Repetition[]{
new Repetition(day(23), YES_MANUAL), new Repetition(day(23), YES, true),
new Repetition(day(18), YES_MANUAL), new Repetition(day(18), YES, true),
new Repetition(day(8), YES_MANUAL), new Repetition(day(8), YES, true),
}; };
ArrayList<CheckmarkList.Interval> expected = new ArrayList<>(); ArrayList<CheckmarkList.Interval> expected = new ArrayList<>();
@ -157,11 +157,11 @@ public class CheckmarkListTest extends BaseUnitTest
public void test_buildIntervals_3() throws Exception public void test_buildIntervals_3() throws Exception
{ {
Repetition reps[] = new Repetition[]{ Repetition reps[] = new Repetition[]{
new Repetition(day(23), YES_MANUAL), new Repetition(day(23), YES, true),
new Repetition(day(22), YES_MANUAL), new Repetition(day(22), YES, true),
new Repetition(day(18), YES_MANUAL), new Repetition(day(18), YES, true),
new Repetition(day(15), YES_MANUAL), new Repetition(day(15), YES, true),
new Repetition(day(8), YES_MANUAL), new Repetition(day(8), YES, true),
}; };
ArrayList<CheckmarkList.Interval> expected = new ArrayList<>(); ArrayList<CheckmarkList.Interval> expected = new ArrayList<>();
@ -180,9 +180,9 @@ public class CheckmarkListTest extends BaseUnitTest
public void test_buildIntervals_4() throws Exception public void test_buildIntervals_4() throws Exception
{ {
Repetition[] reps = new Repetition[]{ Repetition[] reps = new Repetition[]{
new Repetition(day(30), YES_MANUAL), new Repetition(day(30), YES, true),
new Repetition(day(20), SKIP), new Repetition(day(20), SKIP, true),
new Repetition(day(10), YES_MANUAL), new Repetition(day(10), YES, true),
}; };
ArrayList<CheckmarkList.Interval> expected = new ArrayList<>(); ArrayList<CheckmarkList.Interval> expected = new ArrayList<>();
@ -199,17 +199,17 @@ public class CheckmarkListTest extends BaseUnitTest
{ {
travelInTime(-3); travelInTime(-3);
int[] expectedValues = { CheckmarkState[] expectedValues = {
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL, new CheckmarkState(YES, true),
YES_AUTO, new CheckmarkState(YES, false),
YES_AUTO, new CheckmarkState(YES, false),
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL new CheckmarkState(YES, true)
}; };
int[] actualValues = nonDailyHabit.getCheckmarks().getAllValues(); CheckmarkState[] actualValues = nonDailyHabit.getCheckmarks().getAllStates();
assertThat(actualValues, equalTo(expectedValues)); assertThat(actualValues, equalTo(expectedValues));
} }
@ -219,23 +219,23 @@ public class CheckmarkListTest extends BaseUnitTest
{ {
travelInTime(3); travelInTime(3);
int[] expectedValues = { CheckmarkState[] expectedValues = {
NO, new CheckmarkState(NO, false),
NO, new CheckmarkState(NO, false),
NO, new CheckmarkState(NO, false),
YES_MANUAL, new CheckmarkState(YES, true),
NO, new CheckmarkState(NO, false),
YES_AUTO, new CheckmarkState(YES, false),
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL, new CheckmarkState(YES, true),
YES_AUTO, new CheckmarkState(YES, false),
YES_AUTO, new CheckmarkState(YES, false),
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL new CheckmarkState(YES, true)
}; };
int[] actualValues = nonDailyHabit.getCheckmarks().getAllValues(); CheckmarkState[] actualValues = nonDailyHabit.getCheckmarks().getAllStates();
assertThat(actualValues, equalTo(expectedValues)); assertThat(actualValues, equalTo(expectedValues));
} }
@ -243,8 +243,8 @@ public class CheckmarkListTest extends BaseUnitTest
@Test @Test
public void test_getAllValues_withEmptyHabit() public void test_getAllValues_withEmptyHabit()
{ {
int[] expectedValues = new int[0]; CheckmarkState[] expectedValues = new CheckmarkState[0];
int[] actualValues = emptyHabit.getCheckmarks().getAllValues(); CheckmarkState[] actualValues = emptyHabit.getCheckmarks().getAllStates();
assertThat(actualValues, equalTo(expectedValues)); assertThat(actualValues, equalTo(expectedValues));
} }
@ -252,20 +252,20 @@ public class CheckmarkListTest extends BaseUnitTest
@Test @Test
public void test_getAllValues_withNonDailyHabit() public void test_getAllValues_withNonDailyHabit()
{ {
int[] expectedValues = { CheckmarkState[] expectedValues = {
YES_MANUAL, new CheckmarkState(YES, true),
NO, new CheckmarkState(NO, false),
YES_AUTO, new CheckmarkState(YES, false),
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL, new CheckmarkState(YES, true),
YES_AUTO, new CheckmarkState(YES, false),
YES_AUTO, new CheckmarkState(YES, false),
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL new CheckmarkState(YES, true)
}; };
int[] actualValues = nonDailyHabit.getCheckmarks().getAllValues(); CheckmarkState[] actualValues = nonDailyHabit.getCheckmarks().getAllStates();
assertThat(actualValues, equalTo(expectedValues)); assertThat(actualValues, equalTo(expectedValues));
} }
@ -276,9 +276,9 @@ public class CheckmarkListTest extends BaseUnitTest
CheckmarkList checkmarks = numericalHabit.getCheckmarks(); CheckmarkList checkmarks = numericalHabit.getCheckmarks();
List<Checkmark> expected = List<Checkmark> expected =
Arrays.asList(new Checkmark(day(1), 200), new Checkmark(day(2), 0), Arrays.asList(new Checkmark(day(1), 200, true), new Checkmark(day(2), 0, false),
new Checkmark(day(3), 300), new Checkmark(day(4), 0), new Checkmark(day(3), 300, true), new Checkmark(day(4), 0, false),
new Checkmark(day(5), 400)); new Checkmark(day(5), 400, true));
List<Checkmark> actual = checkmarks.getByInterval(day(5), day(1)); List<Checkmark> actual = checkmarks.getByInterval(day(5), day(1));
assertThat(actual, equalTo(expected)); assertThat(actual, equalTo(expected));
@ -293,7 +293,8 @@ public class CheckmarkListTest extends BaseUnitTest
assertThat(checkmarks.getTodayValue(), equalTo(NO)); assertThat(checkmarks.getTodayValue(), equalTo(NO));
travelInTime(0); travelInTime(0);
assertThat(checkmarks.getTodayValue(), equalTo(YES_MANUAL)); assertThat(checkmarks.getTodayValue(), equalTo(YES));
assertThat(checkmarks.getTodayState().isManualInput(), equalTo(true));
travelInTime(1); travelInTime(1);
assertThat(checkmarks.getTodayValue(), equalTo(NO)); assertThat(checkmarks.getTodayValue(), equalTo(NO));
@ -302,10 +303,10 @@ public class CheckmarkListTest extends BaseUnitTest
@Test @Test
public void test_getValues_withInvalidInterval() public void test_getValues_withInvalidInterval()
{ {
int values[] = nonDailyHabit CheckmarkState[] values = nonDailyHabit
.getCheckmarks() .getCheckmarks()
.getValues(new Timestamp(0L).plus(100), new Timestamp(0L)); .getCheckmarkStates(new Timestamp(0L).plus(100), new Timestamp(0L));
assertThat(values, equalTo(new int[0])); assertThat(values, equalTo(new CheckmarkState[0]));
} }
@Test @Test
@ -314,21 +315,21 @@ public class CheckmarkListTest extends BaseUnitTest
Timestamp from = today.minus(15); Timestamp from = today.minus(15);
Timestamp to = today.minus(5); Timestamp to = today.minus(5);
int[] expectedValues = { CheckmarkState[] expectedValues = {
YES_MANUAL, new CheckmarkState(YES, true),
YES_AUTO, new CheckmarkState(YES, false),
YES_AUTO, new CheckmarkState(YES, false),
YES_MANUAL, new CheckmarkState(YES, true),
YES_MANUAL, new CheckmarkState(YES, true),
NO, new CheckmarkState(NO, false),
NO, new CheckmarkState(NO, false),
NO, new CheckmarkState(NO, false),
NO, new CheckmarkState(NO, false),
NO, new CheckmarkState(NO, false),
NO new CheckmarkState(NO, false)
}; };
int[] actualValues = nonDailyHabit.getCheckmarks().getValues(from, to); CheckmarkState[] actualValues = nonDailyHabit.getCheckmarks().getCheckmarkStates(from, to);
assertThat(actualValues, equalTo(expectedValues)); assertThat(actualValues, equalTo(expectedValues));
} }
@ -369,10 +370,10 @@ public class CheckmarkListTest extends BaseUnitTest
@Test @Test
public void test_writeCSV() throws IOException public void test_writeCSV() throws IOException
{ {
String expectedCSV = "2015-01-25,2\n2015-01-24,0\n2015-01-23,1\n" + String expectedCSV = "2015-01-25,1,1\n2015-01-24,0,0\n2015-01-23,1,0\n" +
"2015-01-22,2\n2015-01-21,2\n2015-01-20,2\n" + "2015-01-22,1,1\n2015-01-21,1,1\n2015-01-20,1,1\n" +
"2015-01-19,1\n2015-01-18,1\n2015-01-17,2\n" + "2015-01-19,1,0\n2015-01-18,1,0\n2015-01-17,1,1\n" +
"2015-01-16,2\n"; "2015-01-16,1,1\n";
StringWriter writer = new StringWriter(); StringWriter writer = new StringWriter();
@ -396,9 +397,9 @@ public class CheckmarkListTest extends BaseUnitTest
public void testToString() throws Exception public void testToString() throws Exception
{ {
Timestamp t = Timestamp.ZERO.plus(100); Timestamp t = Timestamp.ZERO.plus(100);
Checkmark checkmark = new Checkmark(t, 2); Checkmark checkmark = new Checkmark(t, 2, true);
assertThat(checkmark.toString(), assertThat(checkmark.toString(),
equalTo("{timestamp: 1970-04-11, value: 2}")); equalTo("{timestamp: 1970-04-11, value: 2, manualInput: true}"));
CheckmarkList.Interval interval = CheckmarkList.Interval interval =
new CheckmarkList.Interval(t, t.plus(1), t.plus(2)); new CheckmarkList.Interval(t, t.plus(1), t.plus(2));
@ -422,22 +423,22 @@ public class CheckmarkListTest extends BaseUnitTest
List<Checkmark> byMonth = checkmarks.groupBy(MONTH, Calendar.SATURDAY); List<Checkmark> byMonth = checkmarks.groupBy(MONTH, Calendar.SATURDAY);
assertThat(byMonth.size(), equalTo(25)); // from 2013-01-01 to 2015-01-01 assertThat(byMonth.size(), equalTo(25)); // from 2013-01-01 to 2015-01-01
assertThat(byMonth.get(0), equalTo(new Checkmark(timestamp(2015, JANUARY, 1), 0))); assertThat(byMonth.get(0), equalTo(new Checkmark(timestamp(2015, JANUARY, 1), 0, false)));
assertThat(byMonth.get(6), equalTo(new Checkmark(timestamp(2014, JULY, 1), 0))); assertThat(byMonth.get(6), equalTo(new Checkmark(timestamp(2014, JULY, 1), 0, false)));
assertThat(byMonth.get(12), equalTo(new Checkmark(timestamp(2014, JANUARY, 1), 1706))); assertThat(byMonth.get(12), equalTo(new Checkmark(timestamp(2014, JANUARY, 1), 1706, true)));
assertThat(byMonth.get(18), equalTo(new Checkmark(timestamp(2013, JULY, 1), 1379))); assertThat(byMonth.get(18), equalTo(new Checkmark(timestamp(2013, JULY, 1), 1379, true)));
List<Checkmark> byQuarter = checkmarks.groupBy(QUARTER, Calendar.SATURDAY); List<Checkmark> byQuarter = checkmarks.groupBy(QUARTER, Calendar.SATURDAY);
assertThat(byQuarter.size(), equalTo(9)); // from 2013-Q1 to 2015-Q1 assertThat(byQuarter.size(), equalTo(9)); // from 2013-Q1 to 2015-Q1
assertThat(byQuarter.get(0), equalTo(new Checkmark(timestamp(2015, JANUARY, 1), 0))); assertThat(byQuarter.get(0), equalTo(new Checkmark(timestamp(2015, JANUARY, 1), 0, false)));
assertThat(byQuarter.get(4), equalTo(new Checkmark(timestamp(2014, JANUARY, 1), 4964))); assertThat(byQuarter.get(4), equalTo(new Checkmark(timestamp(2014, JANUARY, 1), 4964, true)));
assertThat(byQuarter.get(8), equalTo(new Checkmark(timestamp(2013, JANUARY, 1), 4975))); assertThat(byQuarter.get(8), equalTo(new Checkmark(timestamp(2013, JANUARY, 1), 4975, true)));
List<Checkmark> byYear = checkmarks.groupBy(YEAR, Calendar.SATURDAY); List<Checkmark> byYear = checkmarks.groupBy(YEAR, Calendar.SATURDAY);
assertThat(byYear.size(), equalTo(3)); // from 2013 to 2015 assertThat(byYear.size(), equalTo(3)); // from 2013 to 2015
assertThat(byYear.get(0), equalTo(new Checkmark(timestamp(2015, JANUARY, 1), 0))); assertThat(byYear.get(0), equalTo(new Checkmark(timestamp(2015, JANUARY, 1), 0, false)));
assertThat(byYear.get(1), equalTo(new Checkmark(timestamp(2014, JANUARY, 1), 8227))); assertThat(byYear.get(1), equalTo(new Checkmark(timestamp(2014, JANUARY, 1), 8227, true)));
assertThat(byYear.get(2), equalTo(new Checkmark(timestamp(2013, JANUARY, 1), 16172))); assertThat(byYear.get(2), equalTo(new Checkmark(timestamp(2013, JANUARY, 1), 16172, true)));
} }
@Test @Test

@ -102,19 +102,19 @@ public class HabitTest extends BaseUnitTest
h.setTargetValue(100.0); h.setTargetValue(100.0);
assertFalse(h.isCompletedToday()); assertFalse(h.isCompletedToday());
h.getRepetitions().toggle(getToday(), 200_000); h.getRepetitions().toggle(getToday(), 200_000, true);
assertTrue(h.isCompletedToday()); assertTrue(h.isCompletedToday());
h.getRepetitions().toggle(getToday(), 100_000); h.getRepetitions().toggle(getToday(), 100_000, true);
assertTrue(h.isCompletedToday()); assertTrue(h.isCompletedToday());
h.getRepetitions().toggle(getToday(), 50_000); h.getRepetitions().toggle(getToday(), 50_000, true);
assertFalse(h.isCompletedToday()); assertFalse(h.isCompletedToday());
h.setTargetType(Habit.AT_MOST); h.setTargetType(Habit.AT_MOST);
h.getRepetitions().toggle(getToday(), 200_000); h.getRepetitions().toggle(getToday(), 200_000, true);
assertFalse(h.isCompletedToday()); assertFalse(h.isCompletedToday());
h.getRepetitions().toggle(getToday(), 100_000); h.getRepetitions().toggle(getToday(), 100_000, true);
assertTrue(h.isCompletedToday()); assertTrue(h.isCompletedToday());
h.getRepetitions().toggle(getToday(), 50_000); h.getRepetitions().toggle(getToday(), 50_000, true);
assertTrue(h.isCompletedToday()); assertTrue(h.isCompletedToday());
} }
@ -159,4 +159,4 @@ public class HabitTest extends BaseUnitTest
assertThat(h.toString(), equalTo(expected)); assertThat(h.toString(), equalTo(expected));
} }
} }

@ -170,17 +170,19 @@ public class RepetitionListTest extends BaseUnitTest
reset(listener); reset(listener);
habit.setType(Habit.NUMBER_HABIT); habit.setType(Habit.NUMBER_HABIT);
reps.toggle(today, 100); reps.toggle(today, 100, true);
Repetition check = reps.getByTimestamp(today); Repetition check = reps.getByTimestamp(today);
assertNotNull(check); assertNotNull(check);
assertThat(check.getValue(), equalTo(100)); assertThat(check.getValue(), equalTo(100));
assertThat(check.isManualInput(), equalTo(true));
verify(listener).onModelChange(); verify(listener).onModelChange();
reset(listener); reset(listener);
reps.toggle(today, 500); reps.toggle(today, 500, true);
check = reps.getByTimestamp(today); check = reps.getByTimestamp(today);
assertNotNull(check); assertNotNull(check);
assertThat(check.getValue(), equalTo(500)); assertThat(check.getValue(), equalTo(500));
assertThat(check.isManualInput(), equalTo(true));
verify(listener, times(2)).onModelChange(); verify(listener, times(2)).onModelChange();
reset(listener); reset(listener);
} }
@ -188,7 +190,7 @@ public class RepetitionListTest extends BaseUnitTest
@Test @Test
public void testToString() throws Exception public void testToString() throws Exception
{ {
Repetition rep = new Repetition(Timestamp.ZERO.plus(100), 20); Repetition rep = new Repetition(Timestamp.ZERO.plus(100), 20, true);
assertThat(rep.toString(), equalTo("{timestamp: 1970-04-11, value: 20}")); assertThat(rep.toString(), equalTo("{timestamp: 1970-04-11, value: 20, manualInput: true}"));
} }
} }

@ -70,12 +70,13 @@ public class SQLiteRepetitionListTest extends BaseUnitTest
RepetitionRecord record = getByTimestamp(today.plus(1)); RepetitionRecord record = getByTimestamp(today.plus(1));
assertNull(record); assertNull(record);
Repetition rep = new Repetition(today.plus(1), YES_MANUAL); Repetition rep = new Repetition(today.plus(1), YES, true);
habit.getRepetitions().add(rep); habit.getRepetitions().add(rep);
record = getByTimestamp(today.plus(1)); record = getByTimestamp(today.plus(1));
assertNotNull(record); assertNotNull(record);
assertThat(record.value, equalTo(YES_MANUAL)); assertThat(record.value, equalTo(YES));
assertThat(record.isManualInput(), equalTo(true));
} }
@Test @Test

@ -33,7 +33,7 @@ public class RepetitionRecordTest extends BaseUnitTest
@Test @Test
public void testRecord() throws Exception public void testRecord() throws Exception
{ {
Repetition rep = new Repetition(Timestamp.ZERO.plus(100), 50); Repetition rep = new Repetition(Timestamp.ZERO.plus(100), 50, true);
RepetitionRecord record = new RepetitionRecord(); RepetitionRecord record = new RepetitionRecord();
record.copyFrom(rep); record.copyFrom(rep);
assertThat(rep, equalTo(record.toRepetition())); assertThat(rep, equalTo(record.toRepetition()));

@ -84,7 +84,7 @@ public class HabitCardListCacheTest extends BaseUnitTest
{ {
Habit h2 = habitList.getByPosition(2); Habit h2 = habitList.getByPosition(2);
Timestamp today = DateUtils.getToday(); Timestamp today = DateUtils.getToday();
commandRunner.execute(new CreateRepetitionCommand(habitList, h2, today, Checkmark.NO), h2.getId()); commandRunner.execute(new CreateRepetitionCommand(habitList, h2, today, Checkmark.NO, false), h2.getId());
verify(listener).onItemChanged(2); verify(listener).onItemChanged(2);
verify(listener).onRefreshFinished(); verify(listener).onRefreshFinished();
verifyNoMoreInteractions(listener); verifyNoMoreInteractions(listener);
@ -103,9 +103,9 @@ public class HabitCardListCacheTest extends BaseUnitTest
assertThat(cache.getScore(h.getId()), equalTo(score)); assertThat(cache.getScore(h.getId()), equalTo(score));
Timestamp today = DateUtils.getToday(); Timestamp today = DateUtils.getToday();
int[] actualCheckmarks = cache.getCheckmarks(h.getId()); CheckmarkState[] actualCheckmarks = cache.getCheckmarkStates(h.getId());
int[] expectedCheckmarks = CheckmarkState[] expectedCheckmarks =
h.getCheckmarks().getValues(today.minus(9), today); h.getCheckmarks().getCheckmarkStates(today.minus(9), today);
assertThat(actualCheckmarks, equalTo(expectedCheckmarks)); assertThat(actualCheckmarks, equalTo(expectedCheckmarks));
} }
@ -181,4 +181,4 @@ public class HabitCardListCacheTest extends BaseUnitTest
habitList.remove(h); habitList.remove(h);
} }
} }

@ -173,8 +173,8 @@ public class ListHabitsBehaviorTest extends BaseUnitTest
public void testOnToggle() public void testOnToggle()
{ {
assertTrue(habit1.isCompletedToday()); assertTrue(habit1.isCompletedToday());
behavior.onToggle(habit1, DateUtils.getToday(), Checkmark.NO); behavior.onToggle(habit1, DateUtils.getToday(), Checkmark.NO, false);
assertFalse(habit1.isCompletedToday()); assertFalse(habit1.isCompletedToday());
} }
} }

Loading…
Cancel
Save