Add instrumented unit tests for SQLite lists

pull/145/head
Alinson S. Xavier 9 years ago
parent 2b23b36e36
commit 9a44774284

@ -15,6 +15,7 @@ android {
buildConfigField "String", "databaseFilename", "\"uhabits.db\"" buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArgument "size", "medium"
} }
buildTypes { buildTypes {
@ -40,7 +41,7 @@ android {
unitTests.all { unitTests.all {
testLogging { testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError" events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false} outputs.upToDateWhen { false }
showStandardStreams = true showStandardStreams = true
} }
} }

@ -55,7 +55,7 @@ public class BaseAndroidTest
protected AndroidTestComponent androidTestComponent; protected AndroidTestComponent androidTestComponent;
protected HabitFixtures habitFixtures; protected HabitFixtures fixtures;
@Before @Before
public void setUp() public void setUp()
@ -76,7 +76,7 @@ public class BaseAndroidTest
HabitsApplication.setComponent(androidTestComponent); HabitsApplication.setComponent(androidTestComponent);
androidTestComponent.inject(this); androidTestComponent.inject(this);
habitFixtures = new HabitFixtures(habitList); fixtures = new HabitFixtures(habitList);
} }
protected void waitForAsyncTasks() protected void waitForAsyncTasks()

@ -0,0 +1,117 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteCheckmarkListTest extends BaseAndroidTest
{
private Habit habit;
private CheckmarkList checkmarks;
private long today;
private long day;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
checkmarks = habit.getCheckmarks();
checkmarks.getToday(); // compute checkmarks
today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testAdd()
{
checkmarks.invalidateNewerThan(0);
List<Checkmark> list = new LinkedList<>();
list.add(new Checkmark(habit, 0, 0));
list.add(new Checkmark(habit, 1, 1));
list.add(new Checkmark(habit, 2, 2));
checkmarks.add(list);
List<CheckmarkRecord> records = getAllRecords();
assertThat(records.size(), equalTo(3));
assertThat(records.get(0).timestamp, equalTo(2L));
}
@Test
public void testGetByInterval()
{
long from = today - 10 * day;
long to = today - 3 * day;
List<Checkmark> list = checkmarks.getByInterval(from, to);
assertThat(list.size(), equalTo(8));
assertThat(list.get(0).getTimestamp(), equalTo(today - 3 * day));
assertThat(list.get(3).getTimestamp(), equalTo(today - 6 * day));
assertThat(list.get(7).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testInvalidateNewerThan()
{
List<CheckmarkRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
checkmarks.invalidateNewerThan(today - 20 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(100));
assertThat(records.get(0).timestamp, equalTo(today - 21 * day));
}
private List<CheckmarkRecord> getAllRecords()
{
return new Select()
.from(CheckmarkRecord.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc")
.execute();
}
}

@ -0,0 +1,229 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.junit.*;
import org.junit.rules.*;
import org.junit.runner.*;
import java.util.*;
import static junit.framework.Assert.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.IsEqual.*;
@SuppressWarnings("JavaDoc")
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteHabitListTest extends BaseAndroidTest
{
@Rule
public ExpectedException exception = ExpectedException.none();
@Override
public void setUp()
{
super.setUp();
fixtures.purgeHabits(habitList);
for (int i = 0; i < 10; i++)
{
Habit h = new Habit();
h.setName("habit " + i);
h.setId((long) i);
if (i % 2 == 0) h.setArchived(1);
HabitRecord record = new HabitRecord();
record.copyFrom(h);
record.position = i;
record.save(i);
}
}
@Test
public void testAdd_withDuplicate()
{
Habit habit = new Habit();
habitList.add(habit);
exception.expect(IllegalArgumentException.class);
habitList.add(habit);
}
@Test
public void testAdd_withId()
{
Habit habit = new Habit();
habit.setName("Hello world with id");
habit.setId(12300L);
habitList.add(habit);
assertThat(habit.getId(), equalTo(12300L));
HabitRecord record = getRecord(12300L);
assertNotNull(record);
assertThat(record.name, equalTo(habit.getName()));
}
@Test
public void testAdd_withoutId()
{
Habit habit = new Habit();
habit.setName("Hello world");
assertNull(habit.getId());
habitList.add(habit);
assertNotNull(habit.getId());
HabitRecord record = getRecord(habit.getId());
assertNotNull(record);
assertThat(record.name, equalTo(habit.getName()));
}
@Test
public void testCountActive()
{
assertThat(habitList.countActive(), equalTo(5));
}
@Test
public void testCountWithArchived()
{
assertThat(habitList.countWithArchived(), equalTo(10));
}
@Test
public void testGetAll_withArchived()
{
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(10));
assertThat(habits.get(3).getName(), equalTo("habit 3"));
}
@Test
public void testGetAll_withoutArchived()
{
List<Habit> habits = habitList.getAll(false);
assertThat(habits.size(), equalTo(5));
assertThat(habits.get(3).getName(), equalTo("habit 7"));
List<Habit> another = habitList.getAll(false);
assertThat(habits, equalTo(another));
}
@Test
public void testGetById()
{
Habit h1 = habitList.getById(0);
assertNotNull(h1);
assertThat(h1.getName(), equalTo("habit 0"));
Habit h2 = habitList.getById(0);
assertNotNull(h2);
assertThat(h1, equalTo(h2));
}
@Test
public void testGetById_withInvalid()
{
long invalidId = 9183792001L;
Habit h1 = habitList.getById(invalidId);
assertNull(h1);
}
@Test
public void testGetByPosition()
{
Habit h = habitList.getByPosition(5);
assertNotNull(h);
assertThat(h.getName(), equalTo("habit 5"));
h = habitList.getByPosition(5000);
assertNull(h);
}
@Test
public void testIndexOf()
{
Habit h1 = habitList.getByPosition(5);
assertNotNull(h1);
assertThat(habitList.indexOf(h1), equalTo(5));
Habit h2 = new Habit();
assertThat(habitList.indexOf(h2), equalTo(-1));
h2.setId(1000L);
assertThat(habitList.indexOf(h2), equalTo(-1));
}
@Test
public void test_reorder()
{
// Same as HabitListTest.java
// TODO: remove duplication
int operations[][] = {
{5, 2}, {3, 7}, {4, 4}, {3, 2}
};
int expectedPosition[][] = {
{0, 1, 3, 4, 5, 2, 6, 7, 8, 9},
{0, 1, 7, 3, 4, 2, 5, 6, 8, 9},
{0, 1, 7, 3, 4, 2, 5, 6, 8, 9},
{0, 1, 7, 2, 4, 3, 5, 6, 8, 9},
};
for (int i = 0; i < operations.length; i++)
{
int from = operations[i][0];
int to = operations[i][1];
Habit fromHabit = habitList.getByPosition(from);
Habit toHabit = habitList.getByPosition(to);
habitList.reorder(fromHabit, toHabit);
int actualPositions[] = new int[10];
for (int j = 0; j < 10; j++)
{
Habit h = habitList.getById(j);
assertNotNull(h);
actualPositions[j] = habitList.indexOf(h);
}
assertThat(actualPositions, equalTo(expectedPosition[i]));
}
}
private HabitRecord getRecord(long id)
{
return new Select()
.from(HabitRecord.class)
.where("id = ?", id)
.executeSingle();
}
}

@ -0,0 +1,145 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.support.annotation.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.IsNot.not;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteRepetitionListTest extends BaseAndroidTest
{
private Habit habit;
private long today;
private RepetitionList repetitions;
private long day;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
repetitions = habit.getRepetitions();
today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testAdd()
{
RepetitionRecord record = getByTimestamp(today + day);
assertThat(record, is(nullValue()));
Repetition rep = new Repetition(habit, today + day);
habit.getRepetitions().add(rep);
record = getByTimestamp(today + day);
assertThat(record, is(not(nullValue())));
}
@Test
public void testGetByInterval()
{
List<Repetition> reps =
repetitions.getByInterval(today - 10 * day, today);
assertThat(reps.size(), equalTo(8));
assertThat(reps.get(0).getTimestamp(), equalTo(today - 10 * day));
assertThat(reps.get(4).getTimestamp(), equalTo(today - 5 * day));
assertThat(reps.get(5).getTimestamp(), equalTo(today - 3 * day));
}
@Test
public void testGetByTimestamp()
{
Repetition rep = repetitions.getByTimestamp(today);
assertThat(rep, is(not(nullValue())));
assertThat(rep.getHabit(), equalTo(habit));
assertThat(rep.getTimestamp(), equalTo(today));
rep = repetitions.getByTimestamp(today - 2 * day);
assertThat(rep, is(nullValue()));
}
@Test
public void testGetOldest()
{
Repetition rep = repetitions.getOldest();
assertThat(rep, is(not(nullValue())));
assertThat(rep.getHabit(), equalTo(habit));
assertThat(rep.getTimestamp(), equalTo(today - 120 * day));
}
@Test
public void testGetOldest_withEmptyHabit()
{
Habit empty = fixtures.createEmptyHabit();
Repetition rep = empty.getRepetitions().getOldest();
assertThat(rep, is(nullValue()));
}
@Test
public void testRemove()
{
RepetitionRecord record = getByTimestamp(today);
assertThat(record, is(not(nullValue())));
Repetition rep = record.toRepetition();
repetitions.remove(rep);
record = getByTimestamp(today);
assertThat(record, is(nullValue()));
}
@Nullable
private RepetitionRecord getByTimestamp(long timestamp)
{
return selectByTimestamp(timestamp).executeSingle();
}
@NonNull
private From selectByTimestamp(long timestamp)
{
return new Select()
.from(RepetitionRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp = ?", timestamp);
}
}

@ -0,0 +1,126 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.sqlite;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
@SuppressWarnings("JavaDoc")
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteScoreListTest extends BaseAndroidTest
{
private Habit habit;
private ScoreList scores;
private long today;
private long day;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
scores = habit.getScores();
today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testGetAll()
{
List<Score> list = scores.getAll();
assertThat(list.size(), equalTo(121));
assertThat(list.get(0).getTimestamp(), equalTo(today));
assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testInvalidateNewerThan()
{
scores.getTodayValue(); // force recompute
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
scores.invalidateNewerThan(today - 10 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(110));
assertThat(records.get(0).timestamp, equalTo(today - 11 * day));
}
@Test
public void testAdd()
{
new Delete().from(ScoreRecord.class).execute();
List<Score> list = new LinkedList<>();
list.add(new Score(habit, today, 0));
list.add(new Score(habit, today - day, 0));
list.add(new Score(habit, today - 2 * day, 0));
scores.add(list);
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(3));
assertThat(records.get(0).timestamp, equalTo(today));
}
@Test
public void testGetByTimestamp()
{
Score s = scores.getByTimestamp(today);
assertNotNull(s);
assertThat(s.getTimestamp(), equalTo(today));
s = scores.getByTimestamp(today - 200 * day);
assertNull(s);
}
private List<ScoreRecord> getAllRecords()
{
return new Select()
.from(ScoreRecord.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc")
.execute();
}
}

@ -23,7 +23,7 @@ import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.contrib.RecyclerViewActions; import android.support.test.espresso.contrib.RecyclerViewActions;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.sqlite.HabitRecord; import org.isoron.uhabits.models.sqlite.records.HabitRecord;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;

@ -30,7 +30,7 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.LargeTest;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.sqlite.HabitRecord; import org.isoron.uhabits.models.sqlite.records.HabitRecord;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.MainActivity; import org.isoron.uhabits.MainActivity;
import org.junit.After; import org.junit.After;

@ -25,7 +25,6 @@ import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.ArchiveHabitsCommand; import org.isoron.uhabits.commands.ArchiveHabitsCommand;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -48,7 +47,7 @@ public class ArchiveHabitsCommandTest extends BaseAndroidTest
{ {
super.setUp(); super.setUp();
habit = habitFixtures.createShortHabit(); habit = fixtures.createShortHabit();
command = new ArchiveHabitsCommand(Collections.singletonList(habit)); command = new ArchiveHabitsCommand(Collections.singletonList(habit));
} }

@ -25,7 +25,6 @@ import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.ChangeHabitColorCommand; import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -51,7 +50,7 @@ public class ChangeHabitColorCommandTest extends BaseAndroidTest
for(int i = 0; i < 3; i ++) for(int i = 0; i < 3; i ++)
{ {
Habit habit = habitFixtures.createShortHabit(); Habit habit = fixtures.createShortHabit();
habit.setColor(i + 1); habit.setColor(i + 1);
habits.add(habit); habits.add(habit);
} }

@ -23,7 +23,6 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.commands.CreateHabitCommand; import org.isoron.uhabits.commands.CreateHabitCommand;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.junit.Before; import org.junit.Before;
@ -54,7 +53,7 @@ public class CreateHabitCommandTest extends BaseAndroidTest
model.setName("New habit"); model.setName("New habit");
command = new CreateHabitCommand(model); command = new CreateHabitCommand(model);
habitFixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
} }
@Test @Test

@ -19,22 +19,20 @@
package org.isoron.uhabits.unit.commands; package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.*;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.*;
import org.isoron.uhabits.commands.DeleteHabitsCommand; import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.junit.Before; import org.junit.*;
import org.junit.Rule; import org.junit.rules.*;
import org.junit.Test; import org.junit.runner.*;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import java.util.LinkedList; import java.util.*;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.*;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest
@ -53,18 +51,18 @@ public class DeleteHabitsCommandTest extends BaseAndroidTest
{ {
super.setUp(); super.setUp();
habitFixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
habits = new LinkedList<>(); habits = new LinkedList<>();
// Habits that should be deleted // Habits that should be deleted
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)
{ {
Habit habit = habitFixtures.createShortHabit(); Habit habit = fixtures.createShortHabit();
habits.add(habit); habits.add(habit);
} }
// Extra habit that should not be deleted // Extra habit that should not be deleted
Habit extraHabit = habitFixtures.createShortHabit(); Habit extraHabit = fixtures.createShortHabit();
extraHabit.setName("extra"); extraHabit.setName("extra");
command = new DeleteHabitsCommand(habits); command = new DeleteHabitsCommand(habits);

@ -50,7 +50,7 @@ public class EditHabitCommandTest extends BaseAndroidTest
{ {
super.setUp(); super.setUp();
habit = habitFixtures.createShortHabit(); habit = fixtures.createShortHabit();
habit.setName("original"); habit.setName("original");
habit.setFreqDen(1); habit.setFreqDen(1);
habit.setFreqNum(1); habit.setFreqNum(1);

@ -47,7 +47,7 @@ public class ToggleRepetitionCommandTest extends BaseAndroidTest
{ {
super.setUp(); super.setUp();
habit = habitFixtures.createShortHabit(); habit = fixtures.createShortHabit();
today = DateUtils.getStartOfToday(); today = DateUtils.getStartOfToday();
command = new ToggleRepetitionCommand(habit, today); command = new ToggleRepetitionCommand(habit, today);

@ -47,7 +47,7 @@ public class UnarchiveHabitsCommandTest extends BaseAndroidTest
{ {
super.setUp(); super.setUp();
habit = habitFixtures.createShortHabit(); habit = fixtures.createShortHabit();
habit.setArchived(1); habit.setArchived(1);
habitList.update(habit); habitList.update(habit);

@ -53,9 +53,9 @@ public class HabitsCSVExporterTest extends BaseAndroidTest
{ {
super.setUp(); super.setUp();
habitFixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
habitFixtures.createShortHabit(); fixtures.createShortHabit();
habitFixtures.createEmptyHabit(); fixtures.createEmptyHabit();
Context targetContext = InstrumentationRegistry.getTargetContext(); Context targetContext = InstrumentationRegistry.getTargetContext();
baseDir = targetContext.getCacheDir(); baseDir = targetContext.getCacheDir();

@ -59,7 +59,7 @@ public class ImportTest extends BaseAndroidTest
super.setUp(); super.setUp();
DateUtils.setFixedLocalTime(null); DateUtils.setFixedLocalTime(null);
habitFixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
context = InstrumentationRegistry.getInstrumentation().getContext(); context = InstrumentationRegistry.getInstrumentation().getContext();
baseDir = FileUtils.getFilesDir("Backups"); baseDir = FileUtils.getFilesDir("Backups");
if(baseDir == null) fail("baseDir should not be null"); if(baseDir == null) fail("baseDir should not be null");

@ -1,33 +0,0 @@
/*
* 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.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ScoreListTest extends BaseAndroidTest
{
}

@ -19,24 +19,21 @@
package org.isoron.uhabits.unit.tasks; package org.isoron.uhabits.unit.tasks;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.*;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.ExportCSVTask; import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.unit.HabitFixtures; import org.junit.*;
import org.junit.Before; import org.junit.runner.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File; import java.io.*;
import java.util.List; import java.util.*;
import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.*;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.IsNot.not; import static org.hamcrest.core.IsNot.not;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@ -52,7 +49,7 @@ public class ExportCSVTaskTest extends BaseAndroidTest
@Test @Test
public void testExportCSV() throws Throwable public void testExportCSV() throws Throwable
{ {
habitFixtures.createShortHabit(); fixtures.createShortHabit();
List<Habit> habits = habitList.getAll(true); List<Habit> habits = habitList.getAll(true);
ExportCSVTask task = new ExportCSVTask(habits, null); ExportCSVTask task = new ExportCSVTask(habits, null);

@ -47,7 +47,7 @@ public class CheckmarkWidgetViewTest extends ViewTest
super.setUp(); super.setUp();
InterfaceUtils.setFixedTheme(R.style.TransparentWidgetTheme); InterfaceUtils.setFixedTheme(R.style.TransparentWidgetTheme);
habit = habitFixtures.createShortHabit(); habit = fixtures.createShortHabit();
view = new CheckmarkWidgetView(targetContext); view = new CheckmarkWidgetView(targetContext);
view.setHabit(habit); view.setHabit(habit);
refreshData(view); refreshData(view);

@ -39,8 +39,8 @@ public class HabitFrequencyViewTest extends ViewTest
{ {
super.setUp(); super.setUp();
habitFixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
Habit habit = habitFixtures.createLongHabit(); Habit habit = fixtures.createLongHabit();
view = new HabitFrequencyView(targetContext); view = new HabitFrequencyView(targetContext);
view.setHabit(habit); view.setHabit(habit);

@ -47,8 +47,8 @@ public class HabitHistoryViewTest extends ViewTest
{ {
super.setUp(); super.setUp();
habitFixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
habit = habitFixtures.createLongHabit(); habit = fixtures.createLongHabit();
view = new HabitHistoryView(targetContext); view = new HabitHistoryView(targetContext);
view.setHabit(habit); view.setHabit(habit);

@ -42,8 +42,8 @@ public class HabitScoreViewTest extends ViewTest
{ {
super.setUp(); super.setUp();
habitFixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
habit = habitFixtures.createLongHabit(); habit = fixtures.createLongHabit();
view = new HabitScoreView(targetContext); view = new HabitScoreView(targetContext);
view.setHabit(habit); view.setHabit(habit);

@ -40,8 +40,8 @@ public class HabitStreakViewTest extends ViewTest
{ {
super.setUp(); super.setUp();
habitFixtures.purgeHabits(habitList); fixtures.purgeHabits(habitList);
Habit habit = habitFixtures.createLongHabit(); Habit habit = fixtures.createLongHabit();
view = new HabitStreakView(targetContext); view = new HabitStreakView(targetContext);
measureView(dpToPixels(300), dpToPixels(100), view); measureView(dpToPixels(300), dpToPixels(100), view);

@ -19,7 +19,7 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.*;
/** /**
* A Checkmark represents the completion status of the habit for a given day. * A Checkmark represents the completion status of the habit for a given day.
@ -62,6 +62,11 @@ public class Checkmark
this.value = value; this.value = value;
} }
public int compareNewer(Checkmark other)
{
return Long.signum(this.getTimestamp() - other.getTimestamp());
}
public Habit getHabit() public Habit getHabit()
{ {
return habit; return habit;

@ -19,16 +19,13 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.*;
import java.io.IOException; import java.io.*;
import java.io.Writer; import java.text.*;
import java.text.SimpleDateFormat; import java.util.*;
import java.util.Date;
import java.util.List;
/** /**
* The collection of {@link Checkmark}s belonging to a habit. * The collection of {@link Checkmark}s belonging to a habit.
@ -44,20 +41,30 @@ public abstract class CheckmarkList
this.habit = habit; this.habit = habit;
} }
/**
* Adds all the given checkmarks to the list.
* <p>
* This should never be called by the application, since the checkmarks are
* computed automatically from the list of repetitions.
*
* @param checkmarks the checkmarks to be added.
*/
public abstract void add(List<Checkmark> checkmarks);
/** /**
* Returns the values for all the checkmarks, since the oldest repetition of * Returns the values for all the checkmarks, since the oldest repetition of
* the habit until today. If there are no repetitions at all, returns an * the habit until today.
* empty array.
* <p> * <p>
* The values are returned in an array containing one integer value for each * If there are no repetitions at all, returns an empty array. The values
* day since the first repetition of the habit until today. The first entry * 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 * corresponds to today, the second entry corresponds to yesterday, and so
* on. * on.
* *
* @return values for the checkmarks in the interval * @return values for the checkmarks in the interval
*/ */
@NonNull @NonNull
public int[] getAllValues() public 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];
@ -68,17 +75,32 @@ public abstract class CheckmarkList
return getValues(fromTimestamp, toTimestamp); return getValues(fromTimestamp, toTimestamp);
} }
/**
* Returns the list of checkmarks that fall within the given interval.
* <p>
* There is exactly one checkmark per day in the interval. The endpoints of
* the interval are included. The list is ordered by timestamp (decreasing).
* That is, the first checkmark corresponds to the newest timestamp, and the
* last checkmark corresponds to the oldest timestamp.
*
* @param fromTimestamp timestamp of the beginning of the interval.
* @param toTimestamp timestamp of the end of the interval.
* @return the list of checkmarks within the interval.
*/
@NonNull
public abstract List<Checkmark> getByInterval(long fromTimestamp,
long toTimestamp);
/** /**
* Returns the checkmark for today. * Returns the checkmark for today.
* *
* @return checkmark for today * @return checkmark for today
*/ */
@Nullable @Nullable
public Checkmark getToday() public final Checkmark getToday()
{ {
long today = DateUtils.getStartOfToday(); computeAll();
compute(today, today); return getNewestComputed();
return getNewest();
} }
/** /**
@ -86,7 +108,7 @@ public abstract class CheckmarkList
* *
* @return value of today's checkmark * @return value of today's checkmark
*/ */
public int getTodayValue() public final int getTodayValue()
{ {
Checkmark today = getToday(); Checkmark today = getToday();
if (today != null) return today.getValue(); if (today != null) return today.getValue();
@ -106,7 +128,17 @@ public abstract class CheckmarkList
* @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 abstract int[] getValues(long from, long to); public final int[] getValues(long from, long to)
{
List<Checkmark> checkmarks = getByInterval(from, to);
int values[] = new int[checkmarks.size()];
int i = 0;
for (Checkmark c : checkmarks)
values[i++] = c.getValue();
return values;
}
/** /**
* Marks as invalid every checkmark that has timestamp either equal or newer * Marks as invalid every checkmark that has timestamp either equal or newer
@ -119,13 +151,11 @@ public abstract class CheckmarkList
/** /**
* 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.
* There is one line for each checkmark. Each line contains two fields:
* timestamp and value.
* *
* @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 void writeCSV(Writer out) throws IOException public final void writeCSV(Writer out) throws IOException
{ {
computeAll(); computeAll();
@ -149,11 +179,11 @@ public abstract class CheckmarkList
* @param from timestamp for the beginning of the interval * @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval * @param to timestamp for the end of the interval
*/ */
protected void compute(long from, final long to) protected final void compute(long from, final long to)
{ {
final long day = DateUtils.millisecondsInOneDay; final long day = DateUtils.millisecondsInOneDay;
Checkmark newestCheckmark = getNewest(); Checkmark newestCheckmark = getNewestComputed();
if (newestCheckmark != null) if (newestCheckmark != null)
from = newestCheckmark.getTimestamp() + day; from = newestCheckmark.getTimestamp() + day;
@ -185,12 +215,16 @@ public abstract class CheckmarkList
checks[i] = Checkmark.CHECKED_IMPLICITLY; checks[i] = Checkmark.CHECKED_IMPLICITLY;
} }
List<Checkmark> checkmarks = new LinkedList<>();
long timestamps[] = new long[nDays];
for (int i = 0; i < nDays; i++) for (int i = 0; i < nDays; i++)
timestamps[i] = to - i * day; {
int value = checks[i];
long timestamp = to - i * day;
checkmarks.add(new Checkmark(habit, timestamp, value));
}
insert(timestamps, checks); add(checkmarks);
} }
/** /**
@ -198,24 +232,21 @@ public abstract class CheckmarkList
* repetition until today. Days that already have a corresponding checkmark * repetition until today. Days that already have a corresponding checkmark
* are skipped. * are skipped.
*/ */
protected void computeAll() protected final void computeAll()
{ {
Repetition oldest = habit.getRepetitions().getOldest(); Repetition oldest = habit.getRepetitions().getOldest();
if (oldest == null) return; if (oldest == null) return;
Long today = DateUtils.getStartOfToday(); Long today = DateUtils.getStartOfToday();
compute(oldest.getTimestamp(), today); compute(oldest.getTimestamp(), today);
} }
/** /**
* Returns newest checkmark that has already been computed. Ignores any * Returns newest checkmark that has already been computed.
* checkmark that has timestamp in the future. This does not update the * <p>
* cache. * Ignores any checkmark that has timestamp in the future.
* *
* @return newest checkmark already computed * @return newest checkmark already computed
*/ */
protected abstract Checkmark getNewest(); protected abstract Checkmark getNewestComputed();
protected abstract void insert(long timestamps[], int values[]);
} }

@ -19,17 +19,16 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import android.net.Uri; import android.net.*;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.*;
import java.util.Locale; import java.util.*;
import javax.inject.Inject; import javax.inject.*;
/** /**
* The thing that the user wants to track. * The thing that the user wants to track.
@ -105,7 +104,7 @@ public class Habit
checkmarks = factory.buildCheckmarkList(this); checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this); streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this); scores = factory.buildScoreList(this);
repetitions = factory.buidRepetitionList(this); repetitions = factory.buildRepetitionList(this);
} }
/** /**
@ -128,7 +127,7 @@ public class Habit
checkmarks = factory.buildCheckmarkList(this); checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this); streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this); scores = factory.buildScoreList(this);
repetitions = factory.buidRepetitionList(this); repetitions = factory.buildRepetitionList(this);
} }
/** /**
@ -235,7 +234,7 @@ public class Habit
return freqNum; return freqNum;
} }
public void setFreqNum(Integer freqNum) public void setFreqNum(@NonNull Integer freqNum)
{ {
this.freqNum = freqNum; this.freqNum = freqNum;
} }
@ -243,16 +242,18 @@ public class Habit
/** /**
* Not currently used. * Not currently used.
*/ */
@NonNull
public Integer getHighlight() public Integer getHighlight()
{ {
return highlight; return highlight;
} }
public void setHighlight(Integer highlight) public void setHighlight(@NonNull Integer highlight)
{ {
this.highlight = highlight; this.highlight = highlight;
} }
@Nullable
public Long getId() public Long getId()
{ {
return id; return id;
@ -387,7 +388,7 @@ public class Habit
return archived != 0; return archived != 0;
} }
public void setArchived(Integer archived) public void setArchived(@NonNull Integer archived)
{ {
this.archived = archived; this.archived = archived;
} }

@ -19,18 +19,14 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import com.opencsv.CSVWriter; import com.opencsv.*;
import org.isoron.uhabits.utils.ColorUtils; import org.isoron.uhabits.utils.*;
import java.io.IOException; import java.io.*;
import java.io.Writer; import java.util.*;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/** /**
* An ordered collection of {@link Habit}s. * An ordered collection of {@link Habit}s.
@ -43,7 +39,8 @@ public abstract class HabitList
* Creates a new HabitList. * Creates a new HabitList.
* <p> * <p>
* Depending on the implementation, this list can either be empty or be * Depending on the implementation, this list can either be empty or be
* populated by some pre-existing habits. * populated by some pre-existing habits, for example, from a certain
* database.
*/ */
public HabitList() public HabitList()
{ {
@ -52,17 +49,24 @@ public abstract class HabitList
/** /**
* Inserts a new habit in the list. * Inserts a new habit in the list.
* <p>
* If the id of the habit is null, the list will assign it a new id, which
* is guaranteed to be unique in the scope of the list. If id is not null,
* the caller should make sure that the list does not already contain
* another habit with same id, otherwise a RuntimeException will be thrown.
* *
* @param habit the habit to be inserted * @param habit the habit to be inserted
* @throws IllegalArgumentException if the habit is already on the list.
*/ */
public abstract void add(@NonNull Habit habit); public abstract void add(@NonNull Habit habit)
throws IllegalArgumentException;
/** /**
* Returns the total number of unarchived habits. * Returns the total number of active habits.
* *
* @return number of unarchived habits * @return number of active habits
*/ */
public abstract int count(); public abstract int countActive();
/** /**
* Returns the total number of habits, including archived habits. * Returns the total number of habits, including archived habits.

@ -20,17 +20,17 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
/** /**
* Interface implemented by factories that provide concrete implementations * Interface implemented by factories that provide concrete implementations of
* of the core model classes. * the core model classes.
*/ */
public interface ModelFactory public interface ModelFactory
{ {
RepetitionList buidRepetitionList(Habit habit); RepetitionList buildRepetitionList(Habit habit);
HabitList buildHabitList();
CheckmarkList buildCheckmarkList(Habit habit); CheckmarkList buildCheckmarkList(Habit habit);
HabitList buildHabitList();
ScoreList buildScoreList(Habit habit); ScoreList buildScoreList(Habit habit);
StreakList buildStreakList(Habit habit); StreakList buildStreakList(Habit habit);

@ -19,8 +19,7 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import java.util.LinkedList; import java.util.*;
import java.util.List;
/** /**
* A ModelObservable allows objects to subscribe themselves to it and receive * A ModelObservable allows objects to subscribe themselves to it and receive

@ -19,9 +19,9 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import android.support.annotation.NonNull; import android.support.annotation.*;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.*;
/** /**
* Represents a record that the user has performed a certain habit at a certain * Represents a record that the user has performed a certain habit at a certain

@ -19,15 +19,12 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import java.util.Arrays; import java.util.*;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
/** /**
* The collection of {@link Repetition}s belonging to a habit. * The collection of {@link Repetition}s belonging to a habit.
@ -72,15 +69,16 @@ public abstract class RepetitionList
/** /**
* Returns the list of repetitions that happened within the given time * Returns the list of repetitions that happened within the given time
* interval. * interval.
* * <p>
* The list is sorted by timestamp in decreasing order. That is, the first * The list is sorted by timestamp in increasing order. That is, the first
* element corresponds to the most recent timestamp. The endpoints of the * element corresponds to oldest timestamp, while the last element
* interval are included. * corresponds to the newest. The endpoints of the interval are included.
* *
* @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 list of repetitions within given time interval * @return list of repetitions within given time interval
*/ */
// TODO: Change order timestamp desc
public abstract List<Repetition> getByInterval(long fromTimestamp, public abstract List<Repetition> getByInterval(long fromTimestamp,
long toTimestamp); long toTimestamp);

@ -19,13 +19,18 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.*;
/** /**
* Represents how strong a habit is at a certain date. * Represents how strong a habit is at a certain date.
*/ */
public class Score public class Score
{ {
/**
* Maximum score value attainable by any habit.
*/
public static final int MAX_VALUE = 19259478;
/** /**
* Habit to which this score belongs to. * Habit to which this score belongs to.
*/ */
@ -42,11 +47,6 @@ public class Score
*/ */
private final Integer value; private final Integer value;
/**
* Maximum score value attainable by any habit.
*/
public static final int MAX_VALUE = 19259478;
public Score(Habit habit, Long timestamp, Integer value) public Score(Habit habit, Long timestamp, Integer value)
{ {
this.habit = habit; this.habit = habit;

@ -19,21 +19,15 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.*;
import java.io.IOException; import java.io.*;
import java.io.Writer; import java.text.*;
import java.text.SimpleDateFormat; import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
public abstract class ScoreList public abstract class ScoreList implements Iterable<Score>
{ {
protected final Habit habit; protected final Habit habit;
@ -53,8 +47,29 @@ public abstract class ScoreList
observable = new ModelObservable(); observable = new ModelObservable();
} }
/**
* Adds the given scores to the list.
* <p>
* This method should not be called by the application, since the scores are
* computed automatically from the list of repetitions.
*
* @param scores the scores to add.
*/
public abstract void add(List<Score> scores);
public abstract List<Score> getAll(); public abstract List<Score> getAll();
/**
* Returns the score that has the given timestamp.
* <p>
* If no such score exists, returns null.
*
* @param timestamp the timestamp to find.
* @return the score with given timestamp, or null if none exists.
*/
@Nullable
public abstract Score getByTimestamp(long timestamp);
public ModelObservable getObservable() public ModelObservable getObservable()
{ {
return observable; return observable;
@ -72,11 +87,20 @@ public abstract class ScoreList
/** /**
* Returns the value of the score for a given day. * Returns the value of the score for a given day.
* <p>
* If there is no score at the given timestamp (for example, if the
* timestamp given happens before the first repetition of the habit) then
* returns zero.
* *
* @param timestamp the timestamp of a day * @param timestamp the timestamp of a day
* @return score for that day * @return score value for that day
*/ */
public abstract int getValue(long timestamp); public final int getValue(long timestamp)
{
Score s = getByTimestamp(timestamp);
if (s != null) return s.getValue();
return 0;
}
public List<Score> groupBy(DateUtils.TruncateField field) public List<Score> groupBy(DateUtils.TruncateField field)
{ {
@ -95,12 +119,18 @@ public abstract class ScoreList
*/ */
public abstract void invalidateNewerThan(long timestamp); public abstract void invalidateNewerThan(long timestamp);
@Override
public Iterator<Score> iterator()
{
return getAll().iterator();
}
public void writeCSV(Writer out) throws IOException public void writeCSV(Writer out) throws IOException
{ {
computeAll(); computeAll();
SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat();
for (Score s : getAll()) for (Score s : this)
{ {
String timestamp = dateFormat.format(s.getTimestamp()); String timestamp = dateFormat.format(s.getTimestamp());
String score = String score =
@ -109,8 +139,6 @@ public abstract class ScoreList
} }
} }
protected abstract void add(List<Score> scores);
/** /**
* Computes and saves the scores that are missing inside a given time * Computes and saves the scores that are missing inside a given time
* interval. * interval.
@ -173,20 +201,21 @@ public abstract class ScoreList
} }
/** /**
* Returns the score for a certain day. * Returns the most recent score that has already been computed.
* <p>
* If no score has been computed yet, returns null.
* *
* @param timestamp the timestamp for the day * @return the newest score computed, or null if none exist
* @return the score for the day
*/ */
@Nullable @Nullable
protected abstract Score get(long timestamp); protected abstract Score getNewestComputed();
@NonNull @NonNull
private HashMap<Long, ArrayList<Long>> getGroupedValues(DateUtils.TruncateField field) private HashMap<Long, ArrayList<Long>> getGroupedValues(DateUtils.TruncateField field)
{ {
HashMap<Long, ArrayList<Long>> groups = new HashMap<>(); HashMap<Long, ArrayList<Long>> groups = new HashMap<>();
for (Score s : getAll()) for (Score s : this)
{ {
long groupTimestamp = DateUtils.truncate(field, s.getTimestamp()); long groupTimestamp = DateUtils.truncate(field, s.getTimestamp());
@ -199,17 +228,6 @@ public abstract class ScoreList
return groups; return groups;
} }
/**
* Returns the most recent score that has already been computed.
* <p>
* If no score has been computed yet, returns null.
*
* @return the newest score computed, or null if none exist
*/
@Nullable
protected abstract Score getNewestComputed();
@NonNull @NonNull
private List<Score> groupsToAvgScores(HashMap<Long, ArrayList<Long>> groups) private List<Score> groupsToAvgScores(HashMap<Long, ArrayList<Long>> groups)
{ {

@ -19,8 +19,8 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.*;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.*;
public class Streak public class Streak
{ {

@ -19,15 +19,11 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.*;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/** /**
* The collection of {@link Streak}s that belong to a habit. * The collection of {@link Streak}s that belong to a habit.
@ -80,7 +76,7 @@ public abstract class StreakList
List<Streak> streaks = checkmarksToStreaks(beginning, checks); List<Streak> streaks = checkmarksToStreaks(beginning, checks);
removeNewestComputed(); removeNewestComputed();
insert(streaks); add(streaks);
} }
/** /**
@ -155,7 +151,7 @@ public abstract class StreakList
return list; return list;
} }
protected abstract void insert(@NonNull List<Streak> streaks); protected abstract void add(@NonNull List<Streak> streaks);
protected abstract void removeNewestComputed(); protected abstract void removeNewestComputed();
} }

@ -19,13 +19,11 @@
package org.isoron.uhabits.models.memory; package org.isoron.uhabits.models.memory;
import org.isoron.uhabits.models.Checkmark; import android.support.annotation.*;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DateUtils;
import java.util.Collections; import org.isoron.uhabits.models.*;
import java.util.LinkedList;
import java.util.*;
/** /**
* In-memory implementation of {@link CheckmarkList}. * In-memory implementation of {@link CheckmarkList}.
@ -41,20 +39,25 @@ public class MemoryCheckmarkList extends CheckmarkList
} }
@Override @Override
public int[] getValues(long from, long to) public void add(List<Checkmark> checkmarks)
{
list.addAll(checkmarks);
Collections.sort(list, (c1, c2) -> c2.compareNewer(c1));
}
@NonNull
@Override
public List<Checkmark> getByInterval(long fromTimestamp, long toTimestamp)
{ {
compute(from, to); compute(fromTimestamp, toTimestamp);
if (from > to) return new int[0];
int length = (int) ((to - from) / DateUtils.millisecondsInOneDay + 1); List<Checkmark> filtered = new LinkedList<>();
int values[] = new int[length];
int k = 0;
for (Checkmark c : list) for (Checkmark c : list)
if(c.getTimestamp() >= from && c.getTimestamp() <= to) if (c.getTimestamp() >= fromTimestamp &&
values[k++] = c.getValue(); c.getTimestamp() <= toTimestamp) filtered.add(c);
return values; return filtered;
} }
@Override @Override
@ -69,7 +72,7 @@ public class MemoryCheckmarkList extends CheckmarkList
} }
@Override @Override
protected Checkmark getNewest() protected Checkmark getNewestComputed()
{ {
long newestTimestamp = 0; long newestTimestamp = 0;
Checkmark newestCheck = null; Checkmark newestCheck = null;
@ -86,17 +89,4 @@ public class MemoryCheckmarkList extends CheckmarkList
return newestCheck; return newestCheck;
} }
@Override
protected void insert(long[] timestamps, int[] values)
{
for (int i = 0; i < timestamps.length; i++)
{
long t = timestamps[i];
int v = values[i];
list.add(new Checkmark(habit, t, v));
}
Collections.sort(list,
(c1, c2) -> (int) (c2.getTimestamp() - c1.getTimestamp()));
}
} }

@ -19,14 +19,11 @@
package org.isoron.uhabits.models.memory; package org.isoron.uhabits.models.memory;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.HabitList;
import java.util.LinkedList; import java.util.*;
import java.util.List;
/** /**
* In-memory implementation of {@link HabitList}. * In-memory implementation of {@link HabitList}.
@ -42,13 +39,21 @@ public class MemoryHabitList extends HabitList
} }
@Override @Override
public void add(Habit habit) public void add(@NonNull Habit habit) throws IllegalArgumentException
{ {
if (list.contains(habit))
throw new IllegalArgumentException("habit already added");
Long id = habit.getId();
if (id != null && getById(id) != null)
throw new RuntimeException("duplicate id");
if (id == null) habit.setId((long) list.size());
list.addLast(habit); list.addLast(habit);
} }
@Override @Override
public int count() public int countActive()
{ {
int count = 0; int count = 0;
for (Habit h : list) if (!h.isArchived()) count++; for (Habit h : list) if (!h.isArchived()) count++;
@ -61,13 +66,6 @@ public class MemoryHabitList extends HabitList
return list.size(); return list.size();
} }
@Override
public Habit getById(long id)
{
for (Habit h : list) if (h.getId() == id) return h;
return null;
}
@NonNull @NonNull
@Override @Override
public List<Habit> getAll(boolean includeArchive) public List<Habit> getAll(boolean includeArchive)
@ -76,6 +74,13 @@ public class MemoryHabitList extends HabitList
return getFiltered(habit -> !habit.isArchived()); return getFiltered(habit -> !habit.isArchived());
} }
@Override
public Habit getById(long id)
{
for (Habit h : list) if (h.getId() == id) return h;
return null;
}
@Nullable @Nullable
@Override @Override
public Habit getByPosition(int position) public Habit getByPosition(int position)
@ -84,7 +89,7 @@ public class MemoryHabitList extends HabitList
} }
@Override @Override
public int indexOf(Habit h) public int indexOf(@NonNull Habit h)
{ {
return list.indexOf(h); return list.indexOf(h);
} }

@ -19,18 +19,12 @@
package org.isoron.uhabits.models.memory; package org.isoron.uhabits.models.memory;
import org.isoron.uhabits.models.CheckmarkList; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.models.ModelFactory;
import org.isoron.uhabits.models.RepetitionList;
import org.isoron.uhabits.models.ScoreList;
import org.isoron.uhabits.models.StreakList;
public class MemoryModelFactory implements ModelFactory public class MemoryModelFactory implements ModelFactory
{ {
@Override @Override
public RepetitionList buidRepetitionList(Habit habit) public RepetitionList buildRepetitionList(Habit habit)
{ {
return new MemoryRepetitionList(habit); return new MemoryRepetitionList(habit);
} }

@ -19,16 +19,11 @@
package org.isoron.uhabits.models.memory; package org.isoron.uhabits.models.memory;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.RepetitionList;
import java.util.Collections; import java.util.*;
import java.util.LinkedList;
import java.util.List;
/** /**
* In-memory implementation of {@link RepetitionList}. * In-memory implementation of {@link RepetitionList}.
@ -54,6 +49,7 @@ public class MemoryRepetitionList extends RepetitionList
public List<Repetition> getByInterval(long fromTimestamp, long toTimestamp) public List<Repetition> getByInterval(long fromTimestamp, long toTimestamp)
{ {
LinkedList<Repetition> filtered = new LinkedList<>(); LinkedList<Repetition> filtered = new LinkedList<>();
for (Repetition r : list) for (Repetition r : list)
{ {
long t = r.getTimestamp(); long t = r.getTimestamp();

@ -19,16 +19,11 @@
package org.isoron.uhabits.models.memory; package org.isoron.uhabits.models.memory;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.models.ScoreList;
import java.util.Collections; import java.util.*;
import java.util.LinkedList;
import java.util.List;
public class MemoryScoreList extends ScoreList public class MemoryScoreList extends ScoreList
{ {
@ -40,14 +35,6 @@ public class MemoryScoreList extends ScoreList
list = new LinkedList<>(); list = new LinkedList<>();
} }
@Override
public int getValue(long timestamp)
{
Score s = get(timestamp);
if (s != null) return s.getValue();
return 0;
}
@Override @Override
public void invalidateNewerThan(long timestamp) public void invalidateNewerThan(long timestamp)
{ {
@ -67,17 +54,9 @@ public class MemoryScoreList extends ScoreList
return new LinkedList<>(list); return new LinkedList<>(list);
} }
@Override
protected void add(List<Score> scores)
{
list.addAll(scores);
Collections.sort(list,
(s1, s2) -> Long.signum(s2.getTimestamp() - s1.getTimestamp()));
}
@Override
@Nullable @Nullable
protected Score get(long timestamp) @Override
public Score getByTimestamp(long timestamp)
{ {
computeAll(); computeAll();
for (Score s : list) for (Score s : list)
@ -86,11 +65,19 @@ public class MemoryScoreList extends ScoreList
return null; return null;
} }
@Override
public void add(List<Score> scores)
{
list.addAll(scores);
Collections.sort(list,
(s1, s2) -> Long.signum(s2.getTimestamp() - s1.getTimestamp()));
}
@Nullable @Nullable
@Override @Override
protected Score getNewestComputed() protected Score getNewestComputed()
{ {
if(list.isEmpty()) return null; if (list.isEmpty()) return null;
return list.get(0); return list.get(0);
} }
} }

@ -19,14 +19,10 @@
package org.isoron.uhabits.models.memory; package org.isoron.uhabits.models.memory;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Streak; import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.models.StreakList;
import org.isoron.uhabits.utils.DateUtils;
import java.util.Collections; import java.util.*;
import java.util.LinkedList;
import java.util.List;
public class MemoryStreakList extends StreakList public class MemoryStreakList extends StreakList
{ {
@ -43,9 +39,8 @@ public class MemoryStreakList extends StreakList
{ {
Streak newest = null; Streak newest = null;
for(Streak s : list) for (Streak s : list)
if(newest == null || s.getEnd() > newest.getEnd()) if (newest == null || s.getEnd() > newest.getEnd()) newest = s;
newest = s;
return newest; return newest;
} }
@ -55,8 +50,8 @@ public class MemoryStreakList extends StreakList
{ {
LinkedList<Streak> discard = new LinkedList<>(); LinkedList<Streak> discard = new LinkedList<>();
for(Streak s : list) for (Streak s : list)
if(s.getEnd() >= timestamp - DateUtils.millisecondsInOneDay) if (s.getEnd() >= timestamp - DateUtils.millisecondsInOneDay)
discard.add(s); discard.add(s);
list.removeAll(discard); list.removeAll(discard);
@ -64,7 +59,7 @@ public class MemoryStreakList extends StreakList
} }
@Override @Override
protected void insert(List<Streak> streaks) protected void add(List<Streak> streaks)
{ {
list.addAll(streaks); list.addAll(streaks);
Collections.sort(list, (s1, s2) -> s2.compareNewer(s1)); Collections.sort(list, (s1, s2) -> s2.compareNewer(s1));
@ -74,7 +69,7 @@ public class MemoryStreakList extends StreakList
protected void removeNewestComputed() protected void removeNewestComputed()
{ {
Streak newest = getNewestComputed(); Streak newest = getNewestComputed();
if(newest != null) list.remove(newest); if (newest != null) list.remove(newest);
} }
@Override @Override

@ -19,13 +19,7 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import org.isoron.uhabits.models.CheckmarkList; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.models.ModelFactory;
import org.isoron.uhabits.models.RepetitionList;
import org.isoron.uhabits.models.ScoreList;
import org.isoron.uhabits.models.StreakList;
/** /**
* Factory that provides models backed by an SQLite database. * Factory that provides models backed by an SQLite database.
@ -33,7 +27,7 @@ import org.isoron.uhabits.models.StreakList;
public class SQLModelFactory implements ModelFactory public class SQLModelFactory implements ModelFactory
{ {
@Override @Override
public RepetitionList buidRepetitionList(Habit habit) public RepetitionList buildRepetitionList(Habit habit)
{ {
return new SQLiteRepetitionList(habit); return new SQLiteRepetitionList(habit);
} }
@ -47,7 +41,7 @@ public class SQLModelFactory implements ModelFactory
@Override @Override
public HabitList buildHabitList() public HabitList buildHabitList()
{ {
return new SQLiteHabitList(); return SQLiteHabitList.getInstance();
} }
@Override @Override

@ -19,20 +19,17 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.database.Cursor; import android.database.sqlite.*;
import android.database.sqlite.SQLiteDatabase; import android.support.annotation.*;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache; import com.activeandroid.*;
import com.activeandroid.query.Delete; import com.activeandroid.query.*;
import com.activeandroid.query.Select;
import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.CheckmarkList; import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.utils.DateUtils;
import java.util.*;
/** /**
* Implementation of a {@link CheckmarkList} that is backed by SQLite. * Implementation of a {@link CheckmarkList} that is backed by SQLite.
@ -44,6 +41,47 @@ public class SQLiteCheckmarkList extends CheckmarkList
super(habit); super(habit);
} }
@Override
public void add(List<Checkmark> checkmarks)
{
String query =
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (Checkmark c : checkmarks)
{
statement.bindLong(1, habit.getId());
statement.bindLong(2, c.getTimestamp());
statement.bindLong(3, c.getValue());
statement.execute();
}
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
}
@Override
public List<Checkmark> getByInterval(long fromTimestamp, long toTimestamp)
{
computeAll();
List<CheckmarkRecord> records = select()
.and("timestamp >= ?", fromTimestamp)
.and("timestamp <= ?", toTimestamp)
.execute();
return toCheckmarks(records);
}
@Override @Override
public void invalidateNewerThan(long timestamp) public void invalidateNewerThan(long timestamp)
{ {
@ -57,84 +95,29 @@ public class SQLiteCheckmarkList extends CheckmarkList
} }
@Override @Override
@NonNull @Nullable
public int[] getValues(long fromTimestamp, long toTimestamp) protected Checkmark getNewestComputed()
{ {
compute(fromTimestamp, toTimestamp); CheckmarkRecord record = select().limit(1).executeSingle();
if (record == null) return null;
if (fromTimestamp > toTimestamp) return new int[0]; return record.toCheckmark();
String query = "select value, timestamp from Checkmarks where " +
"habit = ? and timestamp >= ? and timestamp <= ?";
SQLiteDatabase db = Cache.openDatabase();
String args[] = {
habit.getId().toString(),
Long.toString(fromTimestamp),
Long.toString(toTimestamp)
};
Cursor cursor = db.rawQuery(query, args);
long day = DateUtils.millisecondsInOneDay;
int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1;
int[] checks = new int[nDays];
if (cursor.moveToFirst())
{
do
{
long timestamp = cursor.getLong(1);
int offset = (int) ((timestamp - fromTimestamp) / day);
checks[nDays - offset - 1] = cursor.getInt(0);
} while (cursor.moveToNext());
}
cursor.close();
return checks;
} }
@Override @NonNull
@Nullable private From select()
protected Checkmark getNewest()
{ {
CheckmarkRecord record = new Select() return new Select()
.from(CheckmarkRecord.class) .from(CheckmarkRecord.class)
.where("habit = ?", habit.getId()) .where("habit = ?", habit.getId())
.and("timestamp <= ?", DateUtils.getStartOfToday()) .and("timestamp <= ?", DateUtils.getStartOfToday())
.orderBy("timestamp desc") .orderBy("timestamp desc");
.limit(1)
.executeSingle();
if(record == null) return null;
return record.toCheckmark();
} }
@Override @NonNull
protected void insert(long timestamps[], int values[]) private List<Checkmark> toCheckmarks(@NonNull List<CheckmarkRecord> records)
{ {
String query = List<Checkmark> checkmarks = new LinkedList<>();
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)"; for (CheckmarkRecord r : records) checkmarks.add(r.toCheckmark());
return checkmarks;
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (int i = 0; i < timestamps.length; i++)
{
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamps[i]);
statement.bindLong(3, values[i]);
statement.execute();
}
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
} }
} }

@ -19,19 +19,14 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import com.activeandroid.query.From; import com.activeandroid.query.*;
import com.activeandroid.query.Select;
import com.activeandroid.query.Update;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.models.sqlite.records.*;
import java.util.HashMap; import java.util.*;
import java.util.LinkedList;
import java.util.List;
/** /**
* Implementation of a {@link HabitList} that is backed by SQLite. * Implementation of a {@link HabitList} that is backed by SQLite.
@ -42,11 +37,19 @@ public class SQLiteHabitList extends HabitList
private HashMap<Long, Habit> cache; private HashMap<Long, Habit> cache;
public SQLiteHabitList() private SQLiteHabitList()
{ {
cache = new HashMap<>(); cache = new HashMap<>();
} }
/**
* Returns the global list of habits.
* <p>
* There is only one list of habit per application, corresponding to the
* habits table of the SQLite database.
*
* @return the global list of habits.
*/
public static SQLiteHabitList getInstance() public static SQLiteHabitList getInstance()
{ {
if (instance == null) instance = new SQLiteHabitList(); if (instance == null) instance = new SQLiteHabitList();
@ -56,15 +59,15 @@ public class SQLiteHabitList extends HabitList
@Override @Override
public void add(@NonNull Habit habit) public void add(@NonNull Habit habit)
{ {
if(cache.containsValue(habit)) if (cache.containsValue(habit))
throw new RuntimeException("habit already in cache"); throw new IllegalArgumentException("habit already added");
HabitRecord record = new HabitRecord(); HabitRecord record = new HabitRecord();
record.copyFrom(habit); record.copyFrom(habit);
record.position = countWithArchived(); record.position = countWithArchived();
Long id = habit.getId(); Long id = habit.getId();
if(id == null) id = record.save(); if (id == null) id = record.save();
else record.save(id); else record.save(id);
habit.setId(id); habit.setId(id);
@ -72,7 +75,7 @@ public class SQLiteHabitList extends HabitList
} }
@Override @Override
public int count() public int countActive()
{ {
return select().count(); return select().count();
} }
@ -128,12 +131,14 @@ public class SQLiteHabitList extends HabitList
.where("position = ?", position) .where("position = ?", position)
.executeSingle(); .executeSingle();
return getById(record.getId()); if(record != null) return getById(record.getId());
return null;
} }
@Override @Override
public int indexOf(@NonNull Habit h) public int indexOf(@NonNull Habit h)
{ {
if (h.getId() == null) return -1;
HabitRecord record = HabitRecord.get(h.getId()); HabitRecord record = HabitRecord.get(h.getId());
if (record == null) return -1; if (record == null) return -1;
return record.position; return record.position;

@ -19,64 +19,66 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import com.activeandroid.query.Delete; import com.activeandroid.query.*;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Repetition; import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.models.RepetitionList; import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.utils.DateUtils;
import java.util.HashMap; import java.util.*;
import java.util.LinkedList;
import java.util.List;
/** /**
* Implementation of a {@link RepetitionList} that is backed by SQLite. * Implementation of a {@link RepetitionList} that is backed by SQLite.
*/ */
public class SQLiteRepetitionList extends RepetitionList public class SQLiteRepetitionList extends RepetitionList
{ {
HashMap<Long, Repetition> cache;
public SQLiteRepetitionList(@NonNull Habit habit) public SQLiteRepetitionList(@NonNull Habit habit)
{ {
super(habit); super(habit);
this.cache = new HashMap<>();
} }
/**
* Adds a repetition to the global SQLite database.
* <p>
* Given a repetition, this creates and saves the corresponding
* RepetitionRecord to the database.
*
* @param rep the repetition to be added
*/
@Override @Override
public void add(Repetition rep) public void add(Repetition rep)
{ {
RepetitionRecord record = new RepetitionRecord(); RepetitionRecord record = new RepetitionRecord();
record.copyFrom(rep); record.copyFrom(rep);
long id = record.save(); record.save();
cache.put(id, rep);
observable.notifyListeners(); observable.notifyListeners();
} }
@Override @Override
public List<Repetition> getByInterval(long timeFrom, long timeTo) public List<Repetition> getByInterval(long timeFrom, long timeTo)
{ {
return getFromRecord(selectFromTo(timeFrom, timeTo).execute()); return toRepetitions(selectFromTo(timeFrom, timeTo).execute());
} }
@Override @Override
@Nullable
public Repetition getByTimestamp(long timestamp) public Repetition getByTimestamp(long timestamp)
{ {
RepetitionRecord record = RepetitionRecord record =
select().where("timestamp = ?", timestamp).executeSingle(); select().where("timestamp = ?", timestamp).executeSingle();
return getFromRecord(record);
if (record == null) return null;
return record.toRepetition();
} }
@Override @Override
public Repetition getOldest() public Repetition getOldest()
{ {
RepetitionRecord record = select().limit(1).executeSingle(); RepetitionRecord record = select().limit(1).executeSingle();
return getFromRecord(record); if (record == null) return null;
return record.toRepetition();
} }
@Override @Override
@ -91,38 +93,6 @@ public class SQLiteRepetitionList extends RepetitionList
observable.notifyListeners(); observable.notifyListeners();
} }
@NonNull
private List<Repetition> getFromRecord(
@Nullable List<RepetitionRecord> records)
{
List<Repetition> reps = new LinkedList<>();
if (records == null) return reps;
for (RepetitionRecord record : records)
{
Repetition rep = getFromRecord(record);
reps.add(rep);
}
return reps;
}
@Nullable
private Repetition getFromRecord(@Nullable RepetitionRecord record)
{
if (record == null) return null;
Long id = record.getId();
if (!cache.containsKey(id))
{
Repetition repetition = record.toRepetition();
cache.put(id, repetition);
}
return cache.get(id);
}
@NonNull @NonNull
private From select() private From select()
{ {
@ -140,4 +110,17 @@ public class SQLiteRepetitionList extends RepetitionList
.and("timestamp >= ?", timeFrom) .and("timestamp >= ?", timeFrom)
.and("timestamp <= ?", timeTo); .and("timestamp <= ?", timeTo);
} }
@NonNull
private List<Repetition> toRepetitions(
@Nullable List<RepetitionRecord> records)
{
List<Repetition> reps = new LinkedList<>();
if (records == null) return reps;
for (RepetitionRecord record : records)
reps.add(record.toRepetition());
return reps;
}
} }

@ -19,23 +19,16 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.*;
import android.database.sqlite.SQLiteStatement; import android.support.annotation.*;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache; import com.activeandroid.*;
import com.activeandroid.query.Delete; import com.activeandroid.query.*;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.models.ScoreList;
import java.util.LinkedList; import java.util.*;
import java.util.List;
/** /**
* Implementation of a ScoreList that is backed by SQLite. * Implementation of a ScoreList that is backed by SQLite.
@ -52,62 +45,33 @@ public class SQLiteScoreList extends ScoreList
super(habit); super(habit);
} }
@Override
public int getValue(long timestamp)
{
computeAll();
String[] args = {habit.getId().toString(), Long.toString(timestamp)};
return SQLiteUtils.intQuery(
"select score from Score where habit = ? and timestamp = ?", args);
}
@Override
public void invalidateNewerThan(long timestamp)
{
new Delete()
.from(ScoreRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
}
@Override @Override
@NonNull @NonNull
public List<Score> getAll() public List<Score> getAll()
{ {
computeAll();
List<ScoreRecord> records = select().execute(); List<ScoreRecord> records = select().execute();
List<Score> scores = new LinkedList<>(); List<Score> scores = new LinkedList<>();
for(ScoreRecord rec : records) for (ScoreRecord rec : records)
scores.add(rec.toScore()); scores.add(rec.toScore());
return scores; return scores;
} }
@Nullable
@Override
protected Score getNewestComputed()
{
ScoreRecord record = select().limit(1).executeSingle();
if(record == null) return null;
return record.toScore();
}
@Override @Override
@Nullable public void invalidateNewerThan(long timestamp)
protected Score get(long timestamp)
{ {
computeAll(); new Delete()
.from(ScoreRecord.class)
ScoreRecord record = .where("habit = ?", habit.getId())
select().where("timestamp = ?", timestamp).executeSingle(); .and("timestamp >= ?", timestamp)
.execute();
if(record == null) return null;
return record.toScore();
} }
@Override @Override
protected void add(List<Score> scores) public void add(List<Score> scores)
{ {
String query = String query =
"insert into Score(habit, timestamp, score) values (?,?,?)"; "insert into Score(habit, timestamp, score) values (?,?,?)";
@ -135,7 +99,29 @@ public class SQLiteScoreList extends ScoreList
} }
} }
protected From select() @Override
@Nullable
public Score getByTimestamp(long timestamp)
{
computeAll();
ScoreRecord record =
select().where("timestamp = ?", timestamp).executeSingle();
if (record == null) return null;
return record.toScore();
}
@Nullable
@Override
protected Score getNewestComputed()
{
ScoreRecord record = select().limit(1).executeSingle();
if (record == null) return null;
return record.toScore();
}
private From select()
{ {
return new Select() return new Select()
.from(ScoreRecord.class) .from(ScoreRecord.class)

@ -19,20 +19,15 @@
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import com.activeandroid.query.Delete; import com.activeandroid.query.*;
import com.activeandroid.query.Select;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Streak; import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.models.StreakList; import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import java.util.LinkedList; import java.util.*;
import java.util.List;
/** /**
* Implementation of a StreakList that is backed by SQLite. * Implementation of a StreakList that is backed by SQLite.
@ -61,7 +56,7 @@ public class SQLiteStreakList extends StreakList
public Streak getNewestComputed() public Streak getNewestComputed()
{ {
StreakRecord newestRecord = getNewestRecord(); StreakRecord newestRecord = getNewestRecord();
if(newestRecord == null) return null; if (newestRecord == null) return null;
return newestRecord.toStreak(); return newestRecord.toStreak();
} }
@ -77,19 +72,8 @@ public class SQLiteStreakList extends StreakList
observable.notifyListeners(); observable.notifyListeners();
} }
@Nullable
private StreakRecord getNewestRecord()
{
return new Select()
.from(StreakRecord.class)
.where("habit = ?", habit.getId())
.orderBy("end desc")
.limit(1)
.executeSingle();
}
@Override @Override
protected void insert(@NonNull List<Streak> streaks) protected void add(@NonNull List<Streak> streaks)
{ {
DatabaseUtils.executeAsTransaction(() -> { DatabaseUtils.executeAsTransaction(() -> {
for (Streak streak : streaks) for (Streak streak : streaks)
@ -101,6 +85,24 @@ public class SQLiteStreakList extends StreakList
}); });
} }
@Override
protected void removeNewestComputed()
{
StreakRecord newestStreak = getNewestRecord();
if (newestStreak != null) newestStreak.delete();
}
@Nullable
private StreakRecord getNewestRecord()
{
return new Select()
.from(StreakRecord.class)
.where("habit = ?", habit.getId())
.orderBy("end desc")
.limit(1)
.executeSingle();
}
@NonNull @NonNull
private List<Streak> recordsToStreaks(List<StreakRecord> records) private List<Streak> recordsToStreaks(List<StreakRecord> records)
{ {
@ -111,11 +113,4 @@ public class SQLiteStreakList extends StreakList
return streaks; return streaks;
} }
@Override
protected void removeNewestComputed()
{
StreakRecord newestStreak = getNewestRecord();
if (newestStreak != null) newestStreak.delete();
}
} }

@ -17,14 +17,13 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite.records;
import com.activeandroid.Model; import com.activeandroid.*;
import com.activeandroid.annotation.Column; import com.activeandroid.annotation.*;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.sqlite.*;
/** /**
* The SQLite database record corresponding to a {@link Checkmark}. * The SQLite database record corresponding to a {@link Checkmark}.

@ -17,20 +17,18 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite.records;
import android.annotation.SuppressLint; import android.annotation.*;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable;
import com.activeandroid.Model; import com.activeandroid.*;
import com.activeandroid.annotation.Column; import com.activeandroid.annotation.*;
import com.activeandroid.annotation.Table; import com.activeandroid.query.*;
import com.activeandroid.query.Delete; import com.activeandroid.util.*;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.DatabaseUtils; import org.isoron.uhabits.utils.*;
/** /**
* The SQLite database record corresponding to a {@link Habit}. * The SQLite database record corresponding to a {@link Habit}.
@ -83,7 +81,7 @@ public class HabitRecord extends Model
} }
@Nullable @Nullable
public static HabitRecord get(Long id) public static HabitRecord get(long id)
{ {
return HabitRecord.load(HabitRecord.class, id); return HabitRecord.load(HabitRecord.class, id);
} }

@ -17,14 +17,13 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite.records;
import com.activeandroid.Model; import com.activeandroid.*;
import com.activeandroid.annotation.Column; import com.activeandroid.annotation.*;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Repetition; import org.isoron.uhabits.models.sqlite.*;
/** /**
* The SQLite database record corresponding to a {@link Repetition}. * The SQLite database record corresponding to a {@link Repetition}.

@ -17,14 +17,13 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite.records;
import com.activeandroid.Model; import com.activeandroid.*;
import com.activeandroid.annotation.Column; import com.activeandroid.annotation.*;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.sqlite.*;
/** /**
* The SQLite database record corresponding to a Score. * The SQLite database record corresponding to a Score.

@ -17,14 +17,13 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.models.sqlite; package org.isoron.uhabits.models.sqlite.records;
import com.activeandroid.Model; import com.activeandroid.*;
import com.activeandroid.annotation.Column; import com.activeandroid.annotation.*;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Streak; import org.isoron.uhabits.models.sqlite.*;
/** /**
* The SQLite database record corresponding to a Streak. * The SQLite database record corresponding to a Streak.

@ -51,10 +51,10 @@ import java.util.Random;
public class HabitScoreView extends ScrollableDataView public class HabitScoreView extends ScrollableDataView
implements HabitDataView, ModelObservable.Listener implements HabitDataView, ModelObservable.Listener
{ {
public static final PorterDuffXfermode XFERMODE_CLEAR = private static final PorterDuffXfermode XFERMODE_CLEAR =
new PorterDuffXfermode(PorterDuff.Mode.CLEAR); new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
public static final PorterDuffXfermode XFERMODE_SRC = private static final PorterDuffXfermode XFERMODE_SRC =
new PorterDuffXfermode(PorterDuff.Mode.SRC); new PorterDuffXfermode(PorterDuff.Mode.SRC);
public static int DEFAULT_BUCKET_SIZES[] = {1, 7, 31, 92, 365}; public static int DEFAULT_BUCKET_SIZES[] = {1, 7, 31, 92, 365};

@ -19,25 +19,17 @@
package org.isoron.uhabits.utils; package org.isoron.uhabits.utils;
import android.content.Context; import android.content.*;
import android.database.Cursor; import android.database.*;
import android.support.annotation.NonNull; import android.support.annotation.*;
import com.activeandroid.ActiveAndroid; import com.activeandroid.*;
import com.activeandroid.Cache;
import com.activeandroid.Configuration; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.HabitsApplication; import java.io.*;
import org.isoron.uhabits.models.sqlite.CheckmarkRecord; import java.text.*;
import org.isoron.uhabits.models.sqlite.HabitRecord;
import org.isoron.uhabits.models.sqlite.RepetitionRecord;
import org.isoron.uhabits.models.sqlite.ScoreRecord;
import org.isoron.uhabits.models.sqlite.StreakRecord;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
public abstract class DatabaseUtils public abstract class DatabaseUtils
{ {

@ -18,15 +18,14 @@
*/ */
package org.isoron.uhabits.widgets; package org.isoron.uhabits.widgets;
import android.app.PendingIntent; import android.app.*;
import android.content.Context; import android.content.*;
import android.view.View; import android.view.*;
import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.*;
import org.isoron.uhabits.R; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.ui.habits.show.views.*;
import org.isoron.uhabits.widgets.views.CheckmarkWidgetView; import org.isoron.uhabits.widgets.views.*;
import org.isoron.uhabits.ui.habits.show.views.HabitDataView;
public class CheckmarkWidgetProvider extends BaseWidgetProvider public class CheckmarkWidgetProvider extends BaseWidgetProvider
{ {
@ -39,33 +38,34 @@ public class CheckmarkWidgetProvider extends BaseWidgetProvider
} }
@Override @Override
protected void refreshCustomViewData(View view) protected int getDefaultHeight()
{ {
((HabitDataView) view).refreshData(); return 125;
} }
@Override @Override
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) protected int getDefaultWidth()
{ {
return HabitBroadcastReceiver.buildCheckIntent(context, habit, null); return 125;
} }
@Override @Override
protected int getDefaultHeight() protected int getLayoutId()
{ {
return 125; return R.layout.widget_wrapper;
} }
@Override @Override
protected int getDefaultWidth() protected PendingIntent getOnClickPendingIntent(Context context,
Habit habit)
{ {
return 125; return HabitBroadcastReceiver.buildCheckIntent(context, habit, null);
} }
@Override @Override
protected int getLayoutId() protected void refreshCustomViewData(View view)
{ {
return R.layout.widget_wrapper; ((HabitDataView) view).refreshData();
} }

@ -20,6 +20,7 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import org.isoron.uhabits.BaseUnitTest; import org.isoron.uhabits.BaseUnitTest;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.DateUtils;
import org.junit.Test; import org.junit.Test;

@ -19,20 +19,17 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import org.hamcrest.MatcherAssert; import org.hamcrest.*;
import org.isoron.uhabits.BaseUnitTest; import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.*;
import org.junit.Test; import org.junit.*;
import java.io.IOException; import java.io.*;
import java.io.StringWriter; import java.util.*;
import java.util.ArrayList;
import java.util.List; import static junit.framework.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static junit.framework.Assert.fail; import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsEqual.equalTo;
public class HabitListTest extends BaseUnitTest public class HabitListTest extends BaseUnitTest
@ -73,7 +70,7 @@ public class HabitListTest extends BaseUnitTest
@Test @Test
public void test_count() public void test_count()
{ {
assertThat(list.count(), equalTo(6)); assertThat(list.countActive(), equalTo(6));
} }
@Test @Test

@ -22,6 +22,7 @@ package org.isoron.uhabits.models;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import org.isoron.uhabits.BaseUnitTest; import org.isoron.uhabits.BaseUnitTest;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.DateUtils;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -33,7 +34,7 @@ import java.util.GregorianCalendar;
import java.util.HashMap; import java.util.HashMap;
import java.util.Random; import java.util.Random;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.Is.is; import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.IsEqual.equalTo;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;

@ -76,7 +76,7 @@ public class ScoreListTest extends BaseUnitTest
int actualValues[] = new int[expectedValues.length]; int actualValues[] = new int[expectedValues.length];
int i = 0; int i = 0;
for (Score s : habit.getScores().getAll()) for (Score s : habit.getScores())
actualValues[i++] = s.getValue(); actualValues[i++] = s.getValue();
assertThat(actualValues, equalTo(expectedValues)); assertThat(actualValues, equalTo(expectedValues));

@ -19,16 +19,15 @@
package org.isoron.uhabits.models; package org.isoron.uhabits.models;
import org.isoron.uhabits.BaseUnitTest; import org.isoron.uhabits.*;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.*;
import org.junit.Test; import org.junit.*;
import java.util.List; import java.util.*;
import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.MatcherAssert.*;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.*;
import static org.mockito.Mockito.verify;
public class StreakListTest extends BaseUnitTest public class StreakListTest extends BaseUnitTest
{ {

Loading…
Cancel
Save