Separate ActiveAndroid from models

pull/145/head
Alinson S. Xavier 9 years ago
parent 18e8390aed
commit 78d4f86cab

@ -1,6 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'com.getkeepsafe.dexcount'
apply plugin: 'me.tatarka.retrolambda'
android {
@ -31,6 +30,7 @@ android {
lintOptions {
checkReleaseBuilds false
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
@ -77,14 +77,3 @@ dependencies {
exclude group: 'com.android.support'
}
}
task grantAnimationPermission(type: Exec, dependsOn: 'installDebug') {
commandLine "adb shell pm grant org.isoron.uhabits android.permission.SET_ANIMATION_SCALE".split(' ')
}
tasks.whenTaskAdded { task ->
if (task.name.startsWith('connected')) {
task.dependsOn grantAnimationPermission
}
}

@ -24,9 +24,11 @@ import android.os.Build;
import android.os.Looper;
import android.support.test.InstrumentationRegistry;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.Preferences;
import org.junit.Before;
@ -36,20 +38,29 @@ import javax.inject.Inject;
public class BaseAndroidTest
{
protected Context testContext;
protected Context targetContext;
// 8:00am, January 25th, 2015 (UTC)
public static final long FIXED_LOCAL_TIME = 1422172800000L;
private static boolean isLooperPrepared;
public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC)
protected Context testContext;
protected Context targetContext;
@Inject
protected Preferences prefs;
@Inject
protected HabitList habitList;
protected AndroidTestComponent androidTestComponent;
protected HabitFixtures habitFixtures;
@Before
public void setUp()
{
if(!isLooperPrepared)
if (!isLooperPrepared)
{
Looper.prepare();
isLooperPrepared = true;
@ -64,9 +75,12 @@ public class BaseAndroidTest
androidTestComponent = DaggerAndroidTestComponent.builder().build();
HabitsApplication.setComponent(androidTestComponent);
androidTestComponent.inject(this);
habitFixtures = new HabitFixtures(habitList);
}
protected void waitForAsyncTasks() throws InterruptedException, TimeoutException
protected void waitForAsyncTasks()
throws InterruptedException, TimeoutException
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
{

@ -39,7 +39,7 @@ public class HabitMatchers
@Override
public boolean matchesSafely(Habit habit)
{
return habit.name.equals(name);
return habit.getName().equals(name);
}
@Override
@ -51,7 +51,7 @@ public class HabitMatchers
@Override
public void describeMismatchSafely(Habit habit, Description description)
{
description.appendText("was ").appendText(habit.name);
description.appendText("was ").appendText(habit.getName());
}
};
}

@ -23,7 +23,7 @@ import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.contrib.RecyclerViewActions;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.sqlite.HabitRecord;
import java.util.Collections;
import java.util.LinkedList;
@ -93,7 +93,7 @@ public class MainActivityActions
onView(withId(R.id.buttonSave))
.perform(click());
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
onData(allOf(is(instanceOf(HabitRecord.class)), withName(name)))
.onChildView(withId(R.id.label));
return name;
@ -135,7 +135,7 @@ public class MainActivityActions
boolean first = true;
for(String name : names)
{
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
onData(allOf(is(instanceOf(HabitRecord.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(first ? longClick() : click());
@ -160,7 +160,7 @@ public class MainActivityActions
public static void assertHabitsExist(List<String> names)
{
for(String name : names)
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
onData(allOf(is(instanceOf(HabitRecord.class)), withName(name)))
.check(matches(isDisplayed()));
}

@ -30,8 +30,8 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.sqlite.HabitRecord;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.MainActivity;
import org.junit.After;
import org.junit.Before;
@ -190,13 +190,13 @@ public class MainTest
{
String name = addHabit(true);
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
onData(allOf(is(instanceOf(HabitRecord.class)), withName(name)))
.onChildView(withId(R.id.checkmarkPanel))
.perform(toggleAllCheckmarks());
Thread.sleep(1200);
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
onData(allOf(is(instanceOf(HabitRecord.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(click());
@ -217,7 +217,7 @@ public class MainTest
{
String name = addHabit();
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
onData(allOf(is(instanceOf(HabitRecord.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(longClick());
@ -247,7 +247,7 @@ public class MainTest
{
String name = addHabit();
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
onData(allOf(is(instanceOf(HabitRecord.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(click());

@ -19,151 +19,76 @@
package org.isoron.uhabits.unit;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.tasks.ExportDBTask;
import org.isoron.uhabits.tasks.ImportDataTask;
import java.io.File;
import java.io.InputStream;
import java.util.Random;
import static org.junit.Assert.fail;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.utils.DateUtils;
public class HabitFixtures
{
public static boolean NON_DAILY_HABIT_CHECKS[] = { true, false, false, true, true, true, false,
false, true, true };
public boolean NON_DAILY_HABIT_CHECKS[] = {
true, false, false, true, true, true, false, false, true, true
};
public static Habit createShortHabit()
{
Habit habit = new Habit();
habit.name = "Wake up early";
habit.description = "Did you wake up before 6am?";
habit.freqNum = 2;
habit.freqDen = 3;
habit.save();
private final HabitList habitList;
long timestamp = DateUtils.getStartOfToday();
for(boolean c : NON_DAILY_HABIT_CHECKS)
public HabitFixtures(HabitList habitList)
{
if(c) habit.repetitions.toggle(timestamp);
timestamp -= DateUtils.millisecondsInOneDay;
this.habitList = habitList;
}
return habit;
}
public static Habit createEmptyHabit()
public Habit createEmptyHabit()
{
Habit habit = new Habit();
habit.name = "Meditate";
habit.description = "Did you meditate this morning?";
habit.color = 3;
habit.freqNum = 1;
habit.freqDen = 1;
habit.save();
habit.setName("Meditate");
habit.setDescription("Did you meditate this morning?");
habit.setColor(3);
habit.setFreqNum(1);
habit.setFreqDen(1);
habitList.add(habit);
return habit;
}
public static Habit createLongHabit()
public Habit createLongHabit()
{
Habit habit = createEmptyHabit();
habit.freqNum = 3;
habit.freqDen = 7;
habit.color = 4;
habit.save();
habit.setFreqNum(3);
habit.setFreqDen(7);
habit.setColor(4);
long day = DateUtils.millisecondsInOneDay;
long today = DateUtils.getStartOfToday();
int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, 28, 50, 51, 52,
53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, 81, 83, 89, 90, 91, 95,
102, 103, 108, 109, 120};
int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27,
28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80,
81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120};
for(int mark : marks)
habit.repetitions.toggle(today - mark * day);
for (int mark : marks)
habit.getRepetitions().toggleTimestamp(today - mark * day);
return habit;
}
public static void generateHugeDataSet() throws Throwable
{
final int nHabits = 30;
final int nYears = 5;
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command()
{
@Override
public void execute()
public Habit createShortHabit()
{
Random rand = new Random();
for(int i = 0; i < nHabits; i++)
{
Log.i("HabitFixture", String.format("Creating habit %d / %d", i, nHabits));
Habit habit = new Habit();
habit.name = String.format("Habit %d", i);
habit.save();
long today = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
for(int j = 0; j < 365 * nYears; j++)
{
if(rand.nextBoolean())
habit.repetitions.toggle(today - j * day);
}
habit.setName("Wake up early");
habit.setDescription("Did you wake up before 6am?");
habit.setFreqNum(2);
habit.setFreqDen(3);
habitList.add(habit);
habit.scores.getTodayValue();
habit.streaks.getAll(1);
}
}
});
ExportDBTask task = new ExportDBTask(null);
task.setListener(new ExportDBTask.Listener()
{
@Override
public void onExportDBFinished(@Nullable String filename)
long timestamp = DateUtils.getStartOfToday();
for (boolean c : NON_DAILY_HABIT_CHECKS)
{
if(filename != null)
Log.i("HabitFixture", String.format("Huge data set exported to %s", filename));
else
Log.i("HabitFixture", "Failed to save database");
}
});
task.execute();
BaseTask.waitForTasks(30000);
if (c) habit.getRepetitions().toggleTimestamp(timestamp);
timestamp -= DateUtils.millisecondsInOneDay;
}
public static void loadHugeDataSet(Context testContext) throws Throwable
{
File baseDir = FileUtils.getFilesDir("Backups");
if(baseDir == null) fail("baseDir should not be null");
File dst = new File(String.format("%s/%s", baseDir.getPath(), "loopHuge.db"));
InputStream in = testContext.getAssets().open("fixtures/loopHuge.db");
FileUtils.copy(in, dst);
ImportDataTask task = new ImportDataTask(dst, null);
task.execute();
BaseTask.waitForTasks(30000);
return habit;
}
public static void purgeHabits()
public void purgeHabits(HabitList habitList)
{
for(Habit h : Habit.getAll(true))
h.cascadeDelete();
for (Habit h : habitList.getAll(true))
habitList.remove(h);
}
}

@ -48,7 +48,7 @@ public class ArchiveHabitsCommandTest extends BaseAndroidTest
{
super.setUp();
habit = HabitFixtures.createShortHabit();
habit = habitFixtures.createShortHabit();
command = new ArchiveHabitsCommand(Collections.singletonList(habit));
}

@ -51,9 +51,8 @@ public class ChangeHabitColorCommandTest extends BaseAndroidTest
for(int i = 0; i < 3; i ++)
{
Habit habit = HabitFixtures.createShortHabit();
habit.color = i+1;
habit.save();
Habit habit = habitFixtures.createShortHabit();
habit.setColor(i + 1);
habits.add(habit);
}
@ -79,12 +78,12 @@ public class ChangeHabitColorCommandTest extends BaseAndroidTest
{
int k = 0;
for(Habit h : habits)
assertThat(h.color, equalTo(++k));
assertThat(h.getColor(), equalTo(++k));
}
private void checkNewColors()
{
for(Habit h : habits)
assertThat(h.color, equalTo(0));
assertThat(h.getColor(), equalTo(0));
}
}

@ -23,9 +23,9 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.commands.CreateHabitCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -42,6 +42,7 @@ public class CreateHabitCommandTest extends BaseAndroidTest
{
private CreateHabitCommand command;
private Habit model;
@Before
@ -50,36 +51,36 @@ public class CreateHabitCommandTest extends BaseAndroidTest
super.setUp();
model = new Habit();
model.name = "New habit";
model.setName("New habit");
command = new CreateHabitCommand(model);
HabitFixtures.purgeHabits();
habitFixtures.purgeHabits(habitList);
}
@Test
public void testExecuteUndoRedo()
{
assertTrue(Habit.getAll(true).isEmpty());
assertTrue(habitList.getAll(true).isEmpty());
command.execute();
List<Habit> allHabits = Habit.getAll(true);
List<Habit> allHabits = habitList.getAll(true);
assertThat(allHabits.size(), equalTo(1));
Habit habit = allHabits.get(0);
Long id = habit.getId();
assertThat(habit.name, equalTo(model.name));
assertThat(habit.getName(), equalTo(model.getName()));
command.undo();
assertTrue(Habit.getAll(true).isEmpty());
assertTrue(habitList.getAll(true).isEmpty());
command.execute();
allHabits = Habit.getAll(true);
allHabits = habitList.getAll(true);
assertThat(allHabits.size(), equalTo(1));
habit = allHabits.get(0);
Long newId = habit.getId();
assertThat(id, equalTo(newId));
assertThat(habit.name, equalTo(model.name));
assertThat(habit.getName(), equalTo(model.getName()));
}
}

@ -25,7 +25,6 @@ import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@ -42,30 +41,31 @@ import static org.hamcrest.Matchers.equalTo;
public class DeleteHabitsCommandTest extends BaseAndroidTest
{
private DeleteHabitsCommand command;
private LinkedList<Habit> habits;
@Rule
public ExpectedException thrown = ExpectedException.none();
@Override
@Before
public void setUp()
{
super.setUp();
HabitFixtures.purgeHabits();
habitFixtures.purgeHabits(habitList);
habits = new LinkedList<>();
// Habits that shuold be deleted
for(int i = 0; i < 3; i ++)
// Habits that should be deleted
for (int i = 0; i < 3; i++)
{
Habit habit = HabitFixtures.createShortHabit();
Habit habit = habitFixtures.createShortHabit();
habits.add(habit);
}
// Extra habit that should not be deleted
Habit extraHabit = HabitFixtures.createShortHabit();
extraHabit.name = "extra";
extraHabit.save();
Habit extraHabit = habitFixtures.createShortHabit();
extraHabit.setName("extra");
command = new DeleteHabitsCommand(habits);
}
@ -73,11 +73,11 @@ public class DeleteHabitsCommandTest extends BaseAndroidTest
@Test
public void testExecuteUndoRedo()
{
assertThat(Habit.getAll(true).size(), equalTo(4));
assertThat(habitList.getAll(true).size(), equalTo(4));
command.execute();
assertThat(Habit.getAll(true).size(), equalTo(1));
assertThat(Habit.getAll(true).get(0).name, equalTo("extra"));
assertThat(habitList.getAll(true).size(), equalTo(1));
assertThat(habitList.getAll(true).get(0).getName(), equalTo("extra"));
thrown.expect(UnsupportedOperationException.class);
command.undo();

@ -25,12 +25,10 @@ import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.EditHabitCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
@ -41,25 +39,25 @@ public class EditHabitCommandTest extends BaseAndroidTest
{
private EditHabitCommand command;
private Habit habit;
private Habit modified;
private Long id;
@Override
@Before
public void setUp()
{
super.setUp();
habit = HabitFixtures.createShortHabit();
habit.name = "original";
habit.freqDen = 1;
habit.freqNum = 1;
habit.save();
id = habit.getId();
habit = habitFixtures.createShortHabit();
habit.setName("original");
habit.setFreqDen(1);
habit.setFreqNum(1);
modified = new Habit(habit);
modified.name = "modified";
modified = new Habit();
modified.copyFrom(habit);
modified.setName("modified");
}
@Test
@ -67,54 +65,44 @@ public class EditHabitCommandTest extends BaseAndroidTest
{
command = new EditHabitCommand(habit, modified);
int originalScore = habit.scores.getTodayValue();
assertThat(habit.name, equalTo("original"));
int originalScore = habit.getScores().getTodayValue();
assertThat(habit.getName(), equalTo("original"));
command.execute();
refreshHabit();
assertThat(habit.name, equalTo("modified"));
assertThat(habit.scores.getTodayValue(), equalTo(originalScore));
assertThat(habit.getName(), equalTo("modified"));
assertThat(habit.getScores().getTodayValue(), equalTo(originalScore));
command.undo();
refreshHabit();
assertThat(habit.name, equalTo("original"));
assertThat(habit.scores.getTodayValue(), equalTo(originalScore));
assertThat(habit.getName(), equalTo("original"));
assertThat(habit.getScores().getTodayValue(), equalTo(originalScore));
command.execute();
refreshHabit();
assertThat(habit.name, equalTo("modified"));
assertThat(habit.scores.getTodayValue(), equalTo(originalScore));
assertThat(habit.getName(), equalTo("modified"));
assertThat(habit.getScores().getTodayValue(), equalTo(originalScore));
}
@Test
public void testExecuteUndoRedo_withModifiedInterval()
{
modified.freqNum = 1;
modified.freqDen = 7;
modified.setFreqNum(1);
modified.setFreqDen(7);
command = new EditHabitCommand(habit, modified);
int originalScore = habit.scores.getTodayValue();
assertThat(habit.name, equalTo("original"));
int originalScore = habit.getScores().getTodayValue();
assertThat(habit.getName(), equalTo("original"));
command.execute();
refreshHabit();
assertThat(habit.name, equalTo("modified"));
assertThat(habit.scores.getTodayValue(), greaterThan(originalScore));
assertThat(habit.getName(), equalTo("modified"));
assertThat(habit.getScores().getTodayValue(),
greaterThan(originalScore));
command.undo();
refreshHabit();
assertThat(habit.name, equalTo("original"));
assertThat(habit.scores.getTodayValue(), equalTo(originalScore));
assertThat(habit.getName(), equalTo("original"));
assertThat(habit.getScores().getTodayValue(), equalTo(originalScore));
command.execute();
refreshHabit();
assertThat(habit.name, equalTo("modified"));
assertThat(habit.scores.getTodayValue(), greaterThan(originalScore));
}
private void refreshHabit()
{
habit = Habit.get(id);
assertTrue(habit != null);
assertThat(habit.getName(), equalTo("modified"));
assertThat(habit.getScores().getTodayValue(),
greaterThan(originalScore));
}
}

@ -24,9 +24,8 @@ import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.utils.DateUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -48,7 +47,7 @@ public class ToggleRepetitionCommandTest extends BaseAndroidTest
{
super.setUp();
habit = HabitFixtures.createShortHabit();
habit = habitFixtures.createShortHabit();
today = DateUtils.getStartOfToday();
command = new ToggleRepetitionCommand(habit, today);
@ -57,15 +56,15 @@ public class ToggleRepetitionCommandTest extends BaseAndroidTest
@Test
public void testExecuteUndoRedo()
{
assertTrue(habit.repetitions.contains(today));
assertTrue(habit.getRepetitions().containsTimestamp(today));
command.execute();
assertFalse(habit.repetitions.contains(today));
assertFalse(habit.getRepetitions().containsTimestamp(today));
command.undo();
assertTrue(habit.repetitions.contains(today));
assertTrue(habit.getRepetitions().containsTimestamp(today));
command.execute();
assertFalse(habit.repetitions.contains(today));
assertFalse(habit.getRepetitions().containsTimestamp(today));
}
}

@ -25,7 +25,6 @@ import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -39,17 +38,18 @@ import static junit.framework.Assert.assertTrue;
@SmallTest
public class UnarchiveHabitsCommandTest extends BaseAndroidTest
{
private UnarchiveHabitsCommand command;
private Habit habit;
@Override
@Before
public void setUp()
{
super.setUp();
habit = HabitFixtures.createShortHabit();
Habit.archive(Collections.singletonList(habit));
habit = habitFixtures.createShortHabit();
habit.setArchived(1);
habitList.update(habit);
command = new UnarchiveHabitsCommand(Collections.singletonList(habit));
}

@ -25,10 +25,9 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.io.HabitsCSVExporter;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.utils.FileUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -54,41 +53,18 @@ public class HabitsCSVExporterTest extends BaseAndroidTest
{
super.setUp();
HabitFixtures.purgeHabits();
HabitFixtures.createShortHabit();
HabitFixtures.createEmptyHabit();
habitFixtures.purgeHabits(habitList);
habitFixtures.createShortHabit();
habitFixtures.createEmptyHabit();
Context targetContext = InstrumentationRegistry.getTargetContext();
baseDir = targetContext.getCacheDir();
}
private void unzip(File file) throws IOException
{
ZipFile zip = new ZipFile(file);
Enumeration<? extends ZipEntry> e = zip.entries();
while(e.hasMoreElements())
{
ZipEntry entry = e.nextElement();
InputStream stream = zip.getInputStream(entry);
String outputFilename = String.format("%s/%s", baseDir.getAbsolutePath(),
entry.getName());
File outputFile = new File(outputFilename);
File parent = outputFile.getParentFile();
if(parent != null) parent.mkdirs();
FileUtils.copy(stream, outputFile);
}
zip.close();
}
@Test
public void testExportCSV() throws IOException
{
List<Habit> habits = Habit.getAll(true);
List<Habit> habits = habitList.getAll(true);
HabitsCSVExporter exporter = new HabitsCSVExporter(habits, baseDir);
String filename = exporter.writeArchive();
@ -105,14 +81,41 @@ public class HabitsCSVExporterTest extends BaseAndroidTest
assertPathExists("002 Meditate/Scores.csv");
}
private void assertAbsolutePathExists(String s)
{
File file = new File(s);
assertTrue(
String.format("File %s should exist", file.getAbsolutePath()),
file.exists());
}
private void assertPathExists(String s)
{
assertAbsolutePathExists(String.format("%s/%s", baseDir.getAbsolutePath(), s));
assertAbsolutePathExists(
String.format("%s/%s", baseDir.getAbsolutePath(), s));
}
private void assertAbsolutePathExists(String s)
private void unzip(File file) throws IOException
{
File file = new File(s);
assertTrue(String.format("File %s should exist", file.getAbsolutePath()), file.exists());
ZipFile zip = new ZipFile(file);
Enumeration<? extends ZipEntry> e = zip.entries();
while (e.hasMoreElements())
{
ZipEntry entry = e.nextElement();
InputStream stream = zip.getInputStream(entry);
String outputFilename =
String.format("%s/%s", baseDir.getAbsolutePath(),
entry.getName());
File outputFile = new File(outputFilename);
File parent = outputFile.getParentFile();
if (parent != null) parent.mkdirs();
FileUtils.copy(stream, outputFile);
}
zip.close();
}
}

@ -25,11 +25,10 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.io.GenericImporter;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -60,7 +59,7 @@ public class ImportTest extends BaseAndroidTest
super.setUp();
DateUtils.setFixedLocalTime(null);
HabitFixtures.purgeHabits();
habitFixtures.purgeHabits(habitList);
context = InstrumentationRegistry.getInstrumentation().getContext();
baseDir = FileUtils.getFilesDir("Backups");
if(baseDir == null) fail("baseDir should not be null");
@ -89,7 +88,7 @@ public class ImportTest extends BaseAndroidTest
{
GregorianCalendar date = DateUtils.getStartOfTodayCalendar();
date.set(year, month - 1, day);
return h.repetitions.contains(date.getTimeInMillis());
return h.getRepetitions().containsTimestamp(date.getTimeInMillis());
}
@Test
@ -97,11 +96,11 @@ public class ImportTest extends BaseAndroidTest
{
importFromFile("tickmate.db");
List<Habit> habits = Habit.getAll(true);
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(3));
Habit h = habits.get(0);
assertThat(h.name, equalTo("Vegan"));
assertThat(h.getName(), equalTo("Vegan"));
assertTrue(containsRepetition(h, 2016, 1, 24));
assertTrue(containsRepetition(h, 2016, 2, 5));
assertTrue(containsRepetition(h, 2016, 3, 18));
@ -113,13 +112,13 @@ public class ImportTest extends BaseAndroidTest
{
importFromFile("rewire.db");
List<Habit> habits = Habit.getAll(true);
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(3));
Habit habit = habits.get(0);
assertThat(habit.name, equalTo("Wake up early"));
assertThat(habit.freqNum, equalTo(3));
assertThat(habit.freqDen, equalTo(7));
assertThat(habit.getName(), equalTo("Wake up early"));
assertThat(habit.getFreqNum(), equalTo(3));
assertThat(habit.getFreqDen(), equalTo(7));
assertFalse(habit.hasReminder());
assertFalse(containsRepetition(habit, 2015, 12, 31));
assertTrue(containsRepetition(habit, 2016, 1, 18));
@ -127,13 +126,13 @@ public class ImportTest extends BaseAndroidTest
assertFalse(containsRepetition(habit, 2016, 3, 10));
habit = habits.get(1);
assertThat(habit.name, equalTo("brush teeth"));
assertThat(habit.freqNum, equalTo(3));
assertThat(habit.freqDen, equalTo(7));
assertThat(habit.reminderHour, equalTo(8));
assertThat(habit.reminderMin, equalTo(0));
assertThat(habit.getName(), equalTo("brush teeth"));
assertThat(habit.getFreqNum(), equalTo(3));
assertThat(habit.getFreqDen(), equalTo(7));
assertThat(habit.getReminderHour(), equalTo(8));
assertThat(habit.getReminderMin(), equalTo(0));
boolean[] reminderDays = {false, true, true, true, true, true, false};
assertThat(habit.reminderDays, equalTo(DateUtils.packWeekdayList(reminderDays)));
assertThat(habit.getReminderDays(), equalTo(DateUtils.packWeekdayList(reminderDays)));
}
@Test
@ -141,14 +140,14 @@ public class ImportTest extends BaseAndroidTest
{
importFromFile("habitbull.csv");
List<Habit> habits = Habit.getAll(true);
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(4));
Habit habit = habits.get(0);
assertThat(habit.name, equalTo("Breed dragons"));
assertThat(habit.description, equalTo("with love and fire"));
assertThat(habit.freqNum, equalTo(1));
assertThat(habit.freqDen, equalTo(1));
assertThat(habit.getName(), equalTo("Breed dragons"));
assertThat(habit.getDescription(), equalTo("with love and fire"));
assertThat(habit.getFreqNum(), equalTo(1));
assertThat(habit.getFreqDen(), equalTo(1));
assertTrue(containsRepetition(habit, 2016, 3, 18));
assertTrue(containsRepetition(habit, 2016, 3, 19));
assertFalse(containsRepetition(habit, 2016, 3, 20));
@ -159,13 +158,13 @@ public class ImportTest extends BaseAndroidTest
{
importFromFile("loop.db");
List<Habit> habits = Habit.getAll(true);
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(9));
Habit habit = habits.get(0);
assertThat(habit.name, equalTo("Wake up early"));
assertThat(habit.freqNum, equalTo(3));
assertThat(habit.freqDen, equalTo(7));
assertThat(habit.getName(), equalTo("Wake up early"));
assertThat(habit.getFreqNum(), equalTo(3));
assertThat(habit.getFreqDen(), equalTo(7));
assertTrue(containsRepetition(habit, 2016, 3, 14));
assertTrue(containsRepetition(habit, 2016, 3, 16));
assertFalse(containsRepetition(habit, 2016, 3, 17));

@ -1,175 +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.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.StringWriter;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY;
import static org.isoron.uhabits.models.Checkmark.CHECKED_IMPLICITLY;
import static org.isoron.uhabits.models.Checkmark.UNCHECKED;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CheckmarkListTest extends BaseAndroidTest
{
Habit nonDailyHabit;
private Habit emptyHabit;
@Before
public void setUp()
{
super.setUp();
HabitFixtures.purgeHabits();
nonDailyHabit = HabitFixtures.createShortHabit();
emptyHabit = HabitFixtures.createEmptyHabit();
}
@After
public void tearDown()
{
DateUtils.setFixedLocalTime(null);
}
@Test
public void test_getAllValues_withNonDailyHabit()
{
int[] expectedValues = { CHECKED_EXPLICITLY, UNCHECKED, CHECKED_IMPLICITLY,
CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, UNCHECKED,
CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY };
int[] actualValues = nonDailyHabit.checkmarks.getAllValues();
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getAllValues_withEmptyHabit()
{
int[] expectedValues = new int[0];
int[] actualValues = emptyHabit.checkmarks.getAllValues();
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getAllValues_moveForwardInTime()
{
travelInTime(3);
int[] expectedValues = { UNCHECKED, UNCHECKED, UNCHECKED, CHECKED_EXPLICITLY, UNCHECKED,
CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY,
UNCHECKED, CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY };
int[] actualValues = nonDailyHabit.checkmarks.getAllValues();
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getAllValues_moveBackwardsInTime()
{
travelInTime(-3);
int[] expectedValues = { CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY,
UNCHECKED, CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY };
int[] actualValues = nonDailyHabit.checkmarks.getAllValues();
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getValues_withInvalidInterval()
{
int values[] = nonDailyHabit.checkmarks.getValues(100L, -100L);
assertThat(values, equalTo(new int[0]));
}
@Test
public void test_getValues_withValidInterval()
{
long from = DateUtils.getStartOfToday() - 15 * DateUtils.millisecondsInOneDay;
long to = DateUtils.getStartOfToday() - 5 * DateUtils.millisecondsInOneDay;
int[] expectedValues = { CHECKED_EXPLICITLY, UNCHECKED, CHECKED_IMPLICITLY,
CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, UNCHECKED, UNCHECKED, UNCHECKED, UNCHECKED,
UNCHECKED, UNCHECKED };
int[] actualValues = nonDailyHabit.checkmarks.getValues(from, to);
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getTodayValue()
{
travelInTime(-1);
assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(UNCHECKED));
travelInTime(0);
assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(CHECKED_EXPLICITLY));
travelInTime(1);
assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(UNCHECKED));
}
@Test
public void test_writeCSV() throws IOException
{
String expectedCSV =
"2015-01-16,2\n" +
"2015-01-17,2\n" +
"2015-01-18,1\n" +
"2015-01-19,0\n" +
"2015-01-20,2\n" +
"2015-01-21,2\n" +
"2015-01-22,2\n" +
"2015-01-23,1\n" +
"2015-01-24,0\n" +
"2015-01-25,2\n";
StringWriter writer = new StringWriter();
nonDailyHabit.checkmarks.writeCSV(writer);
assertThat(writer.toString(), equalTo(expectedCSV));
}
private void travelInTime(int days)
{
DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME +
days * DateUtils.millisecondsInOneDay);
}
}

@ -1,378 +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.hamcrest.MatcherAssert;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.StringWriter;
import java.util.LinkedList;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitTest extends BaseAndroidTest
{
@Before
public void setUp()
{
super.setUp();
HabitFixtures.purgeHabits();
}
@Test
public void testConstructor_default()
{
Habit habit = new Habit();
assertThat(habit.archived, is(0));
assertThat(habit.highlight, is(0));
assertThat(habit.reminderHour, is(nullValue()));
assertThat(habit.reminderMin, is(nullValue()));
assertThat(habit.reminderDays, is(not(nullValue())));
assertThat(habit.streaks, is(not(nullValue())));
assertThat(habit.scores, is(not(nullValue())));
assertThat(habit.repetitions, is(not(nullValue())));
assertThat(habit.checkmarks, is(not(nullValue())));
}
@Test
public void testConstructor_habit()
{
Habit model = new Habit();
model.archived = 1;
model.highlight = 1;
model.color = 0;
model.freqNum = 10;
model.freqDen = 20;
model.reminderDays = 1;
model.reminderHour = 8;
model.reminderMin = 30;
model.position = 0;
Habit habit = new Habit(model);
assertThat(habit.archived, is(model.archived));
assertThat(habit.highlight, is(model.highlight));
assertThat(habit.color, is(model.color));
assertThat(habit.freqNum, is(model.freqNum));
assertThat(habit.freqDen, is(model.freqDen));
assertThat(habit.reminderDays, is(model.reminderDays));
assertThat(habit.reminderHour, is(model.reminderHour));
assertThat(habit.reminderMin, is(model.reminderMin));
assertThat(habit.position, is(model.position));
}
@Test
public void test_get_withValidId()
{
Habit habit = new Habit();
habit.save();
Habit habit2 = Habit.get(habit.getId());
assertThat(habit, equalTo(habit2));
}
@Test
public void test_get_withInvalidId()
{
Habit habit = Habit.get(123456L);
assertThat(habit, is(nullValue()));
}
@Test
public void test_getAll_withoutArchived()
{
List<Habit> habits = new LinkedList<>();
List<Habit> habitsWithArchived = new LinkedList<>();
for(int i = 0; i < 10; i++)
{
Habit h = new Habit();
if(i % 2 == 0)
h.archived = 1;
else
habits.add(h);
habitsWithArchived.add(h);
h.save();
}
assertThat(habits, equalTo(Habit.getAll(false)));
assertThat(habitsWithArchived, equalTo(Habit.getAll(true)));
}
@Test
public void test_getByPosition()
{
List<Habit> habits = new LinkedList<>();
for(int i = 0; i < 10; i++)
{
Habit h = new Habit();
h.save();
habits.add(h);
}
for(int i = 0; i < 10; i++)
{
Habit h = Habit.getByPosition(i);
if(h == null) fail();
assertThat(h, equalTo(habits.get(i)));
}
}
@Test
public void test_count()
{
for(int i = 0; i < 10; i++)
{
Habit h = new Habit();
if(i % 2 == 0) h.archived = 1;
h.save();
}
assertThat(Habit.count(), equalTo(5));
}
@Test
public void test_countWithArchived()
{
for(int i = 0; i < 10; i++)
{
Habit h = new Habit();
if(i % 2 == 0) h.archived = 1;
h.save();
}
assertThat(Habit.countWithArchived(), equalTo(10));
}
@Test
public void test_updateId()
{
Habit habit = new Habit();
habit.name = "Hello World";
habit.save();
Long oldId = habit.getId();
Long newId = 123456L;
Habit.updateId(oldId, newId);
Habit newHabit = Habit.get(newId);
if(newHabit == null) fail();
assertThat(newHabit, is(not(nullValue())));
assertThat(newHabit.name, equalTo(habit.name));
}
@Test
public void test_reorder()
{
List<Long> ids = new LinkedList<>();
int n = 10;
for (int i = 0; i < n; i++)
{
Habit h = new Habit();
h.save();
ids.add(h.getId());
assertThat(h.position, is(i));
}
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 = Habit.getByPosition(from);
Habit toHabit = Habit.getByPosition(to);
Habit.reorder(fromHabit, toHabit);
int actualPositions[] = new int[n];
for (int j = 0; j < n; j++)
{
Habit h = Habit.get(ids.get(j));
if (h == null) fail();
actualPositions[j] = h.position;
}
assertThat(actualPositions, equalTo(expectedPosition[i]));
}
}
@Test
public void test_rebuildOrder()
{
List<Long> ids = new LinkedList<>();
int originalPositions[] = { 0, 1, 1, 4, 6, 8, 10, 10, 13};
for (int p : originalPositions)
{
Habit h = new Habit();
h.position = p;
h.save();
ids.add(h.getId());
}
Habit.rebuildOrder();
for (int i = 0; i < originalPositions.length; i++)
{
Habit h = Habit.get(ids.get(i));
if(h == null) fail();
assertThat(h.position, is(i));
}
}
@Test
public void test_getHabitsWithReminder()
{
List<Habit> habitsWithReminder = new LinkedList<>();
for(int i = 0; i < 10; i++)
{
Habit habit = new Habit();
if(i % 2 == 0)
{
habit.reminderDays = DateUtils.ALL_WEEK_DAYS;
habit.reminderHour = 8;
habit.reminderMin = 30;
habitsWithReminder.add(habit);
}
habit.save();
}
assertThat(habitsWithReminder, equalTo(Habit.getHabitsWithReminder()));
}
@Test
public void test_archive_unarchive()
{
List<Habit> allHabits = new LinkedList<>();
List<Habit> archivedHabits = new LinkedList<>();
List<Habit> unarchivedHabits = new LinkedList<>();
for(int i = 0; i < 10; i++)
{
Habit habit = new Habit();
habit.save();
allHabits.add(habit);
if(i % 2 == 0)
archivedHabits.add(habit);
else
unarchivedHabits.add(habit);
}
Habit.archive(archivedHabits);
assertThat(Habit.getAll(false), equalTo(unarchivedHabits));
assertThat(Habit.getAll(true), equalTo(allHabits));
Habit.unarchive(archivedHabits);
assertThat(Habit.getAll(false), equalTo(allHabits));
assertThat(Habit.getAll(true), equalTo(allHabits));
}
@Test
public void test_setColor()
{
List<Habit> habits = new LinkedList<>();
for(int i = 0; i < 10; i++)
{
Habit habit = new Habit();
habit.color = i;
habit.save();
habits.add(habit);
}
int newColor = 100;
Habit.setColor(habits, newColor);
for(Habit h : habits)
assertThat(h.color, equalTo(newColor));
}
@Test
public void test_hasReminder_clearReminder()
{
Habit h = new Habit();
assertThat(h.hasReminder(), is(false));
h.reminderDays = DateUtils.ALL_WEEK_DAYS;
h.reminderHour = 8;
h.reminderMin = 30;
assertThat(h.hasReminder(), is(true));
h.clearReminder();
assertThat(h.hasReminder(), is(false));
}
@Test
public void test_writeCSV() throws IOException
{
HabitFixtures.createEmptyHabit();
HabitFixtures.createShortHabit();
String expectedCSV =
"Position,Name,Description,NumRepetitions,Interval,Color\n" +
"001,Meditate,Did you meditate this morning?,1,1,#AFB42B\n" +
"002,Wake up early,Did you wake up before 6am?,2,3,#00897B\n";
StringWriter writer = new StringWriter();
Habit.writeCSV(Habit.getAll(true), writer);
MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV));
}
}

@ -1,191 +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.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Random;
import static junit.framework.Assert.assertFalse;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RepetitionListTest extends BaseAndroidTest
{
private Habit habit;
private Habit emptyHabit;
@Before
public void setUp()
{
super.setUp();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createShortHabit();
emptyHabit = HabitFixtures.createEmptyHabit();
}
@After
public void tearDown()
{
DateUtils.setFixedLocalTime(null);
}
@Test
public void test_contains()
{
long current = DateUtils.getStartOfToday();
for(boolean b : HabitFixtures.NON_DAILY_HABIT_CHECKS)
{
assertThat(habit.repetitions.contains(current), equalTo(b));
current -= DateUtils.millisecondsInOneDay;
}
for(int i = 0; i < 3; i++)
{
assertThat(habit.repetitions.contains(current), equalTo(false));
current -= DateUtils.millisecondsInOneDay;
}
}
@Test
public void test_delete()
{
long timestamp = DateUtils.getStartOfToday();
assertThat(habit.repetitions.contains(timestamp), equalTo(true));
habit.repetitions.delete(timestamp);
assertThat(habit.repetitions.contains(timestamp), equalTo(false));
}
@Test
public void test_toggle()
{
long timestamp = DateUtils.getStartOfToday();
assertThat(habit.repetitions.contains(timestamp), equalTo(true));
habit.repetitions.toggle(timestamp);
assertThat(habit.repetitions.contains(timestamp), equalTo(false));
habit.repetitions.toggle(timestamp);
assertThat(habit.repetitions.contains(timestamp), equalTo(true));
}
@Test
public void test_getWeekDayFrequency()
{
Random random = new Random();
Integer weekdayCount[][] = new Integer[12][7];
Integer monthCount[] = new Integer[12];
Arrays.fill(monthCount, 0);
for(Integer row[] : weekdayCount)
Arrays.fill(row, 0);
GregorianCalendar day = DateUtils.getStartOfTodayCalendar();
// Sets the current date to the end of November
day.set(2015, 10, 30);
DateUtils.setFixedLocalTime(day.getTimeInMillis());
// Add repetitions randomly from January to December
// Leaves the month of March empty, to check that it returns null
day.set(2015, 0, 1);
for(int i = 0; i < 365; i ++)
{
if(random.nextBoolean())
{
int month = day.get(Calendar.MONTH);
int week = day.get(Calendar.DAY_OF_WEEK) % 7;
if(month != 2)
{
if (month <= 10)
{
weekdayCount[month][week]++;
monthCount[month]++;
}
emptyHabit.repetitions.toggle(day.getTimeInMillis());
}
}
day.add(Calendar.DAY_OF_YEAR, 1);
}
HashMap<Long, Integer[]> freq = emptyHabit.repetitions.getWeekdayFrequency();
// Repetitions until November should be counted correctly
for(int month = 0; month < 11; month++)
{
day.set(2015, month, 1);
Integer actualCount[] = freq.get(day.getTimeInMillis());
if(monthCount[month] == 0)
assertThat(actualCount, equalTo(null));
else
assertThat(actualCount, equalTo(weekdayCount[month]));
}
// Repetitions in December should be discarded
day.set(2015, 11, 1);
assertThat(freq.get(day.getTimeInMillis()), equalTo(null));
}
@Test
public void test_count()
{
long to = DateUtils.getStartOfToday();
long from = to - 9 * DateUtils.millisecondsInOneDay;
assertThat(habit.repetitions.count(from, to), equalTo(6));
to = DateUtils.getStartOfToday() - DateUtils.millisecondsInOneDay;
from = to - 5 * DateUtils.millisecondsInOneDay;
assertThat(habit.repetitions.count(from, to), equalTo(3));
}
@Test
public void test_getOldest()
{
long expectedOldestTimestamp = DateUtils.getStartOfToday() - 9 * DateUtils.millisecondsInOneDay;
assertThat(habit.repetitions.getOldestTimestamp(), equalTo(expectedOldestTimestamp));
Repetition oldest = habit.repetitions.getOldest();
assertFalse(oldest == null);
assertThat(oldest.timestamp, equalTo(expectedOldestTimestamp));
}
}

@ -23,11 +23,9 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -50,8 +48,8 @@ public class ScoreListTest extends BaseAndroidTest
{
super.setUp();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createEmptyHabit();
habitFixtures.purgeHabits(habitList);
habit = habitFixtures.createEmptyHabit();
}
@After
@ -61,38 +59,53 @@ public class ScoreListTest extends BaseAndroidTest
}
@Test
public void test_invalidateNewerThan()
public void test_getAllValues_withGroups()
{
assertThat(habit.scores.getTodayValue(), equalTo(0));
toggleRepetitions(0, 2);
assertThat(habit.scores.getTodayValue(), equalTo(1948077));
toggleRepetitions(0, 20);
habit.freqNum = 1;
habit.freqDen = 2;
habit.scores.invalidateNewerThan(0);
int expectedValues[] = {11434978, 7894999, 3212362};
assertThat(habit.scores.getTodayValue(), equalTo(1974654));
int actualValues[] = habit.getScores().getAllValues(7);
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getTodayStarValue()
public void test_getAllValues_withoutGroups()
{
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.EMPTY_STAR));
int k = 0;
while(habit.scores.getTodayValue() < Score.HALF_STAR_CUTOFF) toggleRepetitions(k, ++k);
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.HALF_STAR));
toggleRepetitions(0, 20);
while(habit.scores.getTodayValue() < Score.FULL_STAR_CUTOFF) toggleRepetitions(k, ++k);
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.FULL_STAR));
int expectedValues[] = {
12629351,
12266245,
11883254,
11479288,
11053198,
10603773,
10129735,
9629735,
9102352,
8546087,
7959357,
7340494,
6687738,
5999234,
5273023,
4507040,
3699107,
2846927,
1948077,
1000000
};
int actualValues[] = habit.getScores().getAllValues(1);
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getTodayValue()
{
toggleRepetitions(0, 20);
assertThat(habit.scores.getTodayValue(), equalTo(12629351));
assertThat(habit.getScores().getTodayValue(), equalTo(12629351));
}
@Test
@ -100,50 +113,60 @@ public class ScoreListTest extends BaseAndroidTest
{
toggleRepetitions(0, 20);
int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773,
10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023,
4507040, 3699107, 2846927, 1948077, 1000000 };
int expectedValues[] = {
12629351,
12266245,
11883254,
11479288,
11053198,
10603773,
10129735,
9629735,
9102352,
8546087,
7959357,
7340494,
6687738,
5999234,
5273023,
4507040,
3699107,
2846927,
1948077,
1000000
};
long current = DateUtils.getStartOfToday();
for(int expectedValue : expectedValues)
for (int expectedValue : expectedValues)
{
assertThat(habit.scores.getValue(current), equalTo(expectedValue));
assertThat(habit.getScores().getValue(current),
equalTo(expectedValue));
current -= DateUtils.millisecondsInOneDay;
}
}
@Test
public void test_getAllValues_withoutGroups()
public void test_invalidateNewerThan()
{
toggleRepetitions(0, 20);
int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773,
10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023,
4507040, 3699107, 2846927, 1948077, 1000000 };
int actualValues[] = habit.scores.getAllValues(1);
assertThat(actualValues, equalTo(expectedValues));
}
assertThat(habit.getScores().getTodayValue(), equalTo(0));
@Test
public void test_getAllValues_withGroups()
{
toggleRepetitions(0, 20);
toggleRepetitions(0, 2);
assertThat(habit.getScores().getTodayValue(), equalTo(1948077));
int expectedValues[] = { 11434978, 7894999, 3212362 };
habit.setFreqNum(1);
habit.setFreqDen(2);
habit.getScores().invalidateNewerThan(0);
int actualValues[] = habit.scores.getAllValues(7);
assertThat(actualValues, equalTo(expectedValues));
assertThat(habit.getScores().getTodayValue(), equalTo(1974654));
}
@Test
public void test_writeCSV() throws IOException
{
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createShortHabit();
habitFixtures.purgeHabits(habitList);
Habit habit = habitFixtures.createShortHabit();
String expectedCSV =
"2015-01-16,0.0519\n" +
String expectedCSV = "2015-01-16,0.0519\n" +
"2015-01-17,0.1021\n" +
"2015-01-18,0.0986\n" +
"2015-01-19,0.0952\n" +
@ -155,22 +178,19 @@ public class ScoreListTest extends BaseAndroidTest
"2015-01-25,0.2649\n";
StringWriter writer = new StringWriter();
habit.scores.writeCSV(writer);
habit.getScores().writeCSV(writer);
assertThat(writer.toString(), equalTo(expectedCSV));
}
private void toggleRepetitions(final int from, final int to)
{
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command()
{
@Override
public void execute()
{
DatabaseUtils.executeAsTransaction(() -> {
long today = DateUtils.getStartOfToday();
for (int i = from; i < to; i++)
habit.repetitions.toggle(today - i * DateUtils.millisecondsInOneDay);
}
habit
.getRepetitions()
.toggleTimestamp(today - i * DateUtils.millisecondsInOneDay);
});
}
}

@ -36,6 +36,7 @@ import static org.junit.Assert.assertThat;
@SmallTest
public class ScoreTest extends BaseAndroidTest
{
@Override
@Before
public void setUp()
{
@ -61,48 +62,24 @@ public class ScoreTest extends BaseAndroidTest
assertThat(Score.compute(1, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775));
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(
Score.MAX_VALUE));
}
@Test
public void test_compute_withNonDailyHabit()
{
int checkmark = Checkmark.CHECKED_EXPLICITLY;
assertThat(Score.compute(1/3.0, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1/3.0, 5000000, checkmark), equalTo(5916180));
assertThat(Score.compute(1/3.0, 10000000, checkmark), equalTo(10832360));
assertThat(Score.compute(1/3.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
assertThat(Score.compute(1/7.0, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1/7.0, 5000000, checkmark), equalTo(5964398));
assertThat(Score.compute(1/7.0, 10000000, checkmark), equalTo(10928796));
assertThat(Score.compute(1/7.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
}
@Test
public void test_getStarStatus()
{
Score s = new Score();
s.score = Score.FULL_STAR_CUTOFF + 1;
assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR));
s.score = Score.FULL_STAR_CUTOFF;
assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR));
s.score = Score.FULL_STAR_CUTOFF - 1;
assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
s.score = Score.HALF_STAR_CUTOFF + 1;
assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
s.score = Score.HALF_STAR_CUTOFF;
assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
s.score = Score.HALF_STAR_CUTOFF - 1;
assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR));
s.score = 0;
assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR));
assertThat(Score.compute(1 / 3.0, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1 / 3.0, 5000000, checkmark), equalTo(5916180));
assertThat(Score.compute(1 / 3.0, 10000000, checkmark), equalTo(10832360));
assertThat(Score.compute(1 / 3.0, Score.MAX_VALUE, checkmark), equalTo(
Score.MAX_VALUE));
assertThat(Score.compute(1 / 7.0, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1 / 7.0, 5000000, checkmark), equalTo(5964398));
assertThat(Score.compute(1 / 7.0, 10000000, checkmark), equalTo(10928796));
assertThat(Score.compute(1 / 7.0, Score.MAX_VALUE, checkmark), equalTo(
Score.MAX_VALUE));
}
}

@ -52,21 +52,16 @@ public class ExportCSVTaskTest extends BaseAndroidTest
@Test
public void testExportCSV() throws Throwable
{
HabitFixtures.createShortHabit();
List<Habit> habits = Habit.getAll(true);
habitFixtures.createShortHabit();
List<Habit> habits = habitList.getAll(true);
ExportCSVTask task = new ExportCSVTask(habits, null);
task.setListener(new ExportCSVTask.Listener()
{
@Override
public void onExportCSVFinished(String archiveFilename)
{
task.setListener(archiveFilename -> {
assertThat(archiveFilename, is(not(nullValue())));
File f = new File(archiveFilename);
assertTrue(f.exists());
assertTrue(f.canRead());
}
});
task.execute();

@ -40,8 +40,10 @@ public class CheckmarkButtonViewTest extends ViewTest
public static final String PATH = "ui/habits/list/CheckmarkButtonView/";
private CountDownLatch latch;
private CheckmarkButtonView view;
@Override
@Before
public void setUp()
{
@ -51,24 +53,23 @@ public class CheckmarkButtonViewTest extends ViewTest
latch = new CountDownLatch(1);
view = new CheckmarkButtonView(targetContext);
view.setValue(Checkmark.UNCHECKED);
view.setColor(ColorUtils.CSV_PALETTE[7]);
view.setColor(ColorUtils.getAndroidTestColor(7));
measureView(dpToPixels(40), dpToPixels(40), view);
}
protected void assertRendersCheckedExplicitly() throws IOException
{
assertRenders(view, PATH + "render_explicit_check.png");
}
protected void assertRendersUnchecked() throws IOException
@Test
public void testRender_explicitCheck() throws Exception
{
assertRenders(view, PATH + "render_unchecked.png");
view.setValue(Checkmark.CHECKED_EXPLICITLY);
assertRendersCheckedExplicitly();
}
protected void assertRendersCheckedImplicitly() throws IOException
@Test
public void testRender_implicitCheck() throws Exception
{
assertRenders(view, PATH + "render_implicit_check.png");
view.setValue(Checkmark.CHECKED_IMPLICITLY);
assertRendersCheckedImplicitly();
}
@Test
@ -78,18 +79,19 @@ public class CheckmarkButtonViewTest extends ViewTest
assertRendersUnchecked();
}
@Test
public void testRender_explicitCheck() throws Exception
protected void assertRendersCheckedExplicitly() throws IOException
{
view.setValue(Checkmark.CHECKED_EXPLICITLY);
assertRendersCheckedExplicitly();
assertRenders(view, PATH + "render_explicit_check.png");
}
@Test
public void testRender_implicitCheck() throws Exception
protected void assertRendersCheckedImplicitly() throws IOException
{
view.setValue(Checkmark.CHECKED_IMPLICITLY);
assertRendersCheckedImplicitly();
assertRenders(view, PATH + "render_implicit_check.png");
}
protected void assertRendersUnchecked() throws IOException
{
assertRenders(view, PATH + "render_unchecked.png");
}
// @Test

@ -54,13 +54,14 @@ public class CheckmarkPanelViewTest extends ViewTest
Habit habit = new Habit();
latch = new CountDownLatch(1);
checkmarks = new int[]{Checkmark.CHECKED_EXPLICITLY, Checkmark.UNCHECKED,
checkmarks = new int[]{
Checkmark.CHECKED_EXPLICITLY, Checkmark.UNCHECKED,
Checkmark.CHECKED_IMPLICITLY, Checkmark.CHECKED_EXPLICITLY};
view = new CheckmarkPanelView(targetContext);
view.setHabit(habit);
view.setCheckmarkValues(checkmarks);
view.setColor(ColorUtils.CSV_PALETTE[7]);
view.setColor(ColorUtils.getAndroidTestColor(7));
measureView(dpToPixels(200), dpToPixels(200), view);
}

@ -23,11 +23,10 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.views.CheckmarkWidgetView;
import org.isoron.uhabits.widgets.views.CheckmarkWidgetView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -39,6 +38,7 @@ import java.io.IOException;
public class CheckmarkWidgetViewTest extends ViewTest
{
private CheckmarkWidgetView view;
private Habit habit;
@Before
@ -47,7 +47,7 @@ public class CheckmarkWidgetViewTest extends ViewTest
super.setUp();
InterfaceUtils.setFixedTheme(R.style.TransparentWidgetTheme);
habit = HabitFixtures.createShortHabit();
habit = habitFixtures.createShortHabit();
view = new CheckmarkWidgetView(targetContext);
view.setHabit(habit);
refreshData(view);
@ -60,23 +60,14 @@ public class CheckmarkWidgetViewTest extends ViewTest
assertRenders(view, "CheckmarkView/checked.png");
}
@Test
public void testRender_unchecked() throws IOException
{
habit.repetitions.toggle(DateUtils.getStartOfToday());
view.refreshData();
assertRenders(view, "CheckmarkView/unchecked.png");
}
@Test
public void testRender_implicitlyChecked() throws IOException
{
long today = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
habit.repetitions.toggle(today);
habit.repetitions.toggle(today - day);
habit.repetitions.toggle(today - 2 * day);
habit.getRepetitions().toggleTimestamp(today);
habit.getRepetitions().toggleTimestamp(today - day);
habit.getRepetitions().toggleTimestamp(today - 2 * day);
view.refreshData();
assertRenders(view, "CheckmarkView/implicitly_checked.png");
@ -88,4 +79,13 @@ public class CheckmarkWidgetViewTest extends ViewTest
measureView(dpToPixels(300), dpToPixels(300), view);
assertRenders(view, "CheckmarkView/large_size.png");
}
@Test
public void testRender_unchecked() throws IOException
{
habit.getRepetitions().toggleTimestamp(DateUtils.getStartOfToday());
view.refreshData();
assertRenders(view, "CheckmarkView/unchecked.png");
}
}

@ -23,8 +23,7 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.views.HabitFrequencyView;
import org.isoron.uhabits.ui.habits.show.views.HabitFrequencyView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -40,8 +39,8 @@ public class HabitFrequencyViewTest extends ViewTest
{
super.setUp();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();
habitFixtures.purgeHabits(habitList);
Habit habit = habitFixtures.createLongHabit();
view = new HabitFrequencyView(targetContext);
view.setHabit(habit);
@ -56,10 +55,12 @@ public class HabitFrequencyViewTest extends ViewTest
}
@Test
public void testRender_withTransparentBackground() throws Throwable
public void testRender_withDataOffset() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitFrequencyView/renderTransparent.png");
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, "HabitFrequencyView/renderDataOffset.png");
}
@Test
@ -70,11 +71,9 @@ public class HabitFrequencyViewTest extends ViewTest
}
@Test
public void testRender_withDataOffset() throws Throwable
public void testRender_withTransparentBackground() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, "HabitFrequencyView/renderDataOffset.png");
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitFrequencyView/renderTransparent.png");
}
}

@ -22,10 +22,9 @@ package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.views.HabitHistoryView;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.ui.habits.show.views.HabitHistoryView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -40,6 +39,7 @@ import static org.hamcrest.Matchers.equalTo;
public class HabitHistoryViewTest extends ViewTest
{
private Habit habit;
private HabitHistoryView view;
@Before
@ -47,8 +47,8 @@ public class HabitHistoryViewTest extends ViewTest
{
super.setUp();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createLongHabit();
habitFixtures.purgeHabits(habitList);
habit = habitFixtures.createLongHabit();
view = new HabitHistoryView(targetContext);
view.setHabit(habit);
@ -57,69 +57,69 @@ public class HabitHistoryViewTest extends ViewTest
}
@Test
public void testRender() throws Throwable
public void tapDate_atInvalidLocations() throws Throwable
{
assertRenders(view, "HabitHistoryView/render.png");
}
int expectedCheckmarkValues[] = habit.getCheckmarks().getAllValues();
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitHistoryView/renderTransparent.png");
}
view.setIsEditable(true);
tap(view, 118, 13); // header
tap(view, 336, 60); // tomorrow's square
tap(view, 370, 60); // right axis
waitForAsyncTasks();
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "HabitHistoryView/renderDifferentSize.png");
int actualCheckmarkValues[] = habit.getCheckmarks().getAllValues();
assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues));
}
@Test
public void testRender_withDataOffset() throws Throwable
public void tapDate_withEditableView() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
view.setIsEditable(true);
tap(view, 340, 40); // today's square
waitForAsyncTasks();
assertRenders(view, "HabitHistoryView/renderDataOffset.png");
long today = DateUtils.getStartOfToday();
assertFalse(habit.getRepetitions().containsTimestamp(today));
}
@Test
public void tapDate_withEditableView() throws Throwable
public void tapDate_withReadOnlyView() throws Throwable
{
view.setIsEditable(true);
view.setIsEditable(false);
tap(view, 340, 40); // today's square
waitForAsyncTasks();
long today = DateUtils.getStartOfToday();
assertFalse(habit.repetitions.contains(today));
assertTrue(habit.getRepetitions().containsTimestamp(today));
}
@Test
public void tapDate_atInvalidLocations() throws Throwable
public void testRender() throws Throwable
{
int expectedCheckmarkValues[] = habit.checkmarks.getAllValues();
assertRenders(view, "HabitHistoryView/render.png");
}
view.setIsEditable(true);
tap(view, 118, 13); // header
tap(view, 336, 60); // tomorrow's square
tap(view, 370, 60); // right axis
waitForAsyncTasks();
@Test
public void testRender_withDataOffset() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
int actualCheckmarkValues[] = habit.checkmarks.getAllValues();
assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues));
assertRenders(view, "HabitHistoryView/renderDataOffset.png");
}
@Test
public void tapDate_withReadOnlyView() throws Throwable
public void testRender_withDifferentSize() throws Throwable
{
view.setIsEditable(false);
tap(view, 340, 40); // today's square
waitForAsyncTasks();
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "HabitHistoryView/renderDifferentSize.png");
}
long today = DateUtils.getStartOfToday();
assertTrue(habit.repetitions.contains(today));
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitHistoryView/renderTransparent.png");
}
}

@ -24,8 +24,7 @@ import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.views.HabitScoreView;
import org.isoron.uhabits.ui.habits.show.views.HabitScoreView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -35,6 +34,7 @@ import org.junit.runner.RunWith;
public class HabitScoreViewTest extends ViewTest
{
private Habit habit;
private HabitScoreView view;
@Before
@ -42,8 +42,8 @@ public class HabitScoreViewTest extends ViewTest
{
super.setUp();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createLongHabit();
habitFixtures.purgeHabits(habitList);
habit = habitFixtures.createLongHabit();
view = new HabitScoreView(targetContext);
view.setHabit(habit);
@ -55,15 +55,18 @@ public class HabitScoreViewTest extends ViewTest
@Test
public void testRender() throws Throwable
{
Log.d("HabitScoreViewTest", String.format("height=%d", dpToPixels(100)));
Log.d("HabitScoreViewTest",
String.format("height=%d", dpToPixels(100)));
assertRenders(view, "HabitScoreView/render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
public void testRender_withDataOffset() throws Throwable
{
view.setIsTransparencyEnabled(true);
assertRenders(view, "HabitScoreView/renderTransparent.png");
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, "HabitScoreView/renderDataOffset.png");
}
@Test
@ -73,15 +76,6 @@ public class HabitScoreViewTest extends ViewTest
assertRenders(view, "HabitScoreView/renderDifferentSize.png");
}
@Test
public void testRender_withDataOffset() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, "HabitScoreView/renderDataOffset.png");
}
@Test
public void testRender_withMonthlyBucket() throws Throwable
{
@ -92,6 +86,13 @@ public class HabitScoreViewTest extends ViewTest
assertRenders(view, "HabitScoreView/renderMonthly.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsTransparencyEnabled(true);
assertRenders(view, "HabitScoreView/renderTransparent.png");
}
@Test
public void testRender_withYearlyBucket() throws Throwable
{

@ -23,8 +23,7 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.views.HabitStreakView;
import org.isoron.uhabits.ui.habits.show.views.HabitStreakView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -35,13 +34,14 @@ public class HabitStreakViewTest extends ViewTest
{
private HabitStreakView view;
@Override
@Before
public void setUp()
{
super.setUp();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();
habitFixtures.purgeHabits(habitList);
Habit habit = habitFixtures.createLongHabit();
view = new HabitStreakView(targetContext);
measureView(dpToPixels(300), dpToPixels(100), view);
@ -56,13 +56,6 @@ public class HabitStreakViewTest extends ViewTest
assertRenders(view, "HabitStreakView/render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitStreakView/renderTransparent.png");
}
@Test
public void testRender_withSmallSize() throws Throwable
{
@ -71,4 +64,11 @@ public class HabitStreakViewTest extends ViewTest
assertRenders(view, "HabitStreakView/renderSmallSize.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitStreakView/renderTransparent.png");
}
}

@ -1,77 +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.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.views.NumberView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class NumberViewTest extends ViewTest
{
private NumberView view;
@Before
public void setUp()
{
super.setUp();
view = new NumberView(targetContext);
view.setLabel("Hello world");
view.setNumber(31);
view.setColor(ColorUtils.CSV_PALETTE[0]);
measureView(dpToPixels(100), dpToPixels(100), view);
}
@Test
public void testRender_base() throws IOException
{
assertRenders(view, "NumberView/render.png");
}
@Test
public void testRender_withLongLabel() throws IOException
{
view.setLabel("The quick brown fox jumps over the lazy fox");
measureView(dpToPixels(100), dpToPixels(100), view);
assertRenders(view, "NumberView/renderLongLabel.png");
}
@Test
public void testRender_withDifferentParams() throws IOException
{
view.setNumber(500);
view.setColor(ColorUtils.CSV_PALETTE[5]);
view.setTextSize(targetContext.getResources().getDimension(R.dimen.tinyTextSize));
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "NumberView/renderDifferentParams.png");
}
}

@ -24,7 +24,7 @@ import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.utils.ColorUtils;
import org.isoron.uhabits.views.RingView;
import org.isoron.uhabits.ui.habits.show.views.RingView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -45,7 +45,7 @@ public class RingViewTest extends ViewTest
view = new RingView(targetContext);
view.setPercentage(0.6f);
view.setText("60%");
view.setColor(ColorUtils.CSV_PALETTE[0]);
view.setColor(ColorUtils.getAndroidTestColor(0));
view.setBackgroundColor(Color.WHITE);
view.setThickness(dpToPixels(3));
}
@ -61,7 +61,7 @@ public class RingViewTest extends ViewTest
public void testRender_withDifferentParams() throws IOException
{
view.setPercentage(0.25f);
view.setColor(ColorUtils.CSV_PALETTE[5]);
view.setColor(ColorUtils.getAndroidTestColor(5));
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "RingView/renderDifferentParams.png");

@ -30,7 +30,7 @@ import org.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.FileUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.views.HabitDataView;
import org.isoron.uhabits.ui.habits.show.views.HabitDataView;
import java.io.File;
import java.io.FileOutputStream;

@ -23,6 +23,9 @@ import javax.inject.Singleton;
import dagger.Component;
/**
* Dependency injection component for classes that are specific to Android.
*/
@Singleton
@Component(modules = {AndroidModule.class})
public interface AndroidComponent extends BaseComponent

@ -20,6 +20,10 @@
package org.isoron.uhabits;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.models.ModelFactory;
import org.isoron.uhabits.models.sqlite.SQLModelFactory;
import org.isoron.uhabits.models.sqlite.SQLiteHabitList;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListCache;
import org.isoron.uhabits.utils.Preferences;
@ -28,27 +32,46 @@ import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
/**
* Module that provides dependencies when the application is running on
* Android.
* <p>
* This module is also used for instrumented tests.
*/
@Module
public class AndroidModule
{
@Provides
@Singleton
Preferences providePreferences()
CommandRunner provideCommandRunner()
{
return new Preferences();
return new CommandRunner();
}
@Provides
@Singleton
CommandRunner provideCommandRunner()
HabitCardListCache provideHabitCardListCache()
{
return new CommandRunner();
return new HabitCardListCache();
}
@Provides
@Singleton
HabitCardListCache provideHabitCardListCache()
HabitList provideHabitList()
{
return new HabitCardListCache();
return SQLiteHabitList.getInstance();
}
@Provides
ModelFactory provideModelFactory()
{
return new SQLModelFactory();
}
@Provides
@Singleton
Preferences providePreferences()
{
return new Preferences();
}
}

@ -19,16 +19,34 @@
package org.isoron.uhabits;
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.commands.CreateHabitCommand;
import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.commands.EditHabitCommand;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.io.AbstractImporter;
import org.isoron.uhabits.io.HabitsCSVExporter;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.ToggleRepetitionTask;
import org.isoron.uhabits.ui.BaseSystem;
import org.isoron.uhabits.ui.habits.edit.BaseDialogFragment;
import org.isoron.uhabits.ui.habits.edit.HistoryEditorDialog;
import org.isoron.uhabits.ui.habits.list.ListHabitsActivity;
import org.isoron.uhabits.ui.habits.list.ListHabitsController;
import org.isoron.uhabits.ui.habits.list.ListHabitsSelectionMenu;
import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter;
import org.isoron.uhabits.ui.habits.list.model.HabitCardListCache;
import org.isoron.uhabits.ui.habits.list.ListHabitsController;
import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController;
import org.isoron.uhabits.ui.habits.list.model.HintList;
import org.isoron.uhabits.ui.habits.list.views.CheckmarkPanelView;
import org.isoron.uhabits.ui.habits.show.ShowHabitActivity;
import org.isoron.uhabits.widgets.BaseWidgetProvider;
import org.isoron.uhabits.widgets.HabitPickerDialog;
/**
* Base component for dependency injection.
*/
public interface BaseComponent
{
void inject(CheckmarkButtonController checkmarkButtonController);
@ -50,4 +68,36 @@ public interface BaseComponent
void inject(HintList hintList);
void inject(HabitCardListAdapter habitCardListAdapter);
void inject(ArchiveHabitsCommand archiveHabitsCommand);
void inject(ChangeHabitColorCommand changeHabitColorCommand);
void inject(UnarchiveHabitsCommand unarchiveHabitsCommand);
void inject(EditHabitCommand editHabitCommand);
void inject(CreateHabitCommand createHabitCommand);
void inject(HabitPickerDialog habitPickerDialog);
void inject(BaseWidgetProvider baseWidgetProvider);
void inject(ShowHabitActivity showHabitActivity);
void inject(DeleteHabitsCommand deleteHabitsCommand);
void inject(ListHabitsActivity listHabitsActivity);
void inject(BaseSystem baseSystem);
void inject(HistoryEditorDialog historyEditorDialog);
void inject(HabitsApplication application);
void inject(Habit habit);
void inject(AbstractImporter abstractImporter);
void inject(HabitsCSVExporter habitsCSVExporter);
}

@ -40,6 +40,7 @@ import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.ui.habits.show.ShowHabitActivity;
import org.isoron.uhabits.utils.DateUtils;
@ -50,12 +51,26 @@ import java.util.Date;
import javax.inject.Inject;
/**
* The Android BroadacastReceiver for Loop Habit Tracker.
* <p>
* Currently, all broadcast messages are received and processed by this class.
*/
public class HabitBroadcastReceiver extends BroadcastReceiver
{
public static final String ACTION_CHECK = "org.isoron.uhabits.ACTION_CHECK";
public static final String ACTION_DISMISS = "org.isoron.uhabits.ACTION_DISMISS";
public static final String ACTION_SHOW_REMINDER = "org.isoron.uhabits.ACTION_SHOW_REMINDER";
public static final String ACTION_SNOOZE = "org.isoron.uhabits.ACTION_SNOOZE";
public static final String ACTION_DISMISS =
"org.isoron.uhabits.ACTION_DISMISS";
public static final String ACTION_SHOW_REMINDER =
"org.isoron.uhabits.ACTION_SHOW_REMINDER";
public static final String ACTION_SNOOZE =
"org.isoron.uhabits.ACTION_SNOOZE";
@Inject
HabitList habitList;
@Inject
CommandRunner commandRunner;
@ -66,6 +81,68 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
HabitsApplication.getComponent().inject(this);
}
public static PendingIntent buildCheckIntent(Context context,
Habit habit,
Long timestamp)
{
Uri data = habit.getUri();
Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class);
checkIntent.setData(data);
checkIntent.setAction(ACTION_CHECK);
if (timestamp != null) checkIntent.putExtra("timestamp", timestamp);
return PendingIntent.getBroadcast(context, 0, checkIntent,
PendingIntent.FLAG_ONE_SHOT);
}
public static PendingIntent buildDismissIntent(Context context)
{
Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class);
deleteIntent.setAction(ACTION_DISMISS);
return PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
}
public static PendingIntent buildSnoozeIntent(Context context, Habit habit)
{
Uri data = habit.getUri();
Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class);
snoozeIntent.setData(data);
snoozeIntent.setAction(ACTION_SNOOZE);
return PendingIntent.getBroadcast(context, 0, snoozeIntent, 0);
}
public static PendingIntent buildViewHabitIntent(Context context,
Habit habit)
{
Intent intent = new Intent(context, ShowHabitActivity.class);
intent.setData(
Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId()));
return TaskStackBuilder
.create(context.getApplicationContext())
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
public static void dismissNotification(Context context, Habit habit)
{
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
}
public static void sendRefreshBroadcast(Context context)
{
LocalBroadcastManager manager =
LocalBroadcastManager.getInstance(context);
Intent refreshIntent = new Intent(HabitsApplication.ACTION_REFRESH);
manager.sendBroadcast(refreshIntent);
WidgetManager.updateWidgets(context);
}
@Override
public void onReceive(final Context context, Intent intent)
{
@ -89,40 +166,23 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
break;
case Intent.ACTION_BOOT_COMPLETED:
ReminderUtils.createReminderAlarms(context);
ReminderUtils.createReminderAlarms(context, habitList);
break;
}
}
private void createReminderAlarmsDelayed(final Context context)
{
new Handler().postDelayed(() -> ReminderUtils.createReminderAlarms(context), 5000);
}
private void snoozeHabit(Context context, Intent intent)
{
Uri data = intent.getData();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
long delayMinutes = Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
long habitId = ContentUris.parseId(data);
Habit habit = Habit.get(habitId);
if(habit != null)
ReminderUtils.createReminderAlarm(context, habit,
new Date().getTime() + delayMinutes * 60 * 1000);
dismissNotification(context, habitId);
}
private void checkHabit(Context context, Intent intent)
{
Uri data = intent.getData();
Long timestamp = intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
long habitId = ContentUris.parseId(data);
Habit habit = Habit.get(habitId);
if(habit != null)
Habit habit = habitList.getById(habitId);
if (habit != null)
{
ToggleRepetitionCommand command = new ToggleRepetitionCommand(habit, timestamp);
ToggleRepetitionCommand command =
new ToggleRepetitionCommand(habit, timestamp);
commandRunner.execute(command, habitId);
}
@ -130,36 +190,26 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
sendRefreshBroadcast(context);
}
public static void sendRefreshBroadcast(Context context)
{
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context);
Intent refreshIntent = new Intent(HabitsApplication.ACTION_REFRESH);
manager.sendBroadcast(refreshIntent);
WidgetManager.updateWidgets(context);
}
private void dismissAllHabits()
private boolean checkWeekday(Intent intent, Habit habit)
{
Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
}
private void dismissNotification(Context context, Long habitId)
{
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
boolean reminderDays[] =
DateUtils.unpackWeekdayList(habit.getReminderDays());
int weekday = DateUtils.getWeekday(timestamp);
int notificationId = (int) (habitId % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
return reminderDays[weekday];
}
private void createNotification(final Context context, final Intent intent)
{
final Uri data = intent.getData();
final Habit habit = Habit.get(ContentUris.parseId(data));
final Long timestamp = intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
final Long reminderTime = intent.getLongExtra("reminderTime", DateUtils.getStartOfToday());
final Habit habit = habitList.getById(ContentUris.parseId(data));
final Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
final Long reminderTime =
intent.getLongExtra("reminderTime", DateUtils.getStartOfToday());
if (habit == null) return;
@ -170,7 +220,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
@Override
protected void doInBackground()
{
todayValue = habit.checkmarks.getTodayValue();
todayValue = habit.getCheckmarks().getTodayValue();
}
@Override
@ -185,9 +235,12 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
PendingIntent contentPendingIntent =
PendingIntent.getActivity(context, 0, contentIntent, 0);
PendingIntent dismissPendingIntent = buildDismissIntent(context);
PendingIntent checkIntentPending = buildCheckIntent(context, habit, timestamp);
PendingIntent snoozeIntentPending = buildSnoozeIntent(context, habit);
PendingIntent dismissPendingIntent =
buildDismissIntent(context);
PendingIntent checkIntentPending =
buildCheckIntent(context, habit, timestamp);
PendingIntent snoozeIntentPending =
buildSnoozeIntent(context, habit);
Uri ringtoneUri = ReminderUtils.getRingtoneUri(context);
@ -199,14 +252,16 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
Notification notification =
new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.name)
.setContentText(habit.description)
.setContentTitle(habit.getName())
.setContentText(habit.getDescription())
.setContentIntent(contentPendingIntent)
.setDeleteIntent(dismissPendingIntent)
.addAction(R.drawable.ic_action_check,
context.getString(R.string.check), checkIntentPending)
context.getString(R.string.check),
checkIntentPending)
.addAction(R.drawable.ic_action_snooze,
context.getString(R.string.snooze), snoozeIntentPending)
context.getString(R.string.snooze),
snoozeIntentPending)
.setSound(ringtoneUri)
.extend(wearableExtender)
.setWhen(reminderTime)
@ -227,59 +282,39 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
}.execute();
}
public static PendingIntent buildSnoozeIntent(Context context, Habit habit)
{
Uri data = habit.getUri();
Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class);
snoozeIntent.setData(data);
snoozeIntent.setAction(ACTION_SNOOZE);
return PendingIntent.getBroadcast(context, 0, snoozeIntent, 0);
}
public static PendingIntent buildCheckIntent(Context context, Habit habit, Long timestamp)
{
Uri data = habit.getUri();
Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class);
checkIntent.setData(data);
checkIntent.setAction(ACTION_CHECK);
if(timestamp != null) checkIntent.putExtra("timestamp", timestamp);
return PendingIntent.getBroadcast(context, 0, checkIntent, PendingIntent.FLAG_ONE_SHOT);
}
public static PendingIntent buildDismissIntent(Context context)
private void createReminderAlarmsDelayed(final Context context)
{
Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class);
deleteIntent.setAction(ACTION_DISMISS);
return PendingIntent.getBroadcast(context, 0, deleteIntent, 0);
new Handler().postDelayed(
() -> ReminderUtils.createReminderAlarms(context, habitList), 5000);
}
public static PendingIntent buildViewHabitIntent(Context context, Habit habit)
private void dismissAllHabits()
{
Intent intent = new Intent(context, ShowHabitActivity.class);
intent.setData(Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId()));
return TaskStackBuilder.create(context.getApplicationContext())
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
private boolean checkWeekday(Intent intent, Habit habit)
{
Long timestamp = intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
boolean reminderDays[] = DateUtils.unpackWeekdayList(habit.reminderDays);
int weekday = DateUtils.getWeekday(timestamp);
return reminderDays[weekday];
}
public static void dismissNotification(Context context, Habit habit)
private void dismissNotification(Context context, Long habitId)
{
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
int notificationId = (int) (habitId % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
}
private void snoozeHabit(Context context, Intent intent)
{
Uri data = intent.getData();
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
long delayMinutes =
Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
long habitId = ContentUris.parseId(data);
Habit habit = habitList.getById(habitId);
if (habit != null) ReminderUtils.createReminderAlarm(context, habit,
new Date().getTime() + delayMinutes * 60 * 1000);
dismissNotification(context, habitId);
}
}

@ -25,37 +25,53 @@ import android.support.annotation.Nullable;
import com.activeandroid.ActiveAndroid;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.utils.DatabaseUtils;
import java.io.File;
import javax.inject.Inject;
/**
* The Android application for Loop Habit Tracker.
*/
public class HabitsApplication extends Application
{
public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH";
public static final int RESULT_IMPORT_DATA = 1;
public static final String ACTION_REFRESH =
"org.isoron.uhabits.ACTION_REFRESH";
public static final int RESULT_BUG_REPORT = 4;
public static final int RESULT_EXPORT_CSV = 2;
public static final int RESULT_EXPORT_DB = 3;
public static final int RESULT_BUG_REPORT = 4;
public static final int RESULT_IMPORT_DATA = 1;
@Nullable
private static HabitsApplication application;
private static BaseComponent component;
@Nullable
private static Context context;
private static BaseComponent component;
public static boolean isTestMode()
{
try
@Inject
HabitList habitList;
public static BaseComponent getComponent()
{
if(context != null)
context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest");
return true;
return component;
}
catch (final Exception e)
public HabitList getHabitList()
{
return false;
return habitList;
}
public static void setComponent(BaseComponent component)
{
HabitsApplication.component = component;
}
@Nullable
@ -70,14 +86,19 @@ public class HabitsApplication extends Application
return application;
}
public static BaseComponent getComponent()
public static boolean isTestMode()
{
return component;
try
{
if (context != null) context
.getClassLoader()
.loadClass("org.isoron.uhabits.unit.models.HabitTest");
return true;
}
public static void setComponent(BaseComponent component)
catch (final Exception e)
{
HabitsApplication.component = component;
return false;
}
}
@Override
@ -91,9 +112,10 @@ public class HabitsApplication extends Application
if (isTestMode())
{
File db = DatabaseUtils.getDatabaseFile();
if(db.exists()) db.delete();
if (db.exists()) db.delete();
}
component.inject(this);
DatabaseUtils.initializeActiveAndroid();
}

@ -23,6 +23,9 @@ import android.app.backup.BackupAgentHelper;
import android.app.backup.FileBackupHelper;
import android.app.backup.SharedPreferencesBackupHelper;
/**
* An Android BackupAgentHelper customized for this application.
*/
public class HabitsBackupAgent extends BackupAgentHelper
{
@Override

@ -21,6 +21,9 @@ package org.isoron.uhabits;
import org.isoron.uhabits.ui.habits.list.ListHabitsActivity;
/**
* Application that starts upon clicking the launcher icon.
*/
public class MainActivity extends ListHabitsActivity
{
/*

@ -19,38 +19,52 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.List;
import javax.inject.Inject;
/**
* Command to archive a list of habits.
*/
public class ArchiveHabitsCommand extends Command
{
@Inject
HabitList habitList;
private List<Habit> habits;
public ArchiveHabitsCommand(List<Habit> habits)
{
HabitsApplication.getComponent().inject(this);
this.habits = habits;
}
@Override
public void execute()
{
Habit.archive(habits);
for(Habit h : habits) h.setArchived(1);
habitList.update(habits);
}
@Override
public void undo()
{
Habit.unarchive(habits);
for(Habit h : habits) h.setArchived(0);
habitList.update(habits);
}
@Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_archived;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_unarchived;

@ -19,60 +19,65 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
/**
* Command to change the color of a list of habits.
*/
public class ChangeHabitColorCommand extends Command
{
@Inject
HabitList habitList;
List<Habit> habits;
List<Integer> originalColors;
Integer newColor;
public ChangeHabitColorCommand(List<Habit> habits, Integer newColor)
{
HabitsApplication.getComponent().inject(this);
this.habits = habits;
this.newColor = newColor;
this.originalColors = new ArrayList<>(habits.size());
for(Habit h : habits)
originalColors.add(h.color);
for (Habit h : habits) originalColors.add(h.getColor());
}
@Override
public void execute()
{
Habit.setColor(habits, newColor);
for(Habit h : habits) h.setColor(newColor);
habitList.update(habits);
}
@Override
public void undo()
{
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command()
{
@Override
public void execute()
{
int k = 0;
for(Habit h : habits)
{
h.color = originalColors.get(k++);
h.save();
}
}
});
}
public Integer getExecuteStringId()
{
return R.string.toast_habit_changed;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_changed;
}
@Override
public void undo()
{
int k = 0;
for (Habit h : habits) h.setColor(originalColors.get(k++));
habitList.update(habits);
}
}

@ -19,12 +19,19 @@
package org.isoron.uhabits.commands;
/**
* A Command represents a desired set of changes that should be performed on the
* models.
* <p>
* A command can be executed and undone. Each of these operations also provide
* an string that should be displayed to the user upon their completion.
* <p>
* In general, commands should always be executed by a {@link CommandRunner}.
*/
public abstract class Command
{
public abstract void execute();
public abstract void undo();
public Integer getExecuteStringId()
{
return null;
@ -34,4 +41,6 @@ public abstract class Command
{
return null;
}
public abstract void undo();
}

@ -26,6 +26,12 @@ import org.isoron.uhabits.tasks.BaseTask;
import java.util.LinkedList;
/**
* A CommandRunner executes and undoes commands.
* <p>
* CommandRunners also allows objects to subscribe to it, and receive events
* whenever a command is performed.
*/
public class CommandRunner
{
private LinkedList<Listener> listeners;
@ -71,6 +77,10 @@ public class CommandRunner
listeners.remove(l);
}
/**
* Interface implemented by objects that want to receive an event whenever a
* command is executed.
*/
public interface Listener
{
void onCommandExecuted(@NonNull Command command,

@ -19,41 +19,48 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import javax.inject.Inject;
/**
* Command to create a habit.
*/
public class CreateHabitCommand extends Command
{
@Inject
HabitList habitList;
private Habit model;
private Long savedId;
public CreateHabitCommand(Habit model)
{
this.model = model;
HabitsApplication.getComponent().inject(this);
}
@Override
public void execute()
{
Habit savedHabit = new Habit(model);
if (savedId == null)
{
savedHabit.save();
Habit savedHabit = new Habit();
savedHabit.copyFrom(model);
savedHabit.setId(savedId);
habitList.add(savedHabit);
savedId = savedHabit.getId();
}
else
{
savedHabit.save(savedId);
}
}
@Override
public void undo()
{
Habit habit = Habit.get(savedId);
Habit habit = habitList.getById(savedId);
if(habit == null) throw new RuntimeException("Habit not found");
habit.cascadeDelete();
habitList.remove(habit);
}
@Override

@ -19,27 +19,36 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.List;
import javax.inject.Inject;
/**
* Command to delete a list of habits.
*/
public class DeleteHabitsCommand extends Command
{
@Inject
HabitList habitList;
private List<Habit> habits;
public DeleteHabitsCommand(List<Habit> habits)
{
this.habits = habits;
HabitsApplication.getComponent().inject(this);
}
@Override
public void execute()
{
for(Habit h : habits)
h.cascadeDelete();
Habit.rebuildOrder();
habitList.remove(h);
}
@Override
@ -48,11 +57,13 @@ public class DeleteHabitsCommand extends Command
throw new UnsupportedOperationException();
}
@Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_deleted;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_restored;

@ -19,24 +19,43 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import javax.inject.Inject;
/**
* Command to modify a habit.
*/
public class EditHabitCommand extends Command
{
@Inject
HabitList habitList;
private Habit original;
private Habit modified;
private long savedId;
private boolean hasIntervalChanged;
public EditHabitCommand(Habit original, Habit modified)
{
HabitsApplication.getComponent().inject(this);
this.savedId = original.getId();
this.modified = new Habit(modified);
this.original = new Habit(original);
this.modified = new Habit();
this.original = new Habit();
this.modified.copyFrom(modified);
this.original.copyFrom(original);
hasIntervalChanged = (!this.original.freqDen.equals(this.modified.freqDen) ||
!this.original.freqNum.equals(this.modified.freqNum));
hasIntervalChanged =
(!this.original.getFreqDen().equals(this.modified.getFreqDen()) ||
!this.original.getFreqNum().equals(this.modified.getFreqNum()));
}
@Override
@ -45,6 +64,18 @@ public class EditHabitCommand extends Command
copyAttributes(this.modified);
}
@Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_changed;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_changed_back;
}
@Override
public void undo()
{
@ -53,11 +84,11 @@ public class EditHabitCommand extends Command
private void copyAttributes(Habit model)
{
Habit habit = Habit.get(savedId);
if(habit == null) throw new RuntimeException("Habit not found");
Habit habit = habitList.getById(savedId);
if (habit == null) throw new RuntimeException("Habit not found");
habit.copyAttributes(model);
habit.save();
habit.copyFrom(model);
habitList.update(habit);
invalidateIfNeeded(habit);
}
@ -66,19 +97,9 @@ public class EditHabitCommand extends Command
{
if (hasIntervalChanged)
{
habit.checkmarks.deleteNewerThan(0);
habit.streaks.deleteNewerThan(0);
habit.scores.invalidateNewerThan(0);
}
habit.getCheckmarks().invalidateNewerThan(0);
habit.getStreaks().invalidateNewerThan(0);
habit.getScores().invalidateNewerThan(0);
}
public Integer getExecuteStringId()
{
return R.string.toast_habit_changed;
}
public Integer getUndoStringId()
{
return R.string.toast_habit_changed_back;
}
}

@ -21,6 +21,9 @@ package org.isoron.uhabits.commands;
import org.isoron.uhabits.models.Habit;
/**
* Command to toggle a repetition.
*/
public class ToggleRepetitionCommand extends Command
{
private Long offset;
@ -35,7 +38,7 @@ public class ToggleRepetitionCommand extends Command
@Override
public void execute()
{
habit.repetitions.toggle(offset);
habit.getRepetitions().toggleTimestamp(offset);
}
@Override

@ -19,38 +19,52 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.List;
import javax.inject.Inject;
/**
* Command to unarchive a list of habits.
*/
public class UnarchiveHabitsCommand extends Command
{
@Inject
HabitList habitList;
private List<Habit> habits;
public UnarchiveHabitsCommand(List<Habit> habits)
{
this.habits = habits;
HabitsApplication.getComponent().inject(this);
}
@Override
public void execute()
{
Habit.unarchive(habits);
for(Habit h : habits) h.setArchived(0);
habitList.update(habits);
}
@Override
public void undo()
{
Habit.archive(habits);
for(Habit h : habits) h.setArchived(1);
habitList.update(habits);
}
@Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_unarchived;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_archived;

@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides commands to modify the models, such as {@link
* org.isoron.uhabits.commands.CreateHabitCommand}.
*/
package org.isoron.uhabits.commands;

@ -21,13 +21,30 @@ package org.isoron.uhabits.io;
import android.support.annotation.NonNull;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.HabitList;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import javax.inject.Inject;
/**
* AbstractImporter is the base class for all classes that import data from
* files into the app.
*/
public abstract class AbstractImporter
{
@Inject
HabitList habitList;
public AbstractImporter()
{
HabitsApplication.getComponent().inject(this);
}
public abstract boolean canHandle(@NonNull File file) throws IOException;
public abstract void importHabitsFromFile(@NonNull File file) throws IOException;

@ -26,6 +26,10 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
/**
* A GenericImporter decides which implementation of AbstractImporter is able to
* handle a given file and delegates to it the task of importing the data.
*/
public class GenericImporter extends AbstractImporter
{
List<AbstractImporter> importers;
@ -42,8 +46,8 @@ public class GenericImporter extends AbstractImporter
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
for(AbstractImporter importer : importers)
if(importer.canHandle(file)) return true;
for (AbstractImporter importer : importers)
if (importer.canHandle(file)) return true;
return false;
}
@ -51,8 +55,7 @@ public class GenericImporter extends AbstractImporter
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
for(AbstractImporter importer : importers)
if(importer.canHandle(file))
importer.importHabitsFromFile(file);
for (AbstractImporter importer : importers)
if (importer.canHandle(file)) importer.importHabitsFromFile(file);
}
}

@ -24,8 +24,8 @@ import android.support.annotation.NonNull;
import com.activeandroid.ActiveAndroid;
import com.opencsv.CSVReader;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DateUtils;
import java.io.BufferedReader;
import java.io.File;
@ -34,6 +34,9 @@ import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap;
/**
* Class that imports data from HabitBull CSV files.
*/
public class HabitBullCSVImporter extends AbstractImporter
{
@Override
@ -89,16 +92,16 @@ public class HabitBullCSVImporter extends AbstractImporter
if(h == null)
{
h = new Habit();
h.name = name;
h.description = description;
h.freqNum = h.freqDen = 1;
h.save();
h.setName(name);
h.setDescription(description);
h.setFreqDen(1);
h.setFreqNum(1);
habitList.add(h);
habits.put(name, h);
}
if(!h.repetitions.contains(timestamp))
h.repetitions.toggle(timestamp);
if(!h.getRepetitions().containsTimestamp(timestamp))
h.getRepetitions().toggleTimestamp(timestamp);
}
}
}

@ -21,8 +21,10 @@ package org.isoron.uhabits.io;
import android.support.annotation.NonNull;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.models.ScoreList;
import org.isoron.uhabits.utils.DateUtils;
@ -37,6 +39,11 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.inject.Inject;
/**
* Class that exports the application data to CSV files.
*/
public class HabitsCSVExporter
{
private List<Habit> habits;
@ -46,8 +53,13 @@ public class HabitsCSVExporter
private String exportDirName;
@Inject
HabitList habitList;
public HabitsCSVExporter(List<Habit> habits, File dir)
{
HabitsApplication.getComponent().inject(this);
this.habits = habits;
this.exportDirName = dir.getAbsolutePath() + "/";
@ -61,20 +73,20 @@ public class HabitsCSVExporter
new File(exportDirName).mkdirs();
FileWriter out = new FileWriter(exportDirName + filename);
generateFilenames.add(filename);
Habit.writeCSV(habits, out);
habitList.writeCSV(out);
out.close();
for(Habit h : habits)
{
String sane = sanitizeFilename(h.name);
String habitDirName = String.format("%03d %s", h.position + 1, sane);
String sane = sanitizeFilename(h.getName());
String habitDirName = String.format("%03d %s", habitList.indexOf(h) + 1, sane);
habitDirName = habitDirName.trim() + "/";
new File(exportDirName + habitDirName).mkdirs();
generateDirs.add(habitDirName);
writeScores(habitDirName, h.scores);
writeCheckmarks(habitDirName, h.checkmarks);
writeScores(habitDirName, h.getScores());
writeCheckmarks(habitDirName, h.getCheckmarks());
}
}

@ -31,17 +31,21 @@ import org.isoron.uhabits.utils.FileUtils;
import java.io.File;
import java.io.IOException;
/**
* Class that imports data from database files exported by Loop Habit Tracker.
*/
public class LoopDBImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if(!isSQLite3File(file)) return false;
if (!isSQLite3File(file)) return false;
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?",
Cursor c = db.rawQuery(
"select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"Checkmarks", "Repetitions"});
boolean result = (c.moveToFirst() && c.getInt(0) == 2);

@ -23,14 +23,17 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.io.IOException;
import java.util.GregorianCalendar;
/**
* Class that imports database files exported by Rewire.
*/
public class RewireDBImporter extends AbstractImporter
{
@Override
@ -57,7 +60,7 @@ public class RewireDBImporter extends AbstractImporter
final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command()
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Callback()
{
@Override
public void execute()
@ -91,30 +94,30 @@ public class RewireDBImporter extends AbstractImporter
int periodIndex = c.getInt(7);
Habit habit = new Habit();
habit.name = name;
habit.description = description;
habit.setName(name);
habit.setDescription(description);
int periods[] = { 7, 31, 365 };
switch (schedule)
{
case 0:
habit.freqNum = activeDays.split(",").length;
habit.freqDen = 7;
habit.setFreqNum(activeDays.split(",").length);
habit.setFreqDen(7);
break;
case 1:
habit.freqNum = days;
habit.freqDen = periods[periodIndex];
habit.setFreqNum(days);
habit.setFreqDen(periods[periodIndex]);
break;
case 2:
habit.freqNum = 1;
habit.freqDen = repeatingCount;
habit.setFreqNum(1);
habit.setFreqDen(repeatingCount);
break;
}
habit.save();
habitList.add(habit);
createReminder(db, habit, id);
createCheckmarks(db, habit, id);
@ -150,10 +153,10 @@ public class RewireDBImporter extends AbstractImporter
reminderDays[idx] = true;
}
habit.reminderDays = DateUtils.packWeekdayList(reminderDays);
habit.reminderHour = rewireReminder / 60;
habit.reminderMin = rewireReminder % 60;
habit.save();
habit.setReminderDays(DateUtils.packWeekdayList(reminderDays));
habit.setReminderHour(rewireReminder / 60);
habit.setReminderMin(rewireReminder % 60);
habitList.update(habit);
}
finally
{
@ -161,7 +164,8 @@ public class RewireDBImporter extends AbstractImporter
}
}
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int rewireHabitId)
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull
Habit habit, int rewireHabitId)
{
Cursor c = null;
@ -181,7 +185,7 @@ public class RewireDBImporter extends AbstractImporter
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
cal.set(year, month - 1, day);
habit.repetitions.toggle(cal.getTimeInMillis());
habit.getRepetitions().toggleTimestamp(cal.getTimeInMillis());
}
while (c.moveToNext());
}

@ -23,25 +23,29 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.io.IOException;
import java.util.GregorianCalendar;
/**
* Class that imports data from database files exported by Tickmate.
*/
public class TickmateDBImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if(!isSQLite3File(file)) return false;
if (!isSQLite3File(file)) return false;
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?",
Cursor c = db.rawQuery(
"select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"tracks", "track2groups"});
boolean result = (c.moveToFirst() && c.getInt(0) == 2);
@ -54,47 +58,39 @@ public class TickmateDBImporter extends AbstractImporter
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
final SQLiteDatabase db =
SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command()
{
@Override
public void execute()
{
createHabits(db);
}
});
DatabaseUtils.executeAsTransaction(() -> createHabits(db));
db.close();
}
private void createHabits(SQLiteDatabase db)
private void createCheckmarks(@NonNull SQLiteDatabase db,
@NonNull Habit habit,
int tickmateTrackId)
{
Cursor c = null;
try
{
c = db.rawQuery("select _id, name, description from tracks", new String[0]);
String[] params = {Integer.toString(tickmateTrackId)};
c = db.rawQuery(
"select distinct year, month, day from ticks where _track_id=?",
params);
if (!c.moveToFirst()) return;
do
{
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
Habit habit = new Habit();
habit.name = name;
habit.description = description;
habit.freqNum = 1;
habit.freqDen = 1;
habit.save();
int year = c.getInt(0);
int month = c.getInt(1);
int day = c.getInt(2);
createCheckmarks(db, habit, id);
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
cal.set(year, month, day);
}
while (c.moveToNext());
habit.getRepetitions().toggleTimestamp(cal.getTimeInMillis());
} while (c.moveToNext());
}
finally
{
@ -102,28 +98,32 @@ public class TickmateDBImporter extends AbstractImporter
}
}
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int tickmateTrackId)
private void createHabits(SQLiteDatabase db)
{
Cursor c = null;
try
{
String[] params = { Integer.toString(tickmateTrackId) };
c = db.rawQuery("select distinct year, month, day from ticks where _track_id=?", params);
c = db.rawQuery("select _id, name, description from tracks",
new String[0]);
if (!c.moveToFirst()) return;
do
{
int year = c.getInt(0);
int month = c.getInt(1);
int day = c.getInt(2);
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
cal.set(year, month, day);
Habit habit = new Habit();
habit.setName(name);
habit.setDescription(description);
habit.setFreqNum(1);
habit.setFreqDen(1);
habitList.add(habit);
habit.repetitions.toggle(cal.getTimeInMillis());
}
while (c.moveToNext());
createCheckmarks(db, habit, id);
} while (c.moveToNext());
}
finally
{

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides classes that deal with importing from and exporting to files.
*/
package org.isoron.uhabits.io;

@ -19,48 +19,65 @@
package org.isoron.uhabits.models;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.apache.commons.lang3.builder.ToStringBuilder;
@Table(name = "Checkmarks")
public class Checkmark extends Model
/**
* A Checkmark represents the completion status of the habit for a given day.
* <p>
* While repetitions simply record that the habit was performed at a given date,
* a checkmark provides more information, such as whether a repetition was
* expected at that day or not.
* <p>
* Checkmarks are computed automatically from the list of repetitions.
*/
public class Checkmark
{
/**
* Indicates that there was no repetition at the timestamp, even though a repetition was
* expected.
* Indicates that there was a repetition at the timestamp.
*/
public static final int UNCHECKED = 0;
public static final int CHECKED_EXPLICITLY = 2;
/**
* Indicates that there was no repetition at the timestamp, but one was not expected in any
* case, due to the frequency of the habit.
* Indicates that there was no repetition at the timestamp, but one was not
* expected in any case, due to the frequency of the habit.
*/
public static final int CHECKED_IMPLICITLY = 1;
/**
* Indicates that there was a repetition at the timestamp.
* Indicates that there was no repetition at the timestamp, even though a
* repetition was expected.
*/
public static final int CHECKED_EXPLICITLY = 2;
public static final int UNCHECKED = 0;
/**
* The habit to which this checkmark belongs.
*/
@Column(name = "habit")
public Habit habit;
final Habit habit;
/**
* Timestamp of the day to which this checkmark corresponds. Time of the day must be midnight
* (UTC).
*/
@Column(name = "timestamp")
public Long timestamp;
final long timestamp;
/**
* Indicates whether there is a repetition at the given timestamp or not, and whether the
* repetition was expected. Assumes one of the values UNCHECKED, CHECKED_EXPLICITLY or
* CHECKED_IMPLICITLY.
*/
@Column(name = "value")
public Integer value;
final int value;
public Checkmark(Habit habit, long timestamp, int value)
{
this.habit = habit;
this.timestamp = timestamp;
this.value = value;
}
public long getTimestamp()
{
return timestamp;
}
public int getValue()
{
return value;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("timestamp", timestamp)
.append("value", value)
.toString();
}
}

@ -19,18 +19,10 @@
package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.io.IOException;
import java.io.Writer;
@ -38,9 +30,13 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class CheckmarkList
/**
* The collection of {@link Checkmark}s belonging to a habit.
*/
public abstract class CheckmarkList
{
private Habit habit;
protected Habit habit;
public ModelObservable observable = new ModelObservable();
public CheckmarkList(Habit habit)
@ -49,125 +45,123 @@ public class CheckmarkList
}
/**
* Deletes every checkmark that has timestamp either equal or newer than a given timestamp.
* These checkmarks will be recomputed at the next time they are queried.
* 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
* empty array.
* <p>
* The values are returned in an array containing one integer value for each
* day since the first repetition of the habit until today. The first entry
* corresponds to today, the second entry corresponds to yesterday, and so
* on.
*
* @param timestamp the timestamp
* @return values for the checkmarks in the interval
*/
public void deleteNewerThan(long timestamp)
@NonNull
public int[] getAllValues()
{
new Delete().from(Checkmark.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return new int[0];
observable.notifyListeners();
Long fromTimestamp = oldestRep.getTimestamp();
Long toTimestamp = DateUtils.getStartOfToday();
return getValues(fromTimestamp, toTimestamp);
}
/**
* Returns the values of the checkmarks that fall inside a certain interval of time.
*
* The values are returned in an array containing one integer value for each day of the
* interval. The first entry corresponds to the most recent day in the interval. Each subsequent
* entry corresponds to one day older than the previous entry. The boundaries of the time
* interval are included.
* Returns the checkmark for today.
*
* @param fromTimestamp timestamp for the oldest checkmark
* @param toTimestamp timestamp for the newest checkmark
* @return values for the checkmarks inside the given interval
* @return checkmark for today
*/
@NonNull
public int[] getValues(long fromTimestamp, long toTimestamp)
{
compute(fromTimestamp, toTimestamp);
if(fromTimestamp > toTimestamp) return new int[0];
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
@Nullable
public Checkmark getToday()
{
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;
long today = DateUtils.getStartOfToday();
compute(today, today);
return getNewest();
}
/**
* 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 empty array.
*
* The values are returned in an array containing one integer value for each day since the
* first repetition of the habit until today. The first entry corresponds to today, the second
* entry corresponds to yesterday, and so on.
* Returns the value of today's checkmark.
*
* @return values for the checkmarks in the interval
* @return value of today's checkmark
*/
@NonNull
public int[] getAllValues()
public int getTodayValue()
{
Repetition oldestRep = habit.repetitions.getOldest();
if(oldestRep == null) return new int[0];
Checkmark today = getToday();
if (today != null) return today.getValue();
else return Checkmark.UNCHECKED;
}
Long fromTimestamp = oldestRep.timestamp;
Long toTimestamp = DateUtils.getStartOfToday();
/**
* Returns the values of the checkmarks that fall inside a certain interval
* of time.
* <p>
* The values are returned in an array containing one integer value for each
* day of the interval. The first entry corresponds to the most recent day
* in the interval. Each subsequent entry corresponds to one day older than
* the previous entry. The boundaries of the time interval are included.
*
* @param from timestamp for the oldest checkmark
* @param to timestamp for the newest checkmark
* @return values for the checkmarks inside the given interval
*/
public abstract int[] getValues(long from, long to);
return getValues(fromTimestamp, toTimestamp);
}
/**
* Marks as invalid every checkmark that has timestamp either equal or newer
* than a given timestamp. These checkmarks will be recomputed at the next
* time they are queried.
*
* @param timestamp the timestamp
*/
public abstract void invalidateNewerThan(long timestamp);
/**
* Computes and stores one checkmark for each day, since the first repetition until today.
* Days that already have a corresponding checkmark are skipped.
* 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
* @throws IOException in case write operations fail
*/
protected void computeAll()
public void writeCSV(Writer out) throws IOException
{
long fromTimestamp = habit.repetitions.getOldestTimestamp();
if(fromTimestamp == 0) return;
computeAll();
Long toTimestamp = DateUtils.getStartOfToday();
int values[] = getAllValues();
long timestamp = DateUtils.getStartOfToday();
SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat();
compute(fromTimestamp, toTimestamp);
for (int value : values)
{
String date = dateFormat.format(new Date(timestamp));
out.write(String.format("%s,%d\n", date, value));
timestamp -= DateUtils.millisecondsInOneDay;
}
}
/**
* Computes and stores one checkmark for each day that falls inside the specified interval of
* time. Days that already have a corresponding checkmark are skipped.
* Computes and stores one checkmark for each day that falls inside the
* specified interval of time. Days that already have a corresponding
* checkmark are skipped.
*
* @param from timestamp for the beginning of the interval
* @param to timestamp for the end of the interval
*/
protected void compute(long from, final long to)
{
InterfaceUtils.throwIfMainThread();
final long day = DateUtils.millisecondsInOneDay;
Checkmark newestCheckmark = findNewest();
if(newestCheckmark != null) from = newestCheckmark.timestamp + day;
Checkmark newestCheckmark = getNewest();
if (newestCheckmark != null)
from = newestCheckmark.getTimestamp() + day;
if(from > to) return;
if (from > to) return;
long fromExtended = from - (long) (habit.freqDen) * day;
List<Repetition> reps = habit.repetitions
.selectFromTo(fromExtended, to)
.execute();
long fromExtended = from - (long) (habit.getFreqDen()) * day;
List<Repetition> reps =
habit.getRepetitions().getByInterval(fromExtended, to);
final int nDays = (int) ((to - from) / day) + 1;
int nDaysExtended = (int) ((to - fromExtended) / day) + 1;
@ -175,7 +169,7 @@ public class CheckmarkList
for (Repetition rep : reps)
{
int offset = (int) ((rep.timestamp - fromExtended) / day);
int offset = (int) ((rep.getTimestamp() - fromExtended) / day);
checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY;
}
@ -183,11 +177,11 @@ public class CheckmarkList
{
int counter = 0;
for (int j = 0; j < habit.freqDen; j++)
for (int j = 0; j < habit.getFreqDen(); j++)
if (checks[i + j] == 2) counter++;
if (counter >= habit.freqNum)
if(checks[i] != Checkmark.CHECKED_EXPLICITLY)
if (counter >= habit.getFreqNum())
if (checks[i] != Checkmark.CHECKED_EXPLICITLY)
checks[i] = Checkmark.CHECKED_IMPLICITLY;
}
@ -199,106 +193,29 @@ public class CheckmarkList
insert(timestamps, checks);
}
private void insert(long timestamps[], int values[])
{
String query = "insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
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();
}
}
/**
* Returns newest checkmark that has already been computed. Ignores any checkmark that has
* timestamp in the future. This does not update the cache.
*
* @return newest checkmark already computed
* Computes and stores one checkmark for each day, since the first
* repetition until today. Days that already have a corresponding checkmark
* are skipped.
*/
@Nullable
protected Checkmark findNewest()
protected void computeAll()
{
return new Select().from(Checkmark.class)
.where("habit = ?", habit.getId())
.and("timestamp <= ?", DateUtils.getStartOfToday())
.orderBy("timestamp desc")
.limit(1)
.executeSingle();
}
Repetition oldest = habit.getRepetitions().getOldest();
if (oldest == null) return;
/**
* Returns the checkmark for today.
*
* @return checkmark for today
*/
@Nullable
public Checkmark getToday()
{
long today = DateUtils.getStartOfToday();
compute(today, today);
return findNewest();
}
Long today = DateUtils.getStartOfToday();
/**
* Returns the value of today's checkmark.
*
* @return value of today's checkmark
*/
public int getTodayValue()
{
Checkmark today = getToday();
if(today != null) return today.value;
else return Checkmark.UNCHECKED;
compute(oldest.getTimestamp(), today);
}
/**
* 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.
* Returns newest checkmark that has already been computed. Ignores any
* checkmark that has timestamp in the future. This does not update the
* cache.
*
* @param out the writer where the CSV will be output
* @throws IOException in case write operations fail
* @return newest checkmark already computed
*/
protected abstract Checkmark getNewest();
public void writeCSV(Writer out) throws IOException
{
computeAll();
SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat();
String query = "select timestamp, value from checkmarks where habit = ? order by timestamp";
String params[] = { habit.getId().toString() };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return;
do
{
String timestamp = dateFormat.format(new Date(cursor.getLong(0)));
Integer value = cursor.getInt(1);
out.write(String.format("%s,%d\n", timestamp, value));
} while(cursor.moveToNext());
cursor.close();
out.close();
}
protected abstract void insert(long timestamps[], int values[]);
}

@ -19,135 +19,75 @@
package org.isoron.uhabits.models;
import android.annotation.SuppressLint;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.ActiveAndroid;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.query.Update;
import com.activeandroid.util.SQLiteUtils;
import com.opencsv.CSVWriter;
import org.isoron.uhabits.utils.ColorUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.utils.DateUtils;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Locale;
@Table(name = "Habits")
public class Habit extends Model
{
/**
* Name of the habit
*/
@Column(name = "name")
public String name;
import javax.inject.Inject;
/**
* Description of the habit
/**
* The thing that the user wants to track.
*/
@Column(name = "description")
public String description;
public class Habit
{
public static final String HABIT_URI_FORMAT =
"content://org.isoron.uhabits/habit/%d";
/**
* Frequency numerator. If a habit is performed 3 times in 7 days, this field equals 3.
*/
@Column(name = "freq_num")
public Integer freqNum;
@Nullable
private Long id;
/**
* Frequency denominator. If a habit is performed 3 times in 7 days, this field equals 7.
*/
@Column(name = "freq_den")
public Integer freqDen;
@NonNull
private String name;
/**
* Color of the habit.
*
* This number is not an android.graphics.Color, but an index to the activity color palette,
* which changes according to the theme. To convert this color into an android.graphics.Color,
* use ColorHelper.getColor(context, habit.color).
*/
@Column(name = "color")
public Integer color;
@NonNull
private String description;
/**
* Position of the habit. Habits are usually sorted by this field.
*/
@Column(name = "position")
public Integer position;
@NonNull
private Integer freqNum;
@NonNull
private Integer freqDen;
@NonNull
private Integer color;
/**
* Hour of the day the reminder should be shown. If there is no reminder, this equals to null.
*/
@Nullable
@Column(name = "reminder_hour")
public Integer reminderHour;
private Integer reminderHour;
/**
* Minute the reminder should be shown. If there is no reminder, this equals to null.
*/
@Nullable
@Column(name = "reminder_min")
public Integer reminderMin;
private Integer reminderMin;
/**
* Days of the week the reminder should be shown. This field can be converted to a list of
* booleans using the method DateHelper.unpackWeekdayList and converted back to an integer by
* using the method DateHelper.packWeekdayList. If the habit has no reminders, this value
* should be ignored.
*/
@NonNull
@Column(name = "reminder_days")
public Integer reminderDays;
private Integer reminderDays;
/**
* Not currently used.
*/
@Column(name = "highlight")
public Integer highlight;
@NonNull
private Integer highlight;
/**
* Flag that indicates whether the habit is archived. Archived habits are usually omitted from
* listings, unless explicitly included.
*/
@Column(name = "archived")
public Integer archived;
@NonNull
private Integer archived;
/**
* List of streaks belonging to this habit.
*/
@NonNull
public StreakList streaks;
private StreakList streaks;
/**
* List of scores belonging to this habit.
*/
@NonNull
public ScoreList scores;
private ScoreList scores;
/**
* List of repetitions belonging to this habit.
*/
@NonNull
public RepetitionList repetitions;
private RepetitionList repetitions;
/**
* List of checkmarks belonging to this habit.
*/
@NonNull
public CheckmarkList checkmarks;
private CheckmarkList checkmarks;
public ModelObservable observable = new ModelObservable();
private ModelObservable observable = new ModelObservable();
@Inject
ModelFactory factory;
/**
* Constructs a habit with the same attributes as the specified habit.
@ -156,320 +96,281 @@ public class Habit extends Model
*/
public Habit(Habit model)
{
HabitsApplication.getComponent().inject(this);
reminderDays = DateUtils.ALL_WEEK_DAYS;
copyAttributes(model);
copyFrom(model);
checkmarks = new CheckmarkList(this);
streaks = new StreakList(this);
scores = new ScoreList(this);
repetitions = new RepetitionList(this);
checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this);
repetitions = factory.buidRepetitionList(this);
}
/**
* Constructs a habit with default attributes. The habit is not archived, not highlighted, has
* no reminders and is placed in the last position of the list of habits.
* Constructs a habit with default attributes.
* <p>
* The habit is not archived, not highlighted, has no reminders and is
* placed in the last position of the list of habits.
*/
public Habit()
{
HabitsApplication.getComponent().inject(this);
this.color = 5;
this.position = Habit.countWithArchived();
this.highlight = 0;
this.archived = 0;
this.freqDen = 7;
this.freqNum = 3;
this.reminderDays = DateUtils.ALL_WEEK_DAYS;
checkmarks = new CheckmarkList(this);
streaks = new StreakList(this);
scores = new ScoreList(this);
repetitions = new RepetitionList(this);
checkmarks = factory.buildCheckmarkList(this);
streaks = factory.buildStreakList(this);
scores = factory.buildScoreList(this);
repetitions = factory.buidRepetitionList(this);
}
/**
* Returns the habit with specified id.
*
* @param id the id of the habit
* @return the habit, or null if none exist
* Clears the reminder for a habit. This sets all the related fields to
* null.
*/
@Nullable
public static Habit get(long id)
public void clearReminder()
{
return Habit.load(Habit.class, id);
reminderHour = null;
reminderMin = null;
reminderDays = DateUtils.ALL_WEEK_DAYS;
observable.notifyListeners();
}
/**
* Returns a list of all habits, optionally including archived habits.
* Copies all the attributes of the specified habit into this habit
*
* @param includeArchive whether archived habits should be included the list
* @return list of all habits
* @param model the model whose attributes should be copied from
*/
@NonNull
public static List<Habit> getAll(boolean includeArchive)
{
if(includeArchive) return selectWithArchived().execute();
else return select().execute();
public void copyFrom(@NonNull Habit model)
{
this.name = model.getName();
this.description = model.getDescription();
this.freqNum = model.getFreqNum();
this.freqDen = model.getFreqDen();
this.color = model.getColor();
this.reminderHour = model.getReminderHour();
this.reminderMin = model.getReminderMin();
this.reminderDays = model.getReminderDays();
this.highlight = model.getHighlight();
this.archived = model.getArchived();
observable.notifyListeners();
}
/**
* Returns the habit that occupies a certain position.
*
* @param position the position of the desired habit
* @return the habit at that position, or null if there is none
* Flag that indicates whether the habit is archived. Archived habits are
* usually omitted from listings, unless explicitly included.
*/
@Nullable
public static Habit getByPosition(int position)
public Integer getArchived()
{
return selectWithArchived().where("position = ?", position).executeSingle();
return archived;
}
/**
* Changes the id of a habit on the database.
*
* @param oldId the original id
* @param newId the new id
* List of checkmarks belonging to this habit.
*/
@SuppressLint("DefaultLocale")
public static void updateId(long oldId, long newId)
{
SQLiteUtils.execSql(String.format("update Habits set Id = %d where Id = %d", newId, oldId));
}
@NonNull
protected static From select()
public CheckmarkList getCheckmarks()
{
return new Select().from(Habit.class).where("archived = 0").orderBy("position");
return checkmarks;
}
@NonNull
protected static From selectWithArchived()
/**
* Color of the habit.
* <p>
* This number is not an android.graphics.Color, but an index to the
* activity color palette, which changes according to the theme. To convert
* this color into an android.graphics.Color, use ColorHelper.getColor(context,
* habit.color).
*/
public Integer getColor()
{
return new Select().from(Habit.class).orderBy("position");
return color;
}
/**
* Returns the total number of unarchived habits.
*
* @return number of unarchived habits
*/
public static int count()
public void setColor(Integer color)
{
return select().count();
this.color = color;
}
/**
* Returns the total number of habits, including archived habits.
*
* @return number of habits, including archived
* Description of the habit
*/
public static int countWithArchived()
public String getDescription()
{
return selectWithArchived().count();
return description;
}
/**
* Returns a list the habits that have a reminder. Does not include archived habits.
*
* @return list of habits with reminder
*/
@NonNull
public static List<Habit> getHabitsWithReminder()
public void setDescription(String description)
{
return select().where("reminder_hour is not null").execute();
this.description = description;
}
/**
* Changes the position of a habit on the list.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
* Frequency denominator. If a habit is performed 3 times in 7 days, this
* field equals 7.
*/
public static void reorder(Habit from, Habit to)
public Integer getFreqDen()
{
if(from == to) return;
return freqDen;
}
if (to.position < from.position)
public void setFreqDen(Integer freqDen)
{
new Update(Habit.class).set("position = position + 1")
.where("position >= ? and position < ?", to.position, from.position)
.execute();
this.freqDen = freqDen;
}
else
/**
* Frequency numerator. If a habit is performed 3 times in 7 days, this
* field equals 3.
*/
public Integer getFreqNum()
{
new Update(Habit.class).set("position = position - 1")
.where("position > ? and position <= ?", from.position, to.position)
.execute();
return freqNum;
}
from.position = to.position;
from.save();
public void setFreqNum(Integer freqNum)
{
this.freqNum = freqNum;
}
/**
* Recomputes the position for every habit in the database. It should never be necessary
* to call this method.
* Not currently used.
*/
public static void rebuildOrder()
public Integer getHighlight()
{
List<Habit> habits = selectWithArchived().execute();
return highlight;
}
ActiveAndroid.beginTransaction();
try
public void setHighlight(Integer highlight)
{
int i = 0;
for (Habit h : habits)
{
h.position = i++;
h.save();
this.highlight = highlight;
}
ActiveAndroid.setTransactionSuccessful();
}
finally
public Long getId()
{
ActiveAndroid.endTransaction();
return id;
}
public void setId(Long id)
{
this.id = id;
}
/**
* Copies all the attributes of the specified habit into this habit
*
* @param model the model whose attributes should be copied from
* Name of the habit
*/
public void copyAttributes(@NonNull Habit model)
{
this.name = model.name;
this.description = model.description;
this.freqNum = model.freqNum;
this.freqDen = model.freqDen;
this.color = model.color;
this.position = model.position;
this.reminderHour = model.reminderHour;
this.reminderMin = model.reminderMin;
this.reminderDays = model.reminderDays;
this.highlight = model.highlight;
this.archived = model.archived;
public String getName()
{
return name;
}
observable.notifyListeners();
public void setName(String name)
{
this.name = name;
}
/**
* Saves the habit on the database, and assigns the specified id to it.
*
* @param id the id that the habit should receive
*/
public void save(long id)
public ModelObservable getObservable()
{
save();
Habit.updateId(getId(), id);
return observable;
}
/**
* Deletes the habit and all data associated to it, including checkmarks, repetitions and
* scores.
* Days of the week the reminder should be shown. This field can be
* converted to a list of booleans using the method DateHelper.unpackWeekdayList
* and converted back to an integer by using the method
* DateHelper.packWeekdayList. If the habit has no reminders, this value
* should be ignored.
*/
public void cascadeDelete()
{
Long id = getId();
ActiveAndroid.beginTransaction();
try
@NonNull
public Integer getReminderDays()
{
new Delete().from(Checkmark.class).where("habit = ?", id).execute();
new Delete().from(Repetition.class).where("habit = ?", id).execute();
new Delete().from(Score.class).where("habit = ?", id).execute();
new Delete().from(Streak.class).where("habit = ?", id).execute();
delete();
ActiveAndroid.setTransactionSuccessful();
return reminderDays;
}
finally
public void setReminderDays(@NonNull Integer reminderDays)
{
ActiveAndroid.endTransaction();
}
this.reminderDays = reminderDays;
}
/**
* Returns the public URI that identifies this habit
* @return the uri
* Hour of the day the reminder should be shown. If there is no reminder,
* this equals to null.
*/
public Uri getUri()
@Nullable
public Integer getReminderHour()
{
String s = String.format(Locale.US, "content://org.isoron.uhabits/habit/%d", getId());
return Uri.parse(s);
return reminderHour;
}
/**
* Returns whether the habit is archived or not.
* @return true if archived
*/
public boolean isArchived()
public void setReminderHour(@Nullable Integer reminderHour)
{
return archived != 0;
this.reminderHour = reminderHour;
}
private static void updateAttributes(@NonNull List<Habit> habits, @Nullable Integer color,
@Nullable Integer archived)
/**
* Minute the reminder should be shown. If there is no reminder, this equals
* to null.
*/
@Nullable
public Integer getReminderMin()
{
ActiveAndroid.beginTransaction();
return reminderMin;
}
try
{
for (Habit h : habits)
public void setReminderMin(@Nullable Integer reminderMin)
{
if(color != null) h.color = color;
if(archived != null) h.archived = archived;
h.save();
this.reminderMin = reminderMin;
}
ActiveAndroid.setTransactionSuccessful();
}
finally
/**
* List of repetitions belonging to this habit.
*/
@NonNull
public RepetitionList getRepetitions()
{
ActiveAndroid.endTransaction();
for(Habit h : habits)
h.observable.notifyListeners();
}
return repetitions;
}
/**
* Archives an entire list of habits
*
* @param habits the habits to be archived
* List of scores belonging to this habit.
*/
public static void archive(@NonNull List<Habit> habits)
@NonNull
public ScoreList getScores()
{
updateAttributes(habits, null, 1);
return scores;
}
/**
* Unarchives an entire list of habits
*
* @param habits the habits to be unarchived
* List of streaks belonging to this habit.
*/
public static void unarchive(@NonNull List<Habit> habits)
@NonNull
public StreakList getStreaks()
{
updateAttributes(habits, null, 0);
return streaks;
}
/**
* Sets the color for an entire list of habits.
* Returns the public URI that identifies this habit
*
* @param habits the habits to be modified
* @param color the new color to be set
* @return the uri
*/
public static void setColor(@NonNull List<Habit> habits, int color)
public Uri getUri()
{
updateAttributes(habits, color, null);
for(Habit h : habits)
h.observable.notifyListeners();
String s = String.format(Locale.US, HABIT_URI_FORMAT, getId());
return Uri.parse(s);
}
/**
* Checks whether the habit has a reminder set.
*
* @return true if habit has reminder
* @return true if habit has reminder, false otherwise
*/
public boolean hasReminder()
{
@ -477,47 +378,35 @@ public class Habit extends Model
}
/**
* Clears the reminder for a habit. This sets all the related fields to null.
*/
public void clearReminder()
{
reminderHour = null;
reminderMin = null;
reminderDays = DateUtils.ALL_WEEK_DAYS;
observable.notifyListeners();
}
/**
* Writes the list of habits to the given writer, in CSV format. There is one line for each
* habit, containing the fields name, description, frequency numerator, frequency denominator
* and color. The color is written in HTML format (#000000).
* Returns whether the habit is archived or not.
*
* @param habits the list of habits to write
* @param out the writer that will receive the result
* @throws IOException if write operations fail
* @return true if archived
*/
public static void writeCSV(List<Habit> habits, Writer out) throws IOException
public boolean isArchived()
{
String header[] = { "Position", "Name", "Description", "NumRepetitions", "Interval", "Color" };
CSVWriter csv = new CSVWriter(out);
csv.writeNext(header, false);
return archived != 0;
}
for(Habit habit : habits)
{
String[] cols =
public void setArchived(Integer archived)
{
String.format("%03d", habit.position + 1),
habit.name,
habit.description,
Integer.toString(habit.freqNum),
Integer.toString(habit.freqDen),
ColorUtils.toHTML(ColorUtils.CSV_PALETTE[habit.color])
};
csv.writeNext(cols, false);
this.archived = archived;
}
csv.close();
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("id", id)
.append("name", name)
.append("description", description)
.append("freqNum", freqNum)
.append("freqDen", freqDen)
.append("color", color)
.append("reminderHour", reminderHour)
.append("reminderMin", reminderMin)
.append("reminderDays", reminderDays)
.append("highlight", highlight)
.append("archived", archived)
.toString();
}
}

@ -0,0 +1,235 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.opencsv.CSVWriter;
import org.isoron.uhabits.utils.ColorUtils;
import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* An ordered collection of {@link Habit}s.
*/
public abstract class HabitList
{
private ModelObservable observable;
/**
* Creates a new HabitList.
* <p>
* Depending on the implementation, this list can either be empty or be
* populated by some pre-existing habits.
*/
public HabitList()
{
observable = new ModelObservable();
}
/**
* Inserts a new habit in the list.
*
* @param habit the habit to be inserted
*/
public abstract void add(Habit habit);
/**
* Returns the total number of unarchived habits.
*
* @return number of unarchived habits
*/
public abstract int count();
/**
* Returns the total number of habits, including archived habits.
*
* @return number of habits, including archived
*/
public abstract int countWithArchived();
/**
* Returns a list of all habits, optionally including archived habits.
*
* @param includeArchive whether archived habits should be included the
* list
* @return list of all habits
*/
@NonNull
public abstract List<Habit> getAll(boolean includeArchive);
/**
* Returns the habit with specified id.
*
* @param id the id of the habit
* @return the habit, or null if none exist
*/
public abstract Habit getById(long id);
/**
* Returns the habit that occupies a certain position.
*
* @param position the position of the desired habit
* @return the habit at that position, or null if there is none
*/
@Nullable
public abstract Habit getByPosition(int position);
/**
* Returns the list of habits that match a given condition.
*
* @param matcher the matcher that checks the condition
* @return the list of matching habits
*/
@NonNull
public List<Habit> getFiltered(HabitMatcher matcher)
{
LinkedList<Habit> habits = new LinkedList<>();
for (Habit h : getAll(true)) if (matcher.matches(h)) habits.add(h);
return habits;
}
public ModelObservable getObservable()
{
return observable;
}
/**
* Returns a list the habits that have a reminder. Does not include archived
* habits.
*
* @return list of habits with reminder
*/
@NonNull
public List<Habit> getWithReminder()
{
return getFiltered(habit -> habit.hasReminder());
}
/**
* Returns the index of the given habit in the list, or -1 if the list does
* not contain the habit.
*
* @param h the habit
* @return the index of the habit, or -1 if not in the list
*/
public abstract int indexOf(Habit h);
/**
* Removes the given habit from the list.
* <p>
* If the given habit is not in the list, does nothing.
*
* @param h the habit to be removed.
*/
public abstract void remove(@NonNull Habit h);
/**
* Changes the position of a habit in the list.
*
* @param from the habit that should be moved
* @param to the habit that currently occupies the desired position
*/
public abstract void reorder(Habit from, Habit to);
/**
* Notifies the list that a certain list of habits has been modified.
* <p>
* Depending on the implementation, this operation might trigger a write to
* disk, or do nothing at all. To make sure that the habits get persisted,
* this operation must be called.
*
* @param habits the list of habits that have been modified.
*/
public abstract void update(List<Habit> habits);
/**
* Notifies the list that a certain habit has been modified.
* <p>
* See {@link #update(List)} for more details.
*
* @param habit the habit that has been modified.
*/
public void update(Habit habit)
{
update(Collections.singletonList(habit));
}
/**
* Writes the list of habits to the given writer, in CSV format. There is
* one line for each habit, containing the fields name, description,
* frequency numerator, frequency denominator and color. The color is
* written in HTML format (#000000).
*
* @param out the writer that will receive the result
* @throws IOException if write operations fail
*/
public void writeCSV(Writer out) throws IOException
{
String header[] = {
"Position",
"Name",
"Description",
"NumRepetitions",
"Interval",
"Color"
};
CSVWriter csv = new CSVWriter(out);
csv.writeNext(header, false);
for (Habit habit : getAll(true))
{
String[] cols = {
String.format("%03d", indexOf(habit) + 1),
habit.getName(),
habit.getDescription(),
Integer.toString(habit.getFreqNum()),
Integer.toString(habit.getFreqDen()),
ColorUtils.CSV_PALETTE[habit.getColor()]
};
csv.writeNext(cols, false);
}
csv.close();
}
/**
* A HabitMatcher decides whether habits match or not a certain condition.
* They can be used to produce filtered lists of habits.
*/
public interface HabitMatcher
{
/**
* Returns true if the given habit matches.
*
* @param habit the habit to be checked.
* @return true if matches, false otherwise.
*/
boolean matches(Habit habit);
}
}

@ -0,0 +1,37 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models;
/**
* Interface implemented by factories that provide concrete implementations
* of the core model classes.
*/
public interface ModelFactory
{
RepetitionList buidRepetitionList(Habit habit);
HabitList buildHabitList();
CheckmarkList buildCheckmarkList(Habit habit);
ScoreList buildScoreList(Habit habit);
StreakList buildStreakList(Habit habit);
}

@ -22,33 +22,62 @@ package org.isoron.uhabits.models;
import java.util.LinkedList;
import java.util.List;
/**
* A ModelObservable allows objects to subscribe themselves to it and receive
* notifications whenever the model is changed.
*/
public class ModelObservable
{
List<Listener> listeners;
/**
* Creates a new ModelObservable with no listeners.
*/
public ModelObservable()
{
super();
listeners = new LinkedList<>();
}
public interface Listener
/**
* Adds the given listener to the observable.
*
* @param l the listener to be added.
*/
public void addListener(Listener l)
{
void onModelChange();
listeners.add(l);
}
public void addListener(Listener l)
/**
* Notifies every listener that the model has changed.
* <p>
* Only models should call this method.
*/
public void notifyListeners()
{
listeners.add(l);
for (Listener l : listeners) l.onModelChange();
}
/**
* Removes the given listener.
* <p>
* The listener will no longer be notified when the model changes. If the
* given listener is not subscrined to this observable, does nothing.
*
* @param l the listener to be removed
*/
public void removeListener(Listener l)
{
listeners.remove(l);
}
public void notifyListeners()
/**
* Interface implemented by objects that want to be notified when the model
* changes.
*/
public interface Listener
{
for(Listener l : listeners) l.onModelChange();
void onModelChange();
}
}

@ -19,22 +19,51 @@
package org.isoron.uhabits.models;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import android.support.annotation.NonNull;
@Table(name = "Repetitions")
public class Repetition extends Model
{
/**
* Habit to which this repetition belong.
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
* Represents a record that the user has performed a certain habit at a certain
* date.
*/
@Column(name = "habit")
public Habit habit;
public class Repetition
{
@NonNull
private final Habit habit;
private final long timestamp;
/**
* Timestamp of the day this repetition occurred. Time of day should be midnight (UTC).
* Creates a new repetition with given parameters.
* <p>
* The timestamp corresponds to the days this repetition occurred. Time of
* day must be midnight (UTC).
*
* @param habit the habit to which this repetition belongs.
* @param timestamp the time this repetition occurred.
*/
@Column(name = "timestamp")
public Long timestamp;
public Repetition(Habit habit, long timestamp)
{
this.habit = habit;
this.timestamp = timestamp;
}
public Habit getHabit()
{
return habit;
}
public long getTimestamp()
{
return timestamp;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("timestamp", timestamp)
.toString();
}
}

@ -19,199 +19,179 @@
package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import java.util.Arrays;
import java.util.GregorianCalendar;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
public class RepetitionList
/**
* The collection of {@link Repetition}s belonging to a habit.
*/
public abstract class RepetitionList
{
@NonNull
private Habit habit;
public ModelObservable observable = new ModelObservable();
public RepetitionList(@NonNull Habit habit)
{
this.habit = habit;
}
protected final Habit habit;
@NonNull
protected From select()
{
return new Select().from(Repetition.class)
.where("habit = ?", habit.getId())
.and("timestamp <= ?", DateUtils.getStartOfToday())
.orderBy("timestamp");
}
protected final ModelObservable observable;
@NonNull
protected From selectFromTo(long timeFrom, long timeTo)
public RepetitionList(@NonNull Habit habit)
{
return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo);
this.habit = habit;
this.observable = new ModelObservable();
}
/**
* Checks whether there is a repetition at a given timestamp.
* Adds a repetition to the list.
* <p>
* Any implementation of this method must call observable.notifyListeners()
* after the repetition has been added.
*
* @param timestamp the timestamp to check
* @return true if there is a repetition
* @param repetition the repetition to be added.
*/
public boolean contains(long timestamp)
{
int count = select().where("timestamp = ?", timestamp).count();
return (count > 0);
}
public abstract void add(Repetition repetition);
/**
* Deletes the repetition at a given timestamp, if it exists.
* Returns true if the list contains a repetition that has the given
* timestamp.
*
* @param timestamp the timestamp of the repetition to delete
* @param timestamp the timestamp to find.
* @return true if list contains repetition with given timestamp, false
* otherwise.
*/
public void delete(long timestamp)
public boolean containsTimestamp(long timestamp)
{
new Delete().from(Repetition.class)
.where("habit = ?", habit.getId())
.and("timestamp = ?", timestamp)
.execute();
return (getByTimestamp(timestamp) != null);
}
/**
* Toggles the repetition at a certain timestamp. That is, deletes the repetition if it exists
* or creates one if it does not.
* Returns the list of repetitions that happened within the given time
* interval.
*
* The list is sorted by timestamp in decreasing order. That is, the first
* element corresponds to the most recent timestamp. The endpoints of the
* interval are included.
*
* @param timestamp the timestamp of the repetition to toggle
* @param fromTimestamp timestamp of the beginning of the interval
* @param toTimestamp timestamp of the end of the interval
* @return list of repetitions within given time interval
*/
public void toggle(long timestamp)
{
timestamp = DateUtils.getStartOfDay(timestamp);
if (contains(timestamp))
delete(timestamp);
else
insert(timestamp);
habit.scores.invalidateNewerThan(timestamp);
habit.checkmarks.deleteNewerThan(timestamp);
habit.streaks.deleteNewerThan(timestamp);
observable.notifyListeners();
}
private void insert(long timestamp)
{
String[] args = { habit.getId().toString(), Long.toString(timestamp) };
SQLiteUtils.execSql("insert into Repetitions(habit, timestamp) values (?,?)", args);
}
public abstract List<Repetition> getByInterval(long fromTimestamp,
long toTimestamp);
/**
* Returns the oldest repetition for the habit. If there is no repetition, returns null.
* Repetitions in the future are discarded.
* Returns the repetition that has the given timestamp, or null if none
* exists.
*
* @return oldest repetition for the habit
* @param timestamp the repetition timestamp.
* @return the repetition that has the given timestamp.
*/
@Nullable
public Repetition getOldest()
public abstract Repetition getByTimestamp(long timestamp);
@NonNull
public ModelObservable getObservable()
{
return (Repetition) select().limit(1).executeSingle();
return observable;
}
/**
* Returns the timestamp of the oldest repetition. If there are no repetitions, returns zero.
* Repetitions in the future are discarded.
* Returns the oldest repetition in the list.
* <p>
* If the list is empty, returns null. Repetitions in the future are
* discarded.
*
* @return timestamp of the oldest repetition
* @return oldest repetition in the list, or null if list is empty.
*/
public long getOldestTimestamp()
{
String[] args = { habit.getId().toString(), Long.toString(DateUtils.getStartOfToday()) };
String query = "select timestamp from Repetitions where habit = ? and timestamp <= ? " +
"order by timestamp limit 1";
return DatabaseUtils.longQuery(query, args);
}
@Nullable
public abstract Repetition getOldest();
/**
* Returns the total number of repetitions for each month, from the first repetition until
* today, grouped by day of week. The repetitions are returned in a HashMap. The key is the
* timestamp for the first day of the month, at midnight (00:00). The value is an integer
* array with 7 entries. The first entry contains the total number of repetitions during
* the specified month that occurred on a Saturday. The second entry corresponds to Sunday,
* and so on. If there are no repetitions during a certain month, the value is null.
* Returns the total number of repetitions for each month, from the first
* repetition until today, grouped by day of week.
* <p>
* The repetitions are returned in a HashMap. The key is the timestamp for
* the first day of the month, at midnight (00:00). The value is an integer
* array with 7 entries. The first entry contains the total number of
* repetitions during the specified month that occurred on a Saturday. The
* second entry corresponds to Sunday, and so on. If there are no
* repetitions during a certain month, the value is null.
*
* @return total number of repetitions by month versus day of week
*/
@NonNull
public HashMap<Long, Integer[]> getWeekdayFrequency()
{
Repetition oldestRep = getOldest();
if(oldestRep == null) return new HashMap<>();
List<Repetition> reps = getByInterval(0, DateUtils.getStartOfToday());
HashMap<Long, Integer[]> map = new HashMap<>();
String query = "select strftime('%Y', timestamp / 1000, 'unixepoch') as year," +
"strftime('%m', timestamp / 1000, 'unixepoch') as month," +
"strftime('%w', timestamp / 1000, 'unixepoch') as weekday, " +
"count(*) from repetitions " +
"where habit = ? and timestamp <= ? " +
"group by year, month, weekday";
String[] params = { habit.getId().toString(),
Long.toString(DateUtils.getStartOfToday()) };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return new HashMap<>();
HashMap <Long, Integer[]> map = new HashMap<>();
GregorianCalendar date = DateUtils.getStartOfTodayCalendar();
do
for (Repetition r : reps)
{
int year = Integer.parseInt(cursor.getString(0));
int month = Integer.parseInt(cursor.getString(1));
int weekday = (Integer.parseInt(cursor.getString(2)) + 1) % 7;
int count = cursor.getInt(3);
Calendar date = DateUtils.getCalendar(r.getTimestamp());
int weekday = date.get(Calendar.DAY_OF_WEEK) % 7;
date.set(Calendar.DAY_OF_MONTH, 1);
date.set(year, month - 1, 1);
long timestamp = date.getTimeInMillis();
Integer[] list = map.get(timestamp);
if(list == null)
if (list == null)
{
list = new Integer[7];
Arrays.fill(list, 0);
map.put(timestamp, list);
}
list[weekday] = count;
list[weekday]++;
}
while (cursor.moveToNext());
cursor.close();
return map;
}
/**
* Returns the total number of repetitions that happened within the specified interval of time.
* Removes a given repetition from the list.
* <p>
* If the list does not contain the repetition, it is unchanged.
* <p>
* Any implementation of this method must call observable.notifyListeners()
* after the repetition has been added.
*
* @param from beginning of the interval
* @param to end of the interval
* @return number of repetition in the given interval
* @param repetition the repetition to be removed
*/
public int count(long from, long to)
public abstract void remove(@NonNull Repetition repetition);
/**
* Adds or remove a repetition at a certain timestamp.
* <p>
* If there exists a repetition on the list with the given timestamp, the
* method removes this repetition from the list and returns it. If there are
* no repetitions with the given timestamp, creates and adds one to the
* list, then returns it.
*
* @param timestamp the timestamp for the timestamp that should be added or
* removed.
* @return the repetition that has been added or removed.
*/
@NonNull
public Repetition toggleTimestamp(long timestamp)
{
return selectFromTo(from, to).count();
timestamp = DateUtils.getStartOfDay(timestamp);
Repetition rep = getByTimestamp(timestamp);
if (rep != null) remove(rep);
else
{
rep = new Repetition(habit, timestamp);
add(rep);
}
// habit.getScores().invalidateNewerThan(timestamp);
// habit.getCheckmarks().invalidateNewerThan(timestamp);
// habit.getStreaks().invalidateNewerThan(timestamp);
return rep;
}
}

@ -19,78 +19,60 @@
package org.isoron.uhabits.models;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.apache.commons.lang3.builder.ToStringBuilder;
@Table(name = "Score")
public class Score extends Model
{
/**
* Minimum score value required to earn half a star.
*/
public static final int HALF_STAR_CUTOFF = 9629750;
/**
* Minimum score value required to earn a full star.
*/
public static final int FULL_STAR_CUTOFF = 15407600;
/**
* Maximum score value attainable by any habit.
*/
public static final int MAX_VALUE = 19259478;
/**
* Status indicating that the habit has not earned any star.
/**
* Represents how strong a habit is at a certain date.
*/
public static final int EMPTY_STAR = 0;
public class Score
{
/**
* Status indicating that the habit has earned half a star.
* Habit to which this score belongs to.
*/
public static final int HALF_STAR = 1;
private Habit habit;
/**
* Status indicating that the habit has earned a full star.
* Timestamp of the day to which this score applies. Time of day should be
* midnight (UTC).
*/
public static final int FULL_STAR = 2;
private Long timestamp;
/**
* Habit to which this score belongs to.
* Value of the score.
*/
@Column(name = "habit")
public Habit habit;
private Integer value;
/**
* Timestamp of the day to which this score applies. Time of day should be midnight (UTC).
* Maximum score value attainable by any habit.
*/
@Column(name = "timestamp")
public Long timestamp;
public static final int MAX_VALUE = 19259478;
/**
* Value of the score.
*/
@Column(name = "score")
public Integer score;
public Score(Habit habit, Long timestamp, Integer value)
{
this.habit = habit;
this.timestamp = timestamp;
this.value = value;
}
/**
* Given the frequency of the habit, the previous score, and the value of the current checkmark,
* computes the current score for the habit.
*
* The frequency of the habit is the number of repetitions divided by the length of the
* interval. For example, a habit that should be repeated 3 times in 8 days has frequency 3.0 /
* 8.0 = 0.375.
*
* The checkmarkValue should be UNCHECKED, CHECKED_IMPLICITLY or CHECK_EXPLICITLY.
* Given the frequency of the habit, the previous score, and the value of
* the current checkmark, computes the current score for the habit.
* <p>
* The frequency of the habit is the number of repetitions divided by the
* length of the interval. For example, a habit that should be repeated 3
* times in 8 days has frequency 3.0 / 8.0 = 0.375.
* <p>
* The checkmarkValue should be UNCHECKED, CHECKED_IMPLICITLY or
* CHECK_EXPLICITLY.
*
* @param frequency the frequency of the habit
* @param previousScore the previous score of the habit
* @param checkmarkValue the value of the current checkmark
*
* @return the current score
*/
public static int compute(double frequency, int previousScore, int checkmarkValue)
public static int compute(double frequency,
int previousScore,
int checkmarkValue)
{
double multiplier = Math.pow(0.5, 1.0 / (14.0 / frequency - 1));
int score = (int) (previousScore * multiplier);
@ -104,16 +86,27 @@ public class Score extends Model
return score;
}
/**
* Return the current star status for the habit, which can one of EMPTY_STAR, HALF_STAR or
* FULL_STAR.
*
* @return current star status
*/
public int getStarStatus()
public Habit getHabit()
{
return habit;
}
public Long getTimestamp()
{
return timestamp;
}
public Integer getValue()
{
return value;
}
@Override
public String toString()
{
if(score >= Score.FULL_STAR_CUTOFF) return Score.FULL_STAR;
if(score >= Score.HALF_STAR_CUTOFF) return Score.HALF_STAR;
return Score.EMPTY_STAR;
return new ToStringBuilder(this)
.append("timestamp", timestamp)
.append("value", value)
.toString();
}
}

@ -21,229 +21,214 @@ package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
public class ScoreList
public abstract class ScoreList
{
@NonNull
private Habit habit;
public ModelObservable observable = new ModelObservable();
protected final Habit habit;
protected ModelObservable observable;
/**
* Constructs a new ScoreList associated with the given habit.
* Creates a new ScoreList for the given habit.
* <p>
* The list is populated automatically according to the repetitions that the
* habit has.
*
* @param habit the habit this list should be associated with
* @param habit the habit to which the scores belong.
*/
public ScoreList(@NonNull Habit habit)
public ScoreList(Habit habit)
{
this.habit = habit;
}
protected From select()
{
return new Select()
.from(Score.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc");
observable = new ModelObservable();
}
/**
* Marks all scores that have timestamp equal to or newer than the given timestamp as invalid.
* Any following getValue calls will trigger the scores to be recomputed.
* Returns the values of all the scores, from day of the first repetition
* until today, grouped in chunks of specified size.
* <p>
* If the group size is one, then the value of each score is returned
* individually. If the group is, for example, seven, then the days are
* grouped in groups of seven consecutive days.
* <p>
* The values are returned in an array of integers, with one entry for each
* group of days in the interval. This value corresponds to the average of
* the scores for the days inside the group. The first entry corresponds to
* the ending of the interval (that is, the most recent group of days). The
* last entry corresponds to the beginning of the interval. As usual, the
* time of the day for the timestamps should be midnight (UTC). The
* endpoints of the interval are included.
* <p>
* The values are returned in an integer array. There is one entry for each
* day inside the interval. The first entry corresponds to today, while the
* last entry corresponds to the day of the oldest repetition.
*
* @param timestamp the oldest timestamp that should be invalidated
* @param divisor the size of the groups
* @return array of values, with one entry for each group of days
*/
public void invalidateNewerThan(long timestamp)
@NonNull
public int[] getAllValues(long divisor)
{
new Delete().from(Score.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return new int[0];
observable.notifyListeners();
long fromTimestamp = oldestRep.getTimestamp();
long toTimestamp = DateUtils.getStartOfToday();
return getValues(fromTimestamp, toTimestamp, divisor);
}
public ModelObservable getObservable()
{
return observable;
}
/**
* Computes and saves the scores that are missing since the first repetition of the habit.
* Returns the value of the score for today.
*
* @return value of today's score
*/
private void computeAll()
public int getTodayValue()
{
long fromTimestamp = habit.repetitions.getOldestTimestamp();
if(fromTimestamp == 0) return;
long toTimestamp = DateUtils.getStartOfToday();
compute(fromTimestamp, toTimestamp);
return getValue(DateUtils.getStartOfToday());
}
/**
* Computes and saves the scores that are missing inside a given time interval. Scores that
* have already been computed are skipped, therefore there is no harm in calling this function
* more times, or with larger intervals, than strictly needed. The endpoints of the interval are
* included.
* Returns the value of the score for a given day.
*
* This function assumes that there are no gaps on the scores. That is, if the newest score has
* timestamp t, then every score with timestamp lower than t has already been computed.
* @param timestamp the timestamp of a day
* @return score for that day
*/
public abstract int getValue(long timestamp);
/**
* Marks all scores that have timestamp equal to or newer than the given
* timestamp as invalid. Any following getValue calls will trigger the
* scores to be recomputed.
*
* @param from timestamp of the beginning of the interval
* @param to timestamp of the end of the time interval
* @param timestamp the oldest timestamp that should be invalidated
*/
protected void compute(long from, long to)
public abstract void invalidateNewerThan(long timestamp);
public void writeCSV(Writer out) throws IOException
{
InterfaceUtils.throwIfMainThread();
computeAll();
final long day = DateUtils.millisecondsInOneDay;
final double freq = ((double) habit.freqNum) / habit.freqDen;
SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat();
int newestScoreValue = findNewestValue();
long newestTimestamp = findNewestTimestamp();
String query =
"select timestamp, score from score where habit = ? order by timestamp";
String params[] = {habit.getId().toString()};
if(newestTimestamp > 0)
from = newestTimestamp + day;
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
final int checkmarkValues[] = habit.checkmarks.getValues(from, to);
final long beginning = from;
if (!cursor.moveToFirst()) return;
int lastScore = newestScoreValue;
int size = checkmarkValues.length;
do
{
String timestamp = dateFormat.format(new Date(cursor.getLong(0)));
String score = String.format("%.4f",
((float) cursor.getInt(1)) / Score.MAX_VALUE);
out.write(String.format("%s,%s\n", timestamp, score));
long timestamps[] = new long[size];
long values[] = new long[size];
} while (cursor.moveToNext());
for (int i = 0; i < checkmarkValues.length; i++)
{
int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1];
lastScore = Score.compute(freq, lastScore, checkmarkValue);
timestamps[i] = beginning + day * i;
values[i] = lastScore;
cursor.close();
out.close();
}
insert(timestamps, values);
}
protected abstract void add(List<Score> scores);
/**
* Returns the value of the most recent score that was already computed. If no score has been
* computed yet, returns zero.
* Computes and saves the scores that are missing inside a given time
* interval.
* <p>
* Scores that have already been computed are skipped, therefore there is no
* harm in calling this function more times, or with larger intervals, than
* strictly needed. The endpoints of the interval are included.
* <p>
* This function assumes that there are no gaps on the scores. That is, if
* the newest score has timestamp t, then every score with timestamp lower
* than t has already been computed.
*
* @return value of newest score, or zero if none exist
* @param from timestamp of the beginning of the interval
* @param to timestamp of the end of the time interval
*/
protected int findNewestValue()
protected void compute(long from, long to)
{
String args[] = { habit.getId().toString() };
String query = "select score from Score where habit = ? order by timestamp desc limit 1";
return SQLiteUtils.intQuery(query, args);
}
final long day = DateUtils.millisecondsInOneDay;
final double freq = ((double) habit.getFreqNum()) / habit.getFreqDen();
private long findNewestTimestamp()
int newestValue = 0;
long newestTimestamp = 0;
Score newest = getNewestComputed();
if(newest != null)
{
String args[] = { habit.getId().toString() };
String query = "select timestamp from Score where habit = ? order by timestamp desc limit 1";
return DatabaseUtils.longQuery(query, args);
newestValue = newest.getValue();
newestTimestamp = newest.getTimestamp();
}
private void insert(long timestamps[], long values[])
{
String query = "insert into Score(habit, timestamp, score) values (?,?,?)";
if (newestTimestamp > 0) from = newestTimestamp + day;
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
final int checkmarkValues[] = habit.getCheckmarks().getValues(from, to);
final long beginning = from;
try
{
SQLiteStatement statement = db.compileStatement(query);
int lastScore = newestValue;
List<Score> scores = new LinkedList<>();
for (int i = 0; i < timestamps.length; i++)
for (int i = 0; i < checkmarkValues.length; i++)
{
statement.bindLong(1, habit.getId());
statement.bindLong(2, timestamps[i]);
statement.bindLong(3, values[i]);
statement.execute();
int value = checkmarkValues[checkmarkValues.length - i - 1];
lastScore = Score.compute(freq, lastScore, value);
scores.add(new Score(habit, beginning + day * i, lastScore));
}
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
add(scores);
}
/**
* Returns the score for a certain day.
*
* @param timestamp the timestamp for the day
* @return the score for the day
* Computes and saves the scores that are missing since the first repetition
* of the habit.
*/
@Nullable
protected Score get(long timestamp)
protected void computeAll()
{
Repetition oldestRep = habit.repetitions.getOldest();
if(oldestRep == null) return null;
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return;
compute(oldestRep.timestamp, timestamp);
return select().where("timestamp = ?", timestamp).executeSingle();
long toTimestamp = DateUtils.getStartOfToday();
compute(oldestRep.getTimestamp(), toTimestamp);
}
/**
* Returns the value of the score for a given day.
* Returns the score for a certain day.
*
* @param timestamp the timestamp of a day
* @return score for that day
* @param timestamp the timestamp for the day
* @return the score for the day
*/
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);
}
protected abstract Score get(long timestamp);
/**
* Returns the values of all the scores, from day of the first repetition until today, grouped
* in chunks of specified size.
*
* If the group size is one, then the value of each score is returned individually. If the group
* is, for example, seven, then the days are grouped in groups of seven consecutive days.
*
* The values are returned in an array of integers, with one entry for each group of days in the
* interval. This value corresponds to the average of the scores for the days inside the group.
* The first entry corresponds to the ending of the interval (that is, the most recent group of
* days). The last entry corresponds to the beginning of the interval. As usual, the time of the
* day for the timestamps should be midnight (UTC). The endpoints of the interval are included.
*
* The values are returned in an integer array. There is one entry for each day inside the
* interval. The first entry corresponds to today, while the last entry corresponds to the
* day of the oldest repetition.
* Returns the most recent score that was already computed.
* <p>
* If no score has been computed yet, returns null.
*
* @param divisor the size of the groups
* @return array of values, with one entry for each group of days
* @return the newest score computed, or null if none exist
*/
@NonNull
public int[] getAllValues(long divisor)
{
Repetition oldestRep = habit.repetitions.getOldest();
if(oldestRep == null) return new int[0];
long fromTimestamp = oldestRep.timestamp;
long toTimestamp = DateUtils.getStartOfToday();
return getValues(fromTimestamp, toTimestamp, divisor);
}
@Nullable
protected abstract Score getNewestComputed();
/**
* Same as getAllValues(long), but using a specified interval.
@ -253,96 +238,5 @@ public class ScoreList
* @param divisor size of the groups
* @return array of values, with one entry for each group of days
*/
@NonNull
protected int[] getValues(long from, long to, long divisor)
{
compute(from, to);
divisor *= DateUtils.millisecondsInOneDay;
Long offset = to + divisor;
String query = "select ((timestamp - ?) / ?) as time, avg(score) from Score " +
"where habit = ? and timestamp >= ? and timestamp <= ? " +
"group by time order by time desc";
String params[] = { offset.toString(), Long.toString(divisor), habit.getId().toString(),
Long.toString(from), Long.toString(to) };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return new int[0];
int k = 0;
int[] scores = new int[cursor.getCount()];
do
{
scores[k++] = (int) cursor.getFloat(1);
}
while (cursor.moveToNext());
cursor.close();
return scores;
}
/**
* Returns the score for today.
*
* @return score for today
*/
@Nullable
protected Score getToday()
{
return get(DateUtils.getStartOfToday());
}
/**
* Returns the value of the score for today.
*
* @return value of today's score
*/
public int getTodayValue()
{
return getValue(DateUtils.getStartOfToday());
}
/**
* Returns the star status for today. The returned value is either Score.EMPTY_STAR,
* Score.HALF_STAR or Score.FULL_STAR.
*
* @return star status for today
*/
public int getTodayStarStatus()
{
Score score = getToday();
if(score != null) return score.getStarStatus();
else return Score.EMPTY_STAR;
}
public void writeCSV(Writer out) throws IOException
{
computeAll();
SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat();
String query = "select timestamp, score from score where habit = ? order by timestamp";
String params[] = { habit.getId().toString() };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return;
do
{
String timestamp = dateFormat.format(new Date(cursor.getLong(0)));
String score = String.format("%.4f", ((float) cursor.getInt(1)) / Score.MAX_VALUE);
out.write(String.format("%s,%s\n", timestamp, score));
} while(cursor.moveToNext());
cursor.close();
out.close();
}
protected abstract int[] getValues(long from, long to, long divisor);
}

@ -19,20 +19,63 @@
package org.isoron.uhabits.models;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.isoron.uhabits.utils.DateUtils;
public class Streak extends Model
public class Streak
{
@Column(name = "habit")
public Habit habit;
private Habit habit;
@Column(name = "start")
public Long start;
private long start;
@Column(name = "end")
public Long end;
private long end;
@Column(name = "length")
public Long length;
public Streak(Habit habit, long start, long end)
{
this.habit = habit;
this.start = start;
this.end = end;
}
public int compareLonger(Streak other)
{
if (this.getLength() != other.getLength())
return Long.signum(this.getLength() - other.getLength());
return Long.signum(this.getEnd() - other.getEnd());
}
public int compareNewer(Streak other)
{
return Long.signum(this.getEnd() - other.getEnd());
}
public long getEnd()
{
return end;
}
public Habit getHabit()
{
return habit;
}
public long getLength()
{
return (end - start) / DateUtils.millisecondsInOneDay + 1;
}
public long getStart()
{
return start;
}
@Override
public String toString()
{
return new ToStringBuilder(this)
.append("start", start)
.append("end", end)
.toString();
}
}

@ -19,100 +19,123 @@
package org.isoron.uhabits.models;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.activeandroid.ActiveAndroid;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class StreakList
/**
* The collection of {@link Streak}s that belong to a habit.
* <p>
* This list is populated automatically from the list of repetitions.
*/
public abstract class StreakList
{
private Habit habit;
public ModelObservable observable = new ModelObservable();
protected final Habit habit;
protected ModelObservable observable;
public StreakList(Habit habit)
protected StreakList(Habit habit)
{
this.habit = habit;
observable = new ModelObservable();
}
public List<Streak> getAll(int limit)
{
rebuild();
String query = "select * from (select * from streak where habit=? " +
"order by end <> ?, length desc, end desc limit ?) order by end desc";
public abstract List<Streak> getAll();
String params[] = {habit.getId().toString(), Long.toString(DateUtils.getStartOfToday()),
Integer.toString(limit)};
public List<Streak> getBest(int limit)
{
List<Streak> streaks = getAll();
Collections.sort(streaks, (s1, s2) -> s2.compareLonger(s1));
streaks = streaks.subList(0, Math.min(streaks.size(), limit));
Collections.sort(streaks, (s1, s2) -> s2.compareNewer(s1));
return streaks;
}
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
public abstract Streak getNewestComputed();
if(!cursor.moveToFirst())
public ModelObservable getObservable()
{
cursor.close();
return new LinkedList<>();
return observable;
}
List<Streak> streaks = new LinkedList<>();
public abstract void invalidateNewerThan(long timestamp);
do
public void rebuild()
{
Streak s = Streak.load(Streak.class, cursor.getInt(0));
streaks.add(s);
}
while (cursor.moveToNext());
long today = DateUtils.getStartOfToday();
cursor.close();
return streaks;
Long beginning = findBeginning();
if (beginning == null || beginning > today) return;
}
int checks[] = habit.getCheckmarks().getValues(beginning, today);
List<Streak> streaks = checkmarksToStreaks(beginning, checks);
public Streak getNewest()
{
return new Select().from(Streak.class)
.where("habit = ?", habit.getId())
.orderBy("end desc")
.limit(1)
.executeSingle();
removeNewestComputed();
insert(streaks);
}
public void rebuild()
/**
* Converts a list of checkmark values to a list of streaks.
*
* @param beginning the timestamp corresponding to the first checkmark
* value.
* @param checks the checkmarks values, ordered by decreasing timestamp.
* @return the list of streaks.
*/
@NonNull
protected List<Streak> checkmarksToStreaks(Long beginning, int[] checks)
{
InterfaceUtils.throwIfMainThread();
ArrayList<Long> transitions = getTransitions(beginning, checks);
long beginning;
long today = DateUtils.getStartOfToday();
long day = DateUtils.millisecondsInOneDay;
Streak newestStreak = getNewest();
if (newestStreak != null)
List<Streak> streaks = new LinkedList<>();
for (int i = 0; i < transitions.size(); i += 2)
{
beginning = newestStreak.start;
long start = transitions.get(i);
long end = transitions.get(i + 1);
streaks.add(new Streak(habit, start, end));
}
else
{
Repetition oldestRep = habit.repetitions.getOldest();
if (oldestRep == null) return;
beginning = oldestRep.timestamp;
return streaks;
}
if (beginning > today) return;
/**
* Finds the place where we should start when recomputing the streaks.
*
* @return
*/
@Nullable
protected Long findBeginning()
{
Streak newestStreak = getNewestComputed();
if (newestStreak != null) return newestStreak.getStart();
int checks[] = habit.checkmarks.getValues(beginning, today);
ArrayList<Long> list = new ArrayList<>();
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep != null) return oldestRep.getTimestamp();
return null;
}
/**
* Returns the timestamps where there was a transition from performing a
* habit to not performing a habit, and vice-versa.
*
* @param beginning the timestamp for the first checkmark
* @param checks the checkmarks, ordered by decresing timestamp
* @return the list of transitions
*/
@NonNull
protected ArrayList<Long> getTransitions(Long beginning, int[] checks)
{
long day = DateUtils.millisecondsInOneDay;
long current = beginning;
ArrayList<Long> list = new ArrayList<>();
list.add(current);
for (int i = 1; i < checks.length; i++)
@ -126,38 +149,10 @@ public class StreakList
if (list.size() % 2 == 1) list.add(current);
ActiveAndroid.beginTransaction();
if(newestStreak != null) newestStreak.delete();
try
{
for (int i = 0; i < list.size(); i += 2)
{
Streak streak = new Streak();
streak.habit = habit;
streak.start = list.get(i);
streak.end = list.get(i + 1);
streak.length = (streak.end - streak.start) / day + 1;
streak.save();
}
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
return list;
}
protected abstract void insert(List<Streak> streaks);
public void deleteNewerThan(long timestamp)
{
new Delete().from(Streak.class)
.where("habit = ?", habit.getId())
.and("end >= ?", timestamp - DateUtils.millisecondsInOneDay)
.execute();
observable.notifyListeners();
}
protected abstract void removeNewestComputed();
}

@ -0,0 +1,102 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DateUtils;
import java.util.Collections;
import java.util.LinkedList;
/**
* In-memory implementation of {@link CheckmarkList}.
*/
public class MemoryCheckmarkList extends CheckmarkList
{
LinkedList<Checkmark> list;
public MemoryCheckmarkList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public int[] getValues(long from, long to)
{
compute(from, to);
if (from > to) return new int[0];
int length = (int) ((to - from) / DateUtils.millisecondsInOneDay + 1);
int values[] = new int[length];
int k = 0;
for (Checkmark c : list)
if(c.getTimestamp() >= from && c.getTimestamp() <= to)
values[k++] = c.getValue();
return values;
}
@Override
public void invalidateNewerThan(long timestamp)
{
LinkedList<Checkmark> invalid = new LinkedList<>();
for (Checkmark c : list)
if (c.getTimestamp() >= timestamp) invalid.add(c);
list.removeAll(invalid);
}
@Override
protected Checkmark getNewest()
{
long newestTimestamp = 0;
Checkmark newestCheck = null;
for (Checkmark c : list)
{
if (c.getTimestamp() > newestTimestamp)
{
newestCheck = c;
newestTimestamp = c.getTimestamp();
}
}
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()));
}
}

@ -0,0 +1,111 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.LinkedList;
import java.util.List;
/**
* In-memory implementation of {@link HabitList}.
*/
public class MemoryHabitList extends HabitList
{
@NonNull
private LinkedList<Habit> list;
public MemoryHabitList()
{
list = new LinkedList<>();
}
@Override
public void add(Habit habit)
{
list.addLast(habit);
}
@Override
public int count()
{
int count = 0;
for (Habit h : list) if (!h.isArchived()) count++;
return count;
}
@Override
public int countWithArchived()
{
return list.size();
}
@Override
public Habit getById(long id)
{
for (Habit h : list) if (h.getId() == id) return h;
return null;
}
@NonNull
@Override
public List<Habit> getAll(boolean includeArchive)
{
if (includeArchive) return new LinkedList<>(list);
return getFiltered(habit -> !habit.isArchived());
}
@Nullable
@Override
public Habit getByPosition(int position)
{
return list.get(position);
}
@Override
public int indexOf(Habit h)
{
return list.indexOf(h);
}
@Override
public void remove(@NonNull Habit habit)
{
list.remove(habit);
}
@Override
public void reorder(Habit from, Habit to)
{
int toPos = indexOf(to);
list.remove(from);
list.add(toPos, from);
}
@Override
public void update(List<Habit> habits)
{
// NOP
}
}

@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import org.isoron.uhabits.models.CheckmarkList;
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
{
@Override
public RepetitionList buidRepetitionList(Habit habit)
{
return new MemoryRepetitionList(habit);
}
@Override
public HabitList buildHabitList()
{
return new MemoryHabitList();
}
@Override
public CheckmarkList buildCheckmarkList(Habit habit)
{
return new MemoryCheckmarkList(habit);
}
@Override
public ScoreList buildScoreList(Habit habit)
{
return null;
}
@Override
public StreakList buildStreakList(Habit habit)
{
return new MemoryStreakList(habit);
}
}

@ -0,0 +1,106 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.RepetitionList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
/**
* In-memory implementation of {@link RepetitionList}.
*/
public class MemoryRepetitionList extends RepetitionList
{
LinkedList<Repetition> list;
public MemoryRepetitionList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public void add(Repetition repetition)
{
list.add(repetition);
observable.notifyListeners();
}
@Override
public List<Repetition> getByInterval(long fromTimestamp, long toTimestamp)
{
LinkedList<Repetition> filtered = new LinkedList<>();
for (Repetition r : list)
{
long t = r.getTimestamp();
if (t >= fromTimestamp && t <= toTimestamp) filtered.add(r);
}
Collections.sort(filtered,
(r1, r2) -> (int) (r1.getTimestamp() - r2.getTimestamp()));
return filtered;
}
@Nullable
@Override
public Repetition getByTimestamp(long timestamp)
{
for (Repetition r : list)
if (r.getTimestamp() == timestamp) return r;
return null;
}
@Nullable
@Override
public Repetition getOldest()
{
long oldestTime = Long.MAX_VALUE;
Repetition oldestRep = null;
for (Repetition rep : list)
{
if (rep.getTimestamp() < oldestTime)
{
oldestRep = rep;
oldestTime = rep.getTimestamp();
}
}
return oldestRep;
}
@Override
public void remove(@NonNull Repetition repetition)
{
list.remove(repetition);
observable.notifyListeners();
}
}

@ -0,0 +1,86 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.models.memory;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak;
import org.isoron.uhabits.models.StreakList;
import org.isoron.uhabits.utils.DateUtils;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class MemoryStreakList extends StreakList
{
LinkedList<Streak> list;
public MemoryStreakList(Habit habit)
{
super(habit);
list = new LinkedList<>();
}
@Override
public Streak getNewestComputed()
{
Streak newest = null;
for(Streak s : list)
if(newest == null || s.getEnd() > newest.getEnd())
newest = s;
return newest;
}
@Override
public void invalidateNewerThan(long timestamp)
{
LinkedList<Streak> discard = new LinkedList<>();
for(Streak s : list)
if(s.getEnd() >= timestamp - DateUtils.millisecondsInOneDay)
discard.add(s);
list.removeAll(discard);
observable.notifyListeners();
}
@Override
protected void insert(List<Streak> streaks)
{
list.addAll(streaks);
Collections.sort(list, (s1, s2) -> s2.compareNewer(s1));
}
@Override
protected void removeNewestComputed()
{
Streak newest = getNewestComputed();
if(newest != null) list.remove(newest);
}
@Override
public List<Streak> getAll()
{
rebuild();
return new LinkedList<>(list);
}
}

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides in-memory implementation of core models.
*/
package org.isoron.uhabits.models.memory;

@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public Licenses along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides core models classes, such as {@link org.isoron.uhabits.models.Habit}
* and {@link org.isoron.uhabits.models.Repetition}.
*/
package org.isoron.uhabits.models;

@ -0,0 +1,62 @@
/*
* 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 com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
/**
* The SQLite database record corresponding to a {@link Checkmark}.
*/
@Table(name = "Checkmarks")
public class CheckmarkRecord extends Model
{
/**
* The habit to which this checkmark belongs.
*/
@Column(name = "habit")
public HabitRecord habit;
/**
* Timestamp of the day to which this checkmark corresponds. Time of the day
* must be midnight (UTC).
*/
@Column(name = "timestamp")
public Long timestamp;
/**
* Indicates whether there is a repetition at the given timestamp or not,
* and whether the repetition was expected. Assumes one of the values
* UNCHECKED, CHECKED_EXPLICITLY or CHECKED_IMPLICITLY.
*/
@Column(name = "value")
public Integer value;
public Checkmark toCheckmark()
{
SQLiteHabitList habitList = SQLiteHabitList.getInstance();
Habit h = habitList.getById(habit.getId());
return new Checkmark(h, timestamp, value);
}
}

@ -0,0 +1,177 @@
/*
* 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.annotation.SuppressLint;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Delete;
import com.activeandroid.util.SQLiteUtils;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DatabaseUtils;
/**
* The SQLite database record corresponding to a {@link Habit}.
*/
@Table(name = "Habits")
public class HabitRecord extends Model
{
public static final String HABIT_URI_FORMAT =
"content://org.isoron.uhabits/habit/%d";
@Column(name = "name")
public String name;
@Column(name = "description")
public String description;
@Column(name = "freq_num")
public Integer freqNum;
@Column(name = "freq_den")
public Integer freqDen;
@Column(name = "color")
public Integer color;
@Column(name = "position")
public Integer position;
@Nullable
@Column(name = "reminder_hour")
public Integer reminderHour;
@Nullable
@Column(name = "reminder_min")
public Integer reminderMin;
@NonNull
@Column(name = "reminder_days")
public Integer reminderDays;
@Column(name = "highlight")
public Integer highlight;
@Column(name = "archived")
public Integer archived;
public HabitRecord()
{
}
@Nullable
public static HabitRecord get(Long id)
{
return HabitRecord.load(HabitRecord.class, id);
}
/**
* Changes the id of a habit on the database.
*
* @param oldId the original id
* @param newId the new id
*/
@SuppressLint("DefaultLocale")
public static void updateId(long oldId, long newId)
{
SQLiteUtils.execSql(
String.format("update Habits set Id = %d where Id = %d", newId,
oldId));
}
/**
* Deletes the habit and all data associated to it, including checkmarks,
* repetitions and scores.
*/
public void cascadeDelete()
{
Long id = getId();
DatabaseUtils.executeAsTransaction(() -> {
new Delete()
.from(CheckmarkRecord.class)
.where("habit = ?", id)
.execute();
new Delete()
.from(RepetitionRecord.class)
.where("habit = ?", id)
.execute();
new Delete()
.from(ScoreRecord.class)
.where("habit = ?", id)
.execute();
new Delete()
.from(StreakRecord.class)
.where("habit = ?", id)
.execute();
delete();
});
}
public void copyFrom(Habit model)
{
this.name = model.getName();
this.description = model.getDescription();
this.freqNum = model.getFreqNum();
this.freqDen = model.getFreqDen();
this.color = model.getColor();
this.reminderHour = model.getReminderHour();
this.reminderMin = model.getReminderMin();
this.reminderDays = model.getReminderDays();
this.highlight = model.getHighlight();
this.archived = model.getArchived();
}
public void copyTo(Habit habit)
{
habit.setName(this.name);
habit.setDescription(this.description);
habit.setFreqNum(this.freqNum);
habit.setFreqDen(this.freqDen);
habit.setColor(this.color);
habit.setReminderHour(this.reminderHour);
habit.setReminderMin(this.reminderMin);
habit.setReminderDays(this.reminderDays);
habit.setHighlight(this.highlight);
habit.setArchived(this.archived);
habit.setId(this.getId());
}
/**
* Saves the habit on the database, and assigns the specified id to it.
*
* @param id the id that the habit should receive
*/
public void save(long id)
{
save();
updateId(getId(), id);
}
}

@ -0,0 +1,58 @@
/*
* 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 com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
/**
* The SQLite database record corresponding to a {@link Repetition}.
*/
@Table(name = "Repetitions")
public class RepetitionRecord extends Model
{
@Column(name = "habit")
public HabitRecord habit;
@Column(name = "timestamp")
public Long timestamp;
public void copyFrom(Repetition repetition)
{
habit = HabitRecord.get(repetition.getHabit().getId());
timestamp = repetition.getTimestamp();
}
public static RepetitionRecord get(Long id)
{
return RepetitionRecord.load(RepetitionRecord.class, id);
}
public Repetition toRepetition()
{
SQLiteHabitList habitList = SQLiteHabitList.getInstance();
Habit h = habitList.getById(habit.getId());
return new Repetition(h, timestamp);
}
}

@ -0,0 +1,64 @@
/*
* 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 org.isoron.uhabits.models.CheckmarkList;
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.
*/
public class SQLModelFactory implements ModelFactory
{
@Override
public RepetitionList buidRepetitionList(Habit habit)
{
return new SQLiteRepetitionList(habit);
}
@Override
public CheckmarkList buildCheckmarkList(Habit habit)
{
return new SQLiteCheckmarkList(habit);
}
@Override
public HabitList buildHabitList()
{
return new SQLiteHabitList();
}
@Override
public ScoreList buildScoreList(Habit habit)
{
return new SQLiteScoreList(habit);
}
@Override
public StreakList buildStreakList(Habit habit)
{
return new SQLiteStreakList(habit);
}
}

@ -0,0 +1,139 @@
/*
* 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.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.utils.DateUtils;
/**
* Implementation of a {@link CheckmarkList} that is backed by SQLite.
*/
public class SQLiteCheckmarkList extends CheckmarkList
{
public SQLiteCheckmarkList(Habit habit)
{
super(habit);
}
@Override
public void invalidateNewerThan(long timestamp)
{
new Delete()
.from(CheckmarkRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp >= ?", timestamp)
.execute();
observable.notifyListeners();
}
@Override
@NonNull
public int[] getValues(long fromTimestamp, long toTimestamp)
{
compute(fromTimestamp, toTimestamp);
if (fromTimestamp > toTimestamp) return new int[0];
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
@Nullable
protected Checkmark getNewest()
{
CheckmarkRecord record = new Select()
.from(CheckmarkRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp <= ?", DateUtils.getStartOfToday())
.orderBy("timestamp desc")
.limit(1)
.executeSingle();
return record.toCheckmark();
}
@Override
protected void insert(long timestamps[], int values[])
{
String query =
"insert into Checkmarks(habit, timestamp, value) values (?,?,?)";
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();
}
}
}

@ -0,0 +1,232 @@
/*
* 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.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import com.activeandroid.query.Update;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
/**
* Implementation of a {@link HabitList} that is backed by SQLite.
*/
public class SQLiteHabitList extends HabitList
{
private static SQLiteHabitList instance;
private HashMap<Long, Habit> cache;
public SQLiteHabitList()
{
cache = new HashMap<>();
}
public static SQLiteHabitList getInstance()
{
if (instance == null) instance = new SQLiteHabitList();
return instance;
}
@Override
public void add(Habit habit)
{
if(cache.containsValue(habit))
throw new RuntimeException("habit already in cache");
HabitRecord record = new HabitRecord();
record.copyFrom(habit);
record.position = countWithArchived();
Long id = habit.getId();
if(id == null) id = record.save();
else record.save(id);
habit.setId(id);
cache.put(id, habit);
}
@Override
public int count()
{
return select().count();
}
@Override
public int countWithArchived()
{
return selectWithArchived().count();
}
@Override
@NonNull
public List<Habit> getAll(boolean includeArchive)
{
List<HabitRecord> recordList;
if (includeArchive) recordList = selectWithArchived().execute();
else recordList = select().execute();
List<Habit> habits = new LinkedList<>();
for (HabitRecord record : recordList)
{
Habit habit = getById(record.getId());
if (habit == null)
throw new RuntimeException("habit not in database");
habits.add(habit);
}
return habits;
}
@Override
@Nullable
public Habit getById(long id)
{
if (!cache.containsKey(id))
{
HabitRecord record = HabitRecord.get(id);
if (record == null) return null;
Habit habit = new Habit();
record.copyTo(habit);
cache.put(id, habit);
}
return cache.get(id);
}
@Override
@Nullable
public Habit getByPosition(int position)
{
HabitRecord record = selectWithArchived()
.where("position = ?", position)
.executeSingle();
return getById(record.getId());
}
@Override
public int indexOf(Habit h)
{
HabitRecord record = HabitRecord.get(h.getId());
if (record == null) return -1;
return record.position;
}
@Deprecated
public void rebuildOrder()
{
List<Habit> habits = getAll(true);
int i = 0;
for (Habit h : habits)
{
HabitRecord record = HabitRecord.get(h.getId());
if (record == null)
throw new RuntimeException("habit not in database");
record.position = i++;
record.save();
}
update(habits);
}
@Override
public void remove(@NonNull Habit habit)
{
if (!cache.containsKey(habit.getId()))
throw new RuntimeException("habit not in cache");
cache.remove(habit.getId());
HabitRecord record = HabitRecord.get(habit.getId());
if (record == null) throw new RuntimeException("habit not in database");
record.cascadeDelete();
rebuildOrder();
}
@Override
public void reorder(Habit from, Habit to)
{
if (from == to) return;
Integer toPos = indexOf(to);
Integer fromPos = indexOf(from);
if (toPos < fromPos)
{
new Update(HabitRecord.class)
.set("position = position + 1")
.where("position >= ? and position < ?", toPos, fromPos)
.execute();
}
else
{
new Update(HabitRecord.class)
.set("position = position - 1")
.where("position > ? and position <= ?", fromPos, toPos)
.execute();
}
HabitRecord record = HabitRecord.get(from.getId());
if (record == null) throw new RuntimeException("habit not in database");
record.position = toPos;
record.save();
update(from);
}
@Override
public void update(List<Habit> habits)
{
for (Habit h : habits)
{
HabitRecord record = HabitRecord.get(h.getId());
if (record == null)
throw new RuntimeException("habit not in database");
record.copyFrom(h);
record.save();
}
}
@NonNull
private From select()
{
return new Select()
.from(HabitRecord.class)
.where("archived = 0")
.orderBy("position");
}
@NonNull
private From selectWithArchived()
{
return new Select().from(HabitRecord.class).orderBy("position");
}
}

@ -0,0 +1,143 @@
/*
* 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.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.query.Delete;
import com.activeandroid.query.From;
import com.activeandroid.query.Select;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.RepetitionList;
import org.isoron.uhabits.utils.DateUtils;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
/**
* Implementation of a {@link RepetitionList} that is backed by SQLite.
*/
public class SQLiteRepetitionList extends RepetitionList
{
HashMap<Long, Repetition> cache;
public SQLiteRepetitionList(@NonNull Habit habit)
{
super(habit);
this.cache = new HashMap<>();
}
@Override
public void add(Repetition rep)
{
RepetitionRecord record = new RepetitionRecord();
record.copyFrom(rep);
long id = record.save();
cache.put(id, rep);
observable.notifyListeners();
}
@Override
public List<Repetition> getByInterval(long timeFrom, long timeTo)
{
return getFromRecord(selectFromTo(timeFrom, timeTo).execute());
}
@Override
public Repetition getByTimestamp(long timestamp)
{
RepetitionRecord record =
select().where("timestamp = ?", timestamp).executeSingle();
return getFromRecord(record);
}
@Override
public Repetition getOldest()
{
RepetitionRecord record = select().limit(1).executeSingle();
return getFromRecord(record);
}
@Override
public void remove(@NonNull Repetition repetition)
{
new Delete()
.from(RepetitionRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp = ?", repetition.getTimestamp())
.execute();
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
private From select()
{
return new Select()
.from(RepetitionRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp <= ?", DateUtils.getStartOfToday())
.orderBy("timestamp");
}
@NonNull
private From selectFromTo(long timeFrom, long timeTo)
{
return select()
.and("timestamp >= ?", timeFrom)
.and("timestamp <= ?", timeTo);
}
}

@ -0,0 +1,173 @@
/*
* 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.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.activeandroid.Cache;
import com.activeandroid.query.Delete;
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.Repetition;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.models.ScoreList;
import org.isoron.uhabits.utils.DateUtils;
import java.util.List;
/**
* Implementation of a ScoreList that is backed by SQLite.
*/
public class SQLiteScoreList extends ScoreList
{
/**
* Constructs a new ScoreList associated with the given habit.
*
* @param habit the habit this list should be associated with
*/
public SQLiteScoreList(@NonNull Habit 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();
}
@Nullable
@Override
protected Score getNewestComputed()
{
ScoreRecord record = select().limit(1).executeSingle();
return record.toScore();
}
@Override
@Nullable
protected Score get(long timestamp)
{
Repetition oldestRep = habit.getRepetitions().getOldest();
if (oldestRep == null) return null;
compute(oldestRep.getTimestamp(), timestamp);
ScoreRecord record =
select().where("timestamp = ?", timestamp).executeSingle();
return record.toScore();
}
@Override
@NonNull
protected int[] getValues(long from, long to, long divisor)
{
compute(from, to);
divisor *= DateUtils.millisecondsInOneDay;
Long offset = to + divisor;
String query =
"select ((timestamp - ?) / ?) as time, avg(score) from Score " +
"where habit = ? and timestamp >= ? and timestamp <= ? " +
"group by time order by time desc";
String params[] = {
offset.toString(),
Long.toString(divisor),
habit.getId().toString(),
Long.toString(from),
Long.toString(to)
};
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if (!cursor.moveToFirst()) return new int[0];
int k = 0;
int[] scores = new int[cursor.getCount()];
do
{
scores[k++] = (int) cursor.getFloat(1);
} while (cursor.moveToNext());
cursor.close();
return scores;
}
@Override
protected void add(List<Score> scores)
{
String query =
"insert into Score(habit, timestamp, score) values (?,?,?)";
SQLiteDatabase db = Cache.openDatabase();
db.beginTransaction();
try
{
SQLiteStatement statement = db.compileStatement(query);
for (Score s : scores)
{
statement.bindLong(1, habit.getId());
statement.bindLong(2, s.getTimestamp());
statement.bindLong(3, s.getValue());
statement.execute();
}
db.setTransactionSuccessful();
}
finally
{
db.endTransaction();
}
}
protected From select()
{
return new Select()
.from(ScoreRecord.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc");
}
}

@ -0,0 +1,115 @@
/*
* 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 com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak;
import org.isoron.uhabits.models.StreakList;
import org.isoron.uhabits.utils.DatabaseUtils;
import org.isoron.uhabits.utils.DateUtils;
import java.util.LinkedList;
import java.util.List;
/**
* Implementation of a StreakList that is backed by SQLite.
*/
public class SQLiteStreakList extends StreakList
{
public SQLiteStreakList(Habit habit)
{
super(habit);
}
@Override
public List<Streak> getAll()
{
rebuild();
List<StreakRecord> records = new Select()
.from(StreakRecord.class)
.where("habit = ?", habit.getId())
.orderBy("end desc")
.execute();
return recordsToStreaks(records);
}
@Override
public Streak getNewestComputed()
{
rebuild();
return getNewestRecord().toStreak();
}
@Override
public void invalidateNewerThan(long timestamp)
{
new Delete()
.from(StreakRecord.class)
.where("habit = ?", habit.getId())
.and("end >= ?", timestamp - DateUtils.millisecondsInOneDay)
.execute();
observable.notifyListeners();
}
private StreakRecord getNewestRecord()
{
return new Select()
.from(StreakRecord.class)
.where("habit = ?", habit.getId())
.orderBy("end desc")
.limit(1)
.executeSingle();
}
@Override
protected void insert(List<Streak> streaks)
{
DatabaseUtils.executeAsTransaction(() -> {
for (Streak streak : streaks)
{
StreakRecord record = new StreakRecord();
record.copyFrom(streak);
record.save();
}
});
}
private List<Streak> recordsToStreaks(List<StreakRecord> records)
{
LinkedList<Streak> streaks = new LinkedList<>();
for (StreakRecord record : records)
streaks.add(record.toStreak());
return streaks;
}
@Override
protected void removeNewestComputed()
{
StreakRecord newestStreak = getNewestRecord();
if (newestStreak != null) newestStreak.delete();
}
}

@ -0,0 +1,65 @@
/*
* 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 com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
/**
* The SQLite database record corresponding to a Score.
*/
@Table(name = "Score")
public class ScoreRecord extends Model
{
/**
* Habit to which this score belongs to.
*/
@Column(name = "habit")
public HabitRecord habit;
/**
* Timestamp of the day to which this score applies. Time of day should be
* midnight (UTC).
*/
@Column(name = "timestamp")
public Long timestamp;
/**
* Value of the score.
*/
@Column(name = "score")
public Integer score;
/**
* Constructs and returns a {@link Score} based on this record's data.
*
* @return a {@link Score} with this record's data
*/
public Score toScore()
{
SQLiteHabitList habitList = SQLiteHabitList.getInstance();
Habit h = habitList.getById(habit.getId());
return new Score(h, timestamp, score);
}
}

@ -0,0 +1,66 @@
/*
* 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 com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak;
/**
* The SQLite database record corresponding to a Streak.
*/
@Table(name = "Streak")
public class StreakRecord extends Model
{
@Column(name = "habit")
public HabitRecord habit;
@Column(name = "start")
public Long start;
@Column(name = "end")
public Long end;
@Column(name = "length")
public Long length;
public static StreakRecord get(Long id)
{
return StreakRecord.load(StreakRecord.class, id);
}
public void copyFrom(Streak streak)
{
habit = HabitRecord.get(streak.getHabit().getId());
start = streak.getStart();
end = streak.getEnd();
length = streak.getLength();
}
public Streak toStreak()
{
SQLiteHabitList habitList = SQLiteHabitList.getInstance();
Habit h = habitList.getById(habit.getId());
return new Streak(h, start, end);
}
}

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides SQLite implementations of the core models.
*/
package org.isoron.uhabits.models.sqlite;

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides classes for the Loop Habit Tracker app.
*/
package org.isoron.uhabits;

@ -0,0 +1,24 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides async tasks for useful operations such as {@link
* org.isoron.uhabits.tasks.ExportCSVTask}.
*/
package org.isoron.uhabits.tasks;

@ -25,6 +25,8 @@ import android.support.annotation.NonNull;
import android.view.WindowManager;
import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.DateUtils;
import org.isoron.uhabits.utils.FileUtils;
@ -38,13 +40,19 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import javax.inject.Inject;
public class BaseSystem
{
private Context context;
@Inject
HabitList habitList;
public BaseSystem(Context context)
{
this.context = context;
HabitsApplication.getComponent().inject(this);
}
public String getLogcat() throws IOException
@ -146,7 +154,7 @@ public class BaseSystem
@Override
protected void doInBackground()
{
ReminderUtils.createReminderAlarms(context);
ReminderUtils.createReminderAlarms(context, habitList);
}
}.execute();
}

@ -18,6 +18,6 @@
*/
/**
* Contains classes for AboutActivity
* Provides activity that shows information about the app.
*/
package org.isoron.uhabits.ui.about;

@ -85,8 +85,8 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
if (position < 0 || position > 4) throw new IllegalArgumentException();
int freqNums[] = {1, 1, 2, 5, 3};
int freqDens[] = {1, 7, 7, 7, 7};
modifiedHabit.freqNum = freqNums[position];
modifiedHabit.freqDen = freqDens[position];
modifiedHabit.setFreqNum(freqNums[position]);
modifiedHabit.setFreqDen(freqDens[position]);
helper.populateFrequencyFields(modifiedHabit);
}
@ -95,12 +95,12 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
public void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putInt("color", modifiedHabit.color);
outState.putInt("color", modifiedHabit.getColor());
if (modifiedHabit.hasReminder())
{
outState.putInt("reminderMin", modifiedHabit.reminderMin);
outState.putInt("reminderHour", modifiedHabit.reminderHour);
outState.putInt("reminderDays", modifiedHabit.reminderDays);
outState.putInt("reminderMin", modifiedHabit.getReminderMin());
outState.putInt("reminderHour", modifiedHabit.getReminderHour());
outState.putInt("reminderDays", modifiedHabit.getReminderDays());
}
}
@ -123,8 +123,8 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
if (modifiedHabit.hasReminder())
{
defaultHour = modifiedHabit.reminderHour;
defaultMin = modifiedHabit.reminderMin;
defaultHour = modifiedHabit.getReminderHour();
defaultMin = modifiedHabit.getReminderMin();
}
showTimePicker(defaultHour, defaultMin);
@ -147,18 +147,19 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
WeekdayPickerDialog dialog = new WeekdayPickerDialog();
dialog.setListener(new OnWeekdaysPickedListener());
dialog.setSelectedDays(
DateUtils.unpackWeekdayList(modifiedHabit.reminderDays));
DateUtils.unpackWeekdayList(modifiedHabit.getReminderDays()));
dialog.show(getFragmentManager(), "weekdayPicker");
}
protected void restoreSavedInstance(@Nullable Bundle bundle)
{
if (bundle == null) return;
modifiedHabit.color = bundle.getInt("color", modifiedHabit.color);
modifiedHabit.reminderMin = bundle.getInt("reminderMin", -1);
modifiedHabit.reminderHour = bundle.getInt("reminderHour", -1);
modifiedHabit.reminderDays = bundle.getInt("reminderDays", -1);
if (modifiedHabit.reminderMin < 0) modifiedHabit.clearReminder();
modifiedHabit.setColor(
bundle.getInt("color", modifiedHabit.getColor()));
modifiedHabit.setReminderMin(bundle.getInt("reminderMin", -1));
modifiedHabit.setReminderHour(bundle.getInt("reminderHour", -1));
modifiedHabit.setReminderDays(bundle.getInt("reminderDays", -1));
if (modifiedHabit.getReminderMin() < 0) modifiedHabit.clearReminder();
}
protected abstract void saveHabit();
@ -167,7 +168,7 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
void showColorPicker()
{
int androidColor =
ColorUtils.getColor(getContext(), modifiedHabit.color);
ColorUtils.getColor(getContext(), modifiedHabit.getColor());
ColorPickerDialog picker =
ColorPickerDialog.newInstance(R.string.color_picker_default_title,
@ -196,7 +197,7 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
int paletteColor =
ColorUtils.colorToPaletteIndex(getActivity(), androidColor);
prefs.setDefaultHabitColor(paletteColor);
modifiedHabit.color = paletteColor;
modifiedHabit.setColor(paletteColor);
helper.populateColor(paletteColor);
}
}
@ -214,9 +215,9 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
@Override
public void onTimeSet(RadialPickerLayout view, int hour, int minute)
{
modifiedHabit.reminderHour = hour;
modifiedHabit.reminderMin = minute;
modifiedHabit.reminderDays = DateUtils.ALL_WEEK_DAYS;
modifiedHabit.setReminderHour(hour);
modifiedHabit.setReminderMin(minute);
modifiedHabit.setReminderDays(DateUtils.ALL_WEEK_DAYS);
helper.populateReminderFields(modifiedHabit);
}
}
@ -229,8 +230,8 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment
{
if (isSelectionEmpty(selectedDays)) Arrays.fill(selectedDays, true);
modifiedHabit.reminderDays =
DateUtils.packWeekdayList(selectedDays);
modifiedHabit.setReminderDays(
DateUtils.packWeekdayList(selectedDays));
helper.populateReminderFields(modifiedHabit);
}

@ -73,12 +73,12 @@ public class BaseDialogHelper
void parseFormIntoHabit(Habit habit)
{
habit.name = tvName.getText().toString().trim();
habit.description = tvDescription.getText().toString().trim();
habit.setName(tvName.getText().toString().trim());
habit.setDescription(tvDescription.getText().toString().trim());
String freqNum = tvFreqNum.getText().toString();
String freqDen = tvFreqDen.getText().toString();
if (!freqNum.isEmpty()) habit.freqNum = Integer.parseInt(freqNum);
if (!freqDen.isEmpty()) habit.freqDen = Integer.parseInt(freqDen);
if (!freqNum.isEmpty()) habit.setFreqNum(Integer.parseInt(freqNum));
if (!freqDen.isEmpty()) habit.setFreqDen(Integer.parseInt(freqDen));
}
void populateColor(int paletteColor)
@ -89,10 +89,11 @@ public class BaseDialogHelper
protected void populateForm(final Habit habit)
{
if (habit.name != null) tvName.setText(habit.name);
if (habit.description != null) tvDescription.setText(habit.description);
if (habit.getName() != null) tvName.setText(habit.getName());
if (habit.getDescription() != null) tvDescription.setText(
habit.getDescription());
populateColor(habit.color);
populateColor(habit.getColor());
populateFrequencyFields(habit);
populateReminderFields(habit);
}
@ -102,15 +103,15 @@ public class BaseDialogHelper
{
int quickSelectPosition = -1;
if (habit.freqNum.equals(habit.freqDen)) quickSelectPosition = 0;
if (habit.getFreqNum().equals(habit.getFreqDen())) quickSelectPosition = 0;
else if (habit.freqNum == 1 && habit.freqDen == 7)
else if (habit.getFreqNum() == 1 && habit.getFreqDen() == 7)
quickSelectPosition = 1;
else if (habit.freqNum == 2 && habit.freqDen == 7)
else if (habit.getFreqNum() == 2 && habit.getFreqDen() == 7)
quickSelectPosition = 2;
else if (habit.freqNum == 5 && habit.freqDen == 7)
else if (habit.getFreqNum() == 5 && habit.getFreqDen() == 7)
quickSelectPosition = 3;
if (quickSelectPosition >= 0)
@ -118,8 +119,8 @@ public class BaseDialogHelper
else showCustomFrequency();
tvFreqNum.setText(habit.freqNum.toString());
tvFreqDen.setText(habit.freqDen.toString());
tvFreqNum.setText(habit.getFreqNum().toString());
tvFreqDen.setText(habit.getFreqDen().toString());
}
@SuppressWarnings("ConstantConditions")
@ -133,12 +134,13 @@ public class BaseDialogHelper
}
String time =
DateUtils.formatTime(frag.getContext(), habit.reminderHour,
habit.reminderMin);
DateUtils.formatTime(frag.getContext(), habit.getReminderHour(),
habit.getReminderMin());
tvReminderTime.setText(time);
llReminderDays.setVisibility(View.VISIBLE);
boolean weekdays[] = DateUtils.unpackWeekdayList(habit.reminderDays);
boolean weekdays[] = DateUtils.unpackWeekdayList(
habit.getReminderDays());
tvReminderDays.setText(
DateUtils.formatWeekdayList(frag.getContext(), weekdays));
}
@ -161,21 +163,21 @@ public class BaseDialogHelper
{
Boolean valid = true;
if (habit.name.length() == 0)
if (habit.getName().length() == 0)
{
tvName.setError(
frag.getString(R.string.validation_name_should_not_be_blank));
valid = false;
}
if (habit.freqNum <= 0)
if (habit.getFreqNum() <= 0)
{
tvFreqNum.setError(
frag.getString(R.string.validation_number_should_be_positive));
valid = false;
}
if (habit.freqNum > habit.freqDen)
if (habit.getFreqNum() > habit.getFreqDen())
{
tvFreqNum.setError(
frag.getString(R.string.validation_at_most_one_rep_per_day));

@ -19,6 +19,7 @@
package org.isoron.uhabits.ui.habits.edit;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CreateHabitCommand;
@ -36,9 +37,10 @@ public class CreateHabitDialogFragment extends BaseDialogFragment
protected void initializeHabits()
{
modifiedHabit = new Habit();
modifiedHabit.freqNum = 1;
modifiedHabit.freqDen = 1;
modifiedHabit.color = prefs.getDefaultHabitColor(modifiedHabit.color);
modifiedHabit.setFreqNum(1);
modifiedHabit.setFreqDen(1);
modifiedHabit.setColor(
prefs.getDefaultHabitColor(modifiedHabit.getColor()));
}
protected void saveHabit()

@ -21,13 +21,20 @@ package org.isoron.uhabits.ui.habits.edit;
import android.os.Bundle;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.EditHabitCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import javax.inject.Inject;
public class EditHabitDialogFragment extends BaseDialogFragment
{
@Inject
HabitList habitList;
public static EditHabitDialogFragment newInstance(long habitId)
{
EditHabitDialogFragment frag = new EditHabitDialogFragment();
@ -46,14 +53,18 @@ public class EditHabitDialogFragment extends BaseDialogFragment
@Override
protected void initializeHabits()
{
HabitsApplication.getComponent().inject(this);
Long habitId = (Long) getArguments().get("habitId");
if (habitId == null)
throw new IllegalArgumentException("habitId must be specified");
originalHabit = Habit.get(habitId);
modifiedHabit = new Habit(originalHabit);
originalHabit = habitList.getById(habitId);
modifiedHabit = new Habit();
modifiedHabit.copyFrom(originalHabit);
}
@Override
protected void saveHabit()
{
Command command = new EditHabitCommand(originalHabit, modifiedHabit);

@ -27,10 +27,14 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatDialogFragment;
import android.util.DisplayMetrics;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.views.HabitHistoryView;
import org.isoron.uhabits.ui.habits.show.views.HabitHistoryView;
import javax.inject.Inject;
public class HistoryEditorDialog extends AppCompatDialogFragment
implements DialogInterface.OnClickListener
@ -41,16 +45,20 @@ public class HistoryEditorDialog extends AppCompatDialogFragment
HabitHistoryView historyView;
@Inject
HabitList habitList;
@Override
public Dialog onCreateDialog(Bundle savedInstanceState)
{
Context context = getActivity();
HabitsApplication.getComponent().inject(this);
historyView = new HabitHistoryView(context, null);
if (savedInstanceState != null)
{
long id = savedInstanceState.getLong("habit", -1);
if (id > 0) this.habit = Habit.get(id);
if (id > 0) this.habit = habitList.getById(id);
}
int padding =

@ -0,0 +1,23 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Provides dialogs for editing habits and related classes.
*/
package org.isoron.uhabits.ui.habits.edit;

@ -21,25 +21,33 @@ package org.isoron.uhabits.ui.habits.list;
import android.os.Bundle;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.ui.BaseSystem;
import javax.inject.Inject;
/**
* Activity that allows the user to see and modify the list of habits.
*/
public class ListHabitsActivity extends BaseActivity
{
@Inject
HabitList habitList;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
HabitsApplication.getComponent().inject(this);
BaseSystem system = new BaseSystem(this);
ListHabitsScreen screen = new ListHabitsScreen(this);
ListHabitsController controller =
new ListHabitsController(screen, system);
new ListHabitsController(screen, system, habitList);
screen.setController(controller);
setScreen(screen);
controller.onStartup();
}

@ -26,6 +26,7 @@ import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.CommandRunner;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import org.isoron.uhabits.tasks.ExportCSVTask;
import org.isoron.uhabits.tasks.ExportDBTask;
import org.isoron.uhabits.tasks.ImportDataTask;
@ -48,6 +49,9 @@ public class ListHabitsController
@NonNull
private final BaseSystem system;
@NonNull
private final HabitList habitList;
@Inject
Preferences prefs;
@ -55,17 +59,19 @@ public class ListHabitsController
CommandRunner commandRunner;
public ListHabitsController(@NonNull ListHabitsScreen screen,
@NonNull BaseSystem system)
@NonNull BaseSystem system,
@NonNull HabitList habitList)
{
this.screen = screen;
this.system = system;
this.habitList = habitList;
HabitsApplication.getComponent().inject(this);
}
public void onExportCSV()
{
ExportCSVTask task =
new ExportCSVTask(Habit.getAll(true), screen.getProgressBar());
new ExportCSVTask(habitList.getAll(true), screen.getProgressBar());
task.setListener(filename -> {
if (filename != null) screen.showSendFileScreen(filename);
else screen.showMessage(R.string.could_not_export);
@ -92,7 +98,7 @@ public class ListHabitsController
@Override
public void onHabitReorder(@NonNull Habit from, @NonNull Habit to)
{
Habit.reorder(from, to);
habitList.reorder(from, to);
}
public void onImportData(File file)
@ -133,7 +139,8 @@ public class ListHabitsController
try
{
system.dumpBugReportToFile();
} catch (IOException e)
}
catch (IOException e)
{
// ignored
}
@ -146,7 +153,8 @@ public class ListHabitsController
String to = "dev@loophabits.org";
String subject = "Bug Report - Loop Habit Tracker";
screen.showSendEmailScreen(log, to, subject);
} catch (IOException e)
}
catch (IOException e)
{
e.printStackTrace();
screen.showMessage(R.string.bug_report_failed);

@ -115,7 +115,7 @@ public class ListHabitsScreen extends BaseScreen
public void showColorPicker(Habit habit, OnColorSelectedListener callback)
{
int color = ColorUtils.getColor(activity, habit.color);
int color = ColorUtils.getColor(activity, habit.getColor());
ColorPickerDialog picker =
ColorPickerDialog.newInstance(R.string.color_picker_default_title,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save