mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 01:08:50 -06:00
Separate ActiveAndroid from models
This commit is contained in:
@@ -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()
|
||||
private final HabitList habitList;
|
||||
|
||||
public HabitFixtures(HabitList habitList)
|
||||
{
|
||||
this.habitList = habitList;
|
||||
}
|
||||
|
||||
public Habit createEmptyHabit()
|
||||
{
|
||||
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();
|
||||
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 Habit createLongHabit()
|
||||
{
|
||||
Habit habit = createEmptyHabit();
|
||||
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};
|
||||
|
||||
for (int mark : marks)
|
||||
habit.getRepetitions().toggleTimestamp(today - mark * day);
|
||||
|
||||
return habit;
|
||||
}
|
||||
|
||||
public Habit createShortHabit()
|
||||
{
|
||||
Habit habit = new Habit();
|
||||
habit.setName("Wake up early");
|
||||
habit.setDescription("Did you wake up before 6am?");
|
||||
habit.setFreqNum(2);
|
||||
habit.setFreqDen(3);
|
||||
habitList.add(habit);
|
||||
|
||||
long timestamp = DateUtils.getStartOfToday();
|
||||
for(boolean c : NON_DAILY_HABIT_CHECKS)
|
||||
for (boolean c : NON_DAILY_HABIT_CHECKS)
|
||||
{
|
||||
if(c) habit.repetitions.toggle(timestamp);
|
||||
if (c) habit.getRepetitions().toggleTimestamp(timestamp);
|
||||
timestamp -= DateUtils.millisecondsInOneDay;
|
||||
}
|
||||
|
||||
return habit;
|
||||
}
|
||||
|
||||
public static Habit createEmptyHabit()
|
||||
public void purgeHabits(HabitList habitList)
|
||||
{
|
||||
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();
|
||||
return habit;
|
||||
}
|
||||
|
||||
public static Habit createLongHabit()
|
||||
{
|
||||
Habit habit = createEmptyHabit();
|
||||
habit.freqNum = 3;
|
||||
habit.freqDen = 7;
|
||||
habit.color = 4;
|
||||
habit.save();
|
||||
|
||||
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};
|
||||
|
||||
for(int mark : marks)
|
||||
habit.repetitions.toggle(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()
|
||||
{
|
||||
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.scores.getTodayValue();
|
||||
habit.streaks.getAll(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ExportDBTask task = new ExportDBTask(null);
|
||||
task.setListener(new ExportDBTask.Listener()
|
||||
{
|
||||
@Override
|
||||
public void onExportDBFinished(@Nullable String filename)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static void purgeHabits()
|
||||
{
|
||||
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;
|
||||
|
||||
private Habit habit;
|
||||
|
||||
private Habit modified;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp()
|
||||
{
|
||||
super.setUp();
|
||||
|
||||
habit = HabitFixtures.createShortHabit();
|
||||
habit.name = "original";
|
||||
habit.freqDen = 1;
|
||||
habit.freqNum = 1;
|
||||
habit.save();
|
||||
habit = habitFixtures.createShortHabit();
|
||||
habit.setName("original");
|
||||
habit.setFreqDen(1);
|
||||
habit.setFreqNum(1);
|
||||
|
||||
id = habit.getId();
|
||||
|
||||
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 assertPathExists(String s)
|
||||
{
|
||||
assertAbsolutePathExists(String.format("%s/%s", baseDir.getAbsolutePath(), s));
|
||||
}
|
||||
|
||||
private void assertAbsolutePathExists(String s)
|
||||
{
|
||||
File file = new File(s);
|
||||
assertTrue(String.format("File %s should exist", file.getAbsolutePath()), file.exists());
|
||||
assertTrue(
|
||||
String.format("File %s should exist", file.getAbsolutePath()),
|
||||
file.exists());
|
||||
}
|
||||
|
||||
private void assertPathExists(String s)
|
||||
{
|
||||
assertAbsolutePathExists(
|
||||
String.format("%s/%s", baseDir.getAbsolutePath(), s));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,55 +59,14 @@ public class ScoreListTest extends BaseAndroidTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_invalidateNewerThan()
|
||||
{
|
||||
assertThat(habit.scores.getTodayValue(), equalTo(0));
|
||||
|
||||
toggleRepetitions(0, 2);
|
||||
assertThat(habit.scores.getTodayValue(), equalTo(1948077));
|
||||
|
||||
habit.freqNum = 1;
|
||||
habit.freqDen = 2;
|
||||
habit.scores.invalidateNewerThan(0);
|
||||
|
||||
assertThat(habit.scores.getTodayValue(), equalTo(1974654));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getTodayStarValue()
|
||||
{
|
||||
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));
|
||||
|
||||
while(habit.scores.getTodayValue() < Score.FULL_STAR_CUTOFF) toggleRepetitions(k, ++k);
|
||||
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.FULL_STAR));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getTodayValue()
|
||||
{
|
||||
toggleRepetitions(0, 20);
|
||||
assertThat(habit.scores.getTodayValue(), equalTo(12629351));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getValue()
|
||||
public void test_getAllValues_withGroups()
|
||||
{
|
||||
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[] = {11434978, 7894999, 3212362};
|
||||
|
||||
long current = DateUtils.getStartOfToday();
|
||||
for(int expectedValue : expectedValues)
|
||||
{
|
||||
assertThat(habit.scores.getValue(current), equalTo(expectedValue));
|
||||
current -= DateUtils.millisecondsInOneDay;
|
||||
}
|
||||
int actualValues[] = habit.getScores().getAllValues(7);
|
||||
assertThat(actualValues, equalTo(expectedValues));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -117,33 +74,99 @@ 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
|
||||
};
|
||||
|
||||
int actualValues[] = habit.scores.getAllValues(1);
|
||||
int actualValues[] = habit.getScores().getAllValues(1);
|
||||
assertThat(actualValues, equalTo(expectedValues));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getAllValues_withGroups()
|
||||
public void test_getTodayValue()
|
||||
{
|
||||
toggleRepetitions(0, 20);
|
||||
assertThat(habit.getScores().getTodayValue(), equalTo(12629351));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_getValue()
|
||||
{
|
||||
toggleRepetitions(0, 20);
|
||||
|
||||
int expectedValues[] = { 11434978, 7894999, 3212362 };
|
||||
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(7);
|
||||
assertThat(actualValues, equalTo(expectedValues));
|
||||
long current = DateUtils.getStartOfToday();
|
||||
for (int expectedValue : expectedValues)
|
||||
{
|
||||
assertThat(habit.getScores().getValue(current),
|
||||
equalTo(expectedValue));
|
||||
current -= DateUtils.millisecondsInOneDay;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_invalidateNewerThan()
|
||||
{
|
||||
assertThat(habit.getScores().getTodayValue(), equalTo(0));
|
||||
|
||||
toggleRepetitions(0, 2);
|
||||
assertThat(habit.getScores().getTodayValue(), equalTo(1948077));
|
||||
|
||||
habit.setFreqNum(1);
|
||||
habit.setFreqDen(2);
|
||||
habit.getScores().invalidateNewerThan(0);
|
||||
|
||||
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 / 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 / 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,33 +53,11 @@ 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
|
||||
{
|
||||
assertRenders(view, PATH + "render_unchecked.png");
|
||||
}
|
||||
|
||||
protected void assertRendersCheckedImplicitly() throws IOException
|
||||
{
|
||||
assertRenders(view, PATH + "render_implicit_check.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_unchecked() throws Exception
|
||||
{
|
||||
view.setValue(Checkmark.UNCHECKED);
|
||||
assertRendersUnchecked();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_explicitCheck() throws Exception
|
||||
{
|
||||
@@ -92,6 +72,28 @@ public class CheckmarkButtonViewTest extends ViewTest
|
||||
assertRendersCheckedImplicitly();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_unchecked() throws Exception
|
||||
{
|
||||
view.setValue(Checkmark.UNCHECKED);
|
||||
assertRendersUnchecked();
|
||||
}
|
||||
|
||||
protected void assertRendersCheckedExplicitly() throws IOException
|
||||
{
|
||||
assertRenders(view, PATH + "render_explicit_check.png");
|
||||
}
|
||||
|
||||
protected void assertRendersCheckedImplicitly() throws IOException
|
||||
{
|
||||
assertRenders(view, PATH + "render_implicit_check.png");
|
||||
}
|
||||
|
||||
protected void assertRendersUnchecked() throws IOException
|
||||
{
|
||||
assertRenders(view, PATH + "render_unchecked.png");
|
||||
}
|
||||
|
||||
// @Test
|
||||
// public void testLongClick() throws Exception
|
||||
// {
|
||||
|
||||
@@ -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);
|
||||
@@ -56,26 +56,49 @@ public class HabitHistoryViewTest extends ViewTest
|
||||
refreshData(view);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_atInvalidLocations() throws Throwable
|
||||
{
|
||||
int expectedCheckmarkValues[] = habit.getCheckmarks().getAllValues();
|
||||
|
||||
view.setIsEditable(true);
|
||||
tap(view, 118, 13); // header
|
||||
tap(view, 336, 60); // tomorrow's square
|
||||
tap(view, 370, 60); // right axis
|
||||
waitForAsyncTasks();
|
||||
|
||||
int actualCheckmarkValues[] = habit.getCheckmarks().getAllValues();
|
||||
assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_withEditableView() throws Throwable
|
||||
{
|
||||
view.setIsEditable(true);
|
||||
tap(view, 340, 40); // today's square
|
||||
waitForAsyncTasks();
|
||||
|
||||
long today = DateUtils.getStartOfToday();
|
||||
assertFalse(habit.getRepetitions().containsTimestamp(today));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_withReadOnlyView() throws Throwable
|
||||
{
|
||||
view.setIsEditable(false);
|
||||
tap(view, 340, 40); // today's square
|
||||
waitForAsyncTasks();
|
||||
|
||||
long today = DateUtils.getStartOfToday();
|
||||
assertTrue(habit.getRepetitions().containsTimestamp(today));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender() throws Throwable
|
||||
{
|
||||
assertRenders(view, "HabitHistoryView/render.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_withTransparentBackground() throws Throwable
|
||||
{
|
||||
view.setIsBackgroundTransparent(true);
|
||||
assertRenders(view, "HabitHistoryView/renderTransparent.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_withDifferentSize() throws Throwable
|
||||
{
|
||||
measureView(dpToPixels(200), dpToPixels(200), view);
|
||||
assertRenders(view, "HabitHistoryView/renderDifferentSize.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_withDataOffset() throws Throwable
|
||||
{
|
||||
@@ -86,40 +109,17 @@ public class HabitHistoryViewTest extends ViewTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_withEditableView() throws Throwable
|
||||
public void testRender_withDifferentSize() throws Throwable
|
||||
{
|
||||
view.setIsEditable(true);
|
||||
tap(view, 340, 40); // today's square
|
||||
waitForAsyncTasks();
|
||||
|
||||
long today = DateUtils.getStartOfToday();
|
||||
assertFalse(habit.repetitions.contains(today));
|
||||
measureView(dpToPixels(200), dpToPixels(200), view);
|
||||
assertRenders(view, "HabitHistoryView/renderDifferentSize.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_atInvalidLocations() throws Throwable
|
||||
public void testRender_withTransparentBackground() throws Throwable
|
||||
{
|
||||
int expectedCheckmarkValues[] = habit.checkmarks.getAllValues();
|
||||
|
||||
view.setIsEditable(true);
|
||||
tap(view, 118, 13); // header
|
||||
tap(view, 336, 60); // tomorrow's square
|
||||
tap(view, 370, 60); // right axis
|
||||
waitForAsyncTasks();
|
||||
|
||||
int actualCheckmarkValues[] = habit.checkmarks.getAllValues();
|
||||
assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tapDate_withReadOnlyView() throws Throwable
|
||||
{
|
||||
view.setIsEditable(false);
|
||||
tap(view, 340, 40); // today's square
|
||||
waitForAsyncTasks();
|
||||
|
||||
long today = DateUtils.getStartOfToday();
|
||||
assertTrue(habit.repetitions.contains(today));
|
||||
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,24 +55,11 @@ 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
|
||||
{
|
||||
view.setIsTransparencyEnabled(true);
|
||||
assertRenders(view, "HabitScoreView/renderTransparent.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_withDifferentSize() throws Throwable
|
||||
{
|
||||
measureView(dpToPixels(200), dpToPixels(200), view);
|
||||
assertRenders(view, "HabitScoreView/renderDifferentSize.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_withDataOffset() throws Throwable
|
||||
{
|
||||
@@ -82,6 +69,13 @@ public class HabitScoreViewTest extends ViewTest
|
||||
assertRenders(view, "HabitScoreView/renderDataOffset.png");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRender_withDifferentSize() throws Throwable
|
||||
{
|
||||
measureView(dpToPixels(200), dpToPixels(200), view);
|
||||
assertRenders(view, "HabitScoreView/renderDifferentSize.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,16 +32,15 @@ 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()
|
||||
{
|
||||
return new Preferences();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
CommandRunner provideCommandRunner()
|
||||
@@ -51,4 +54,24 @@ public class AndroidModule
|
||||
{
|
||||
return new HabitCardListCache();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
HabitList provideHabitList()
|
||||
{
|
||||
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)
|
||||
private boolean checkWeekday(Intent intent, Habit habit)
|
||||
{
|
||||
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context);
|
||||
Intent refreshIntent = new Intent(HabitsApplication.ACTION_REFRESH);
|
||||
manager.sendBroadcast(refreshIntent);
|
||||
Long timestamp =
|
||||
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
|
||||
|
||||
WidgetManager.updateWidgets(context);
|
||||
boolean reminderDays[] =
|
||||
DateUtils.unpackWeekdayList(habit.getReminderDays());
|
||||
int weekday = DateUtils.getWeekday(timestamp);
|
||||
|
||||
return reminderDays[weekday];
|
||||
}
|
||||
|
||||
private void dismissAllHabits()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void dismissNotification(Context context, Long habitId)
|
||||
{
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
|
||||
|
||||
int notificationId = (int) (habitId % Integer.MAX_VALUE);
|
||||
notificationManager.cancel(notificationId);
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
private void createReminderAlarmsDelayed(final Context context)
|
||||
{
|
||||
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);
|
||||
new Handler().postDelayed(
|
||||
() -> ReminderUtils.createReminderAlarms(context, habitList), 5000);
|
||||
}
|
||||
|
||||
public static PendingIntent buildCheckIntent(Context context, Habit habit, Long timestamp)
|
||||
private void dismissAllHabits()
|
||||
{
|
||||
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 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);
|
||||
}
|
||||
|
||||
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 int RESULT_EXPORT_CSV = 2;
|
||||
public static final int RESULT_EXPORT_DB = 3;
|
||||
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_IMPORT_DATA = 1;
|
||||
|
||||
@Nullable
|
||||
private static HabitsApplication application;
|
||||
|
||||
@Nullable
|
||||
private static Context context;
|
||||
private static BaseComponent component;
|
||||
|
||||
public static boolean isTestMode()
|
||||
@Nullable
|
||||
private static Context context;
|
||||
|
||||
@Inject
|
||||
HabitList habitList;
|
||||
|
||||
public static BaseComponent getComponent()
|
||||
{
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
this.savedId = original.getId();
|
||||
this.modified = new Habit(modified);
|
||||
this.original = new Habit(original);
|
||||
HabitsApplication.getComponent().inject(this);
|
||||
|
||||
hasIntervalChanged = (!this.original.freqDen.equals(this.modified.freqDen) ||
|
||||
!this.original.freqNum.equals(this.modified.freqNum));
|
||||
this.savedId = original.getId();
|
||||
this.modified = new Habit();
|
||||
this.original = new Habit();
|
||||
|
||||
this.modified.copyFrom(modified);
|
||||
this.original.copyFrom(original);
|
||||
|
||||
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,62 +58,26 @@ 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]);
|
||||
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();
|
||||
|
||||
createCheckmarks(db, habit, id);
|
||||
|
||||
}
|
||||
while (c.moveToNext());
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (c != null) c.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int tickmateTrackId)
|
||||
{
|
||||
Cursor c = null;
|
||||
|
||||
try
|
||||
{
|
||||
String[] params = { Integer.toString(tickmateTrackId) };
|
||||
c = db.rawQuery("select distinct year, month, day from ticks where _track_id=?", params);
|
||||
String[] params = {Integer.toString(tickmateTrackId)};
|
||||
c = db.rawQuery(
|
||||
"select distinct year, month, day from ticks where _track_id=?",
|
||||
params);
|
||||
if (!c.moveToFirst()) return;
|
||||
|
||||
do
|
||||
@@ -121,9 +89,41 @@ public class TickmateDBImporter extends AbstractImporter
|
||||
GregorianCalendar cal = DateUtils.getStartOfTodayCalendar();
|
||||
cal.set(year, month, day);
|
||||
|
||||
habit.repetitions.toggle(cal.getTimeInMillis());
|
||||
habit.getRepetitions().toggleTimestamp(cal.getTimeInMillis());
|
||||
} while (c.moveToNext());
|
||||
}
|
||||
while (c.moveToNext());
|
||||
finally
|
||||
{
|
||||
if (c != null) c.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void createHabits(SQLiteDatabase db)
|
||||
{
|
||||
Cursor c = null;
|
||||
|
||||
try
|
||||
{
|
||||
c = db.rawQuery("select _id, name, description from tracks",
|
||||
new String[0]);
|
||||
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.setName(name);
|
||||
habit.setDescription(description);
|
||||
habit.setFreqNum(1);
|
||||
habit.setFreqDen(1);
|
||||
habitList.add(habit);
|
||||
|
||||
createCheckmarks(db, habit, id);
|
||||
|
||||
} while (c.moveToNext());
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
23
app/src/main/java/org/isoron/uhabits/io/package-info.java
Normal file
23
app/src/main/java/org/isoron/uhabits/io/package-info.java
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides 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.
|
||||
*/
|
||||
public static final int UNCHECKED = 0;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public static final int CHECKED_EXPLICITLY = 2;
|
||||
|
||||
/**
|
||||
* The habit to which this checkmark belongs.
|
||||
* Indicates that there was no repetition at the timestamp, but one was not
|
||||
* expected in any case, due to the frequency of the habit.
|
||||
*/
|
||||
@Column(name = "habit")
|
||||
public Habit habit;
|
||||
public static final int CHECKED_IMPLICITLY = 1;
|
||||
|
||||
/**
|
||||
* Timestamp of the day to which this checkmark corresponds. Time of the day must be midnight
|
||||
* (UTC).
|
||||
* Indicates that there was no repetition at the timestamp, even though a
|
||||
* repetition was expected.
|
||||
*/
|
||||
@Column(name = "timestamp")
|
||||
public Long timestamp;
|
||||
public static final int UNCHECKED = 0;
|
||||
|
||||
/**
|
||||
* 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 Habit habit;
|
||||
|
||||
final long timestamp;
|
||||
|
||||
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,200 +45,29 @@ 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.
|
||||
*
|
||||
* @param timestamp the timestamp
|
||||
*/
|
||||
public void deleteNewerThan(long timestamp)
|
||||
{
|
||||
new Delete().from(Checkmark.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp >= ?", timestamp)
|
||||
.execute();
|
||||
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param fromTimestamp timestamp for the oldest checkmark
|
||||
* @param toTimestamp timestamp for the newest checkmark
|
||||
* @return values for the checkmarks inside the given interval
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
*
|
||||
* @return values for the checkmarks in the interval
|
||||
*/
|
||||
@NonNull
|
||||
public int[] getAllValues()
|
||||
{
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if(oldestRep == null) return new int[0];
|
||||
Repetition oldestRep = habit.getRepetitions().getOldest();
|
||||
if (oldestRep == null) return new int[0];
|
||||
|
||||
Long fromTimestamp = oldestRep.timestamp;
|
||||
Long fromTimestamp = oldestRep.getTimestamp();
|
||||
Long toTimestamp = DateUtils.getStartOfToday();
|
||||
|
||||
return getValues(fromTimestamp, toTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and stores one checkmark for each day, since the first repetition until today.
|
||||
* Days that already have a corresponding checkmark are skipped.
|
||||
*/
|
||||
protected void computeAll()
|
||||
{
|
||||
long fromTimestamp = habit.repetitions.getOldestTimestamp();
|
||||
if(fromTimestamp == 0) return;
|
||||
|
||||
Long toTimestamp = DateUtils.getStartOfToday();
|
||||
|
||||
compute(fromTimestamp, toTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
if(from > to) return;
|
||||
|
||||
long fromExtended = from - (long) (habit.freqDen) * day;
|
||||
List<Repetition> reps = habit.repetitions
|
||||
.selectFromTo(fromExtended, to)
|
||||
.execute();
|
||||
|
||||
final int nDays = (int) ((to - from) / day) + 1;
|
||||
int nDaysExtended = (int) ((to - fromExtended) / day) + 1;
|
||||
final int checks[] = new int[nDaysExtended];
|
||||
|
||||
for (Repetition rep : reps)
|
||||
{
|
||||
int offset = (int) ((rep.timestamp - fromExtended) / day);
|
||||
checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY;
|
||||
}
|
||||
|
||||
for (int i = 0; i < nDays; i++)
|
||||
{
|
||||
int counter = 0;
|
||||
|
||||
for (int j = 0; j < habit.freqDen; j++)
|
||||
if (checks[i + j] == 2) counter++;
|
||||
|
||||
if (counter >= habit.freqNum)
|
||||
if(checks[i] != Checkmark.CHECKED_EXPLICITLY)
|
||||
checks[i] = Checkmark.CHECKED_IMPLICITLY;
|
||||
}
|
||||
|
||||
|
||||
long timestamps[] = new long[nDays];
|
||||
for (int i = 0; i < nDays; i++)
|
||||
timestamps[i] = to - i * day;
|
||||
|
||||
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
|
||||
*/
|
||||
@Nullable
|
||||
protected Checkmark findNewest()
|
||||
{
|
||||
return new Select().from(Checkmark.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp <= ?", DateUtils.getStartOfToday())
|
||||
.orderBy("timestamp desc")
|
||||
.limit(1)
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the checkmark for today.
|
||||
*
|
||||
@@ -253,7 +78,7 @@ public class CheckmarkList
|
||||
{
|
||||
long today = DateUtils.getStartOfToday();
|
||||
compute(today, today);
|
||||
return findNewest();
|
||||
return getNewest();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,41 +89,133 @@ public class CheckmarkList
|
||||
public int getTodayValue()
|
||||
{
|
||||
Checkmark today = getToday();
|
||||
if(today != null) return today.value;
|
||||
if (today != null) return today.getValue();
|
||||
else return Checkmark.UNCHECKED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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);
|
||||
|
||||
/**
|
||||
* Marks as invalid every checkmark that has timestamp either equal or newer
|
||||
* than a given timestamp. These checkmarks will be recomputed at the next
|
||||
* time they are queried.
|
||||
*
|
||||
* @param timestamp the timestamp
|
||||
*/
|
||||
public abstract void invalidateNewerThan(long timestamp);
|
||||
|
||||
/**
|
||||
* Writes the entire list of checkmarks to the given writer, in CSV format.
|
||||
* 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
|
||||
*/
|
||||
|
||||
public void writeCSV(Writer out) throws IOException
|
||||
{
|
||||
computeAll();
|
||||
|
||||
int values[] = getAllValues();
|
||||
long timestamp = DateUtils.getStartOfToday();
|
||||
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
|
||||
for (int value : values)
|
||||
{
|
||||
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();
|
||||
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.
|
||||
*
|
||||
* @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)
|
||||
{
|
||||
final long day = DateUtils.millisecondsInOneDay;
|
||||
|
||||
Checkmark newestCheckmark = getNewest();
|
||||
if (newestCheckmark != null)
|
||||
from = newestCheckmark.getTimestamp() + day;
|
||||
|
||||
if (from > to) return;
|
||||
|
||||
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;
|
||||
final int checks[] = new int[nDaysExtended];
|
||||
|
||||
for (Repetition rep : reps)
|
||||
{
|
||||
int offset = (int) ((rep.getTimestamp() - fromExtended) / day);
|
||||
checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY;
|
||||
}
|
||||
|
||||
for (int i = 0; i < nDays; i++)
|
||||
{
|
||||
int counter = 0;
|
||||
|
||||
for (int j = 0; j < habit.getFreqDen(); j++)
|
||||
if (checks[i + j] == 2) counter++;
|
||||
|
||||
if (counter >= habit.getFreqNum())
|
||||
if (checks[i] != Checkmark.CHECKED_EXPLICITLY)
|
||||
checks[i] = Checkmark.CHECKED_IMPLICITLY;
|
||||
}
|
||||
|
||||
|
||||
long timestamps[] = new long[nDays];
|
||||
for (int i = 0; i < nDays; i++)
|
||||
timestamps[i] = to - i * day;
|
||||
|
||||
insert(timestamps, checks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and stores one checkmark for each day, since the first
|
||||
* repetition until today. Days that already have a corresponding checkmark
|
||||
* are skipped.
|
||||
*/
|
||||
protected void computeAll()
|
||||
{
|
||||
Repetition oldest = habit.getRepetitions().getOldest();
|
||||
if (oldest == null) return;
|
||||
|
||||
Long today = DateUtils.getStartOfToday();
|
||||
|
||||
compute(oldest.getTimestamp(), today);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns newest checkmark that has already been computed. Ignores any
|
||||
* checkmark that has timestamp in the future. This does not update the
|
||||
* cache.
|
||||
*
|
||||
* @return newest checkmark already computed
|
||||
*/
|
||||
protected abstract Checkmark getNewest();
|
||||
|
||||
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
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* The thing that the user wants to track.
|
||||
*/
|
||||
public class Habit
|
||||
{
|
||||
/**
|
||||
* Name of the habit
|
||||
*/
|
||||
@Column(name = "name")
|
||||
public String name;
|
||||
public static final String HABIT_URI_FORMAT =
|
||||
"content://org.isoron.uhabits/habit/%d";
|
||||
|
||||
/**
|
||||
* Description of the habit
|
||||
*/
|
||||
@Column(name = "description")
|
||||
public String description;
|
||||
|
||||
/**
|
||||
* Frequency numerator. If a habit is performed 3 times in 7 days, this field equals 3.
|
||||
*/
|
||||
@Column(name = "freq_num")
|
||||
public Integer freqNum;
|
||||
|
||||
/**
|
||||
* Frequency denominator. If a habit is performed 3 times in 7 days, this field equals 7.
|
||||
*/
|
||||
@Column(name = "freq_den")
|
||||
public Integer freqDen;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Position of the habit. Habits are usually sorted by this field.
|
||||
*/
|
||||
@Column(name = "position")
|
||||
public Integer position;
|
||||
|
||||
/**
|
||||
* 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 Long id;
|
||||
|
||||
@NonNull
|
||||
private String name;
|
||||
|
||||
@NonNull
|
||||
private String description;
|
||||
|
||||
@NonNull
|
||||
private Integer freqNum;
|
||||
|
||||
@NonNull
|
||||
private Integer freqDen;
|
||||
|
||||
@NonNull
|
||||
private Integer color;
|
||||
|
||||
/**
|
||||
* 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 reminderHour;
|
||||
|
||||
@Nullable
|
||||
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;
|
||||
|
||||
/**
|
||||
* Flag that indicates whether the habit is archived. Archived habits are usually omitted from
|
||||
* listings, unless explicitly included.
|
||||
*/
|
||||
@Column(name = "archived")
|
||||
public Integer archived;
|
||||
|
||||
/**
|
||||
* List of streaks belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public StreakList streaks;
|
||||
private Integer highlight;
|
||||
|
||||
/**
|
||||
* List of scores belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public ScoreList scores;
|
||||
private Integer archived;
|
||||
|
||||
/**
|
||||
* List of repetitions belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public RepetitionList repetitions;
|
||||
private StreakList streaks;
|
||||
|
||||
/**
|
||||
* List of checkmarks belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public CheckmarkList checkmarks;
|
||||
private ScoreList scores;
|
||||
|
||||
public ModelObservable observable = new ModelObservable();
|
||||
@NonNull
|
||||
private RepetitionList repetitions;
|
||||
|
||||
@NonNull
|
||||
private CheckmarkList checkmarks;
|
||||
|
||||
private ModelObservable observable = new ModelObservable();
|
||||
|
||||
@Inject
|
||||
ModelFactory factory;
|
||||
|
||||
/**
|
||||
* Constructs a habit with the same attributes as the specified habit.
|
||||
@@ -156,328 +96,44 @@ 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
|
||||
*/
|
||||
@Nullable
|
||||
public static Habit get(long id)
|
||||
{
|
||||
return Habit.load(Habit.class, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static List<Habit> getAll(boolean includeArchive)
|
||||
{
|
||||
if(includeArchive) return selectWithArchived().execute();
|
||||
else return select().execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static Habit getByPosition(int position)
|
||||
{
|
||||
return selectWithArchived().where("position = ?", position).executeSingle();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected static From select()
|
||||
{
|
||||
return new Select().from(Habit.class).where("archived = 0").orderBy("position");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected static From selectWithArchived()
|
||||
{
|
||||
return new Select().from(Habit.class).orderBy("position");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of unarchived habits.
|
||||
*
|
||||
* @return number of unarchived habits
|
||||
*/
|
||||
public static int count()
|
||||
{
|
||||
return select().count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of habits, including archived habits.
|
||||
*
|
||||
* @return number of habits, including archived
|
||||
*/
|
||||
public static int countWithArchived()
|
||||
{
|
||||
return selectWithArchived().count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
return select().where("reminder_hour is not null").execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static void reorder(Habit from, Habit to)
|
||||
{
|
||||
if(from == to) return;
|
||||
|
||||
if (to.position < from.position)
|
||||
{
|
||||
new Update(Habit.class).set("position = position + 1")
|
||||
.where("position >= ? and position < ?", to.position, from.position)
|
||||
.execute();
|
||||
}
|
||||
else
|
||||
{
|
||||
new Update(Habit.class).set("position = position - 1")
|
||||
.where("position > ? and position <= ?", from.position, to.position)
|
||||
.execute();
|
||||
}
|
||||
|
||||
from.position = to.position;
|
||||
from.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recomputes the position for every habit in the database. It should never be necessary
|
||||
* to call this method.
|
||||
*/
|
||||
public static void rebuildOrder()
|
||||
{
|
||||
List<Habit> habits = selectWithArchived().execute();
|
||||
|
||||
ActiveAndroid.beginTransaction();
|
||||
try
|
||||
{
|
||||
int i = 0;
|
||||
for (Habit h : habits)
|
||||
{
|
||||
h.position = i++;
|
||||
h.save();
|
||||
}
|
||||
|
||||
ActiveAndroid.setTransactionSuccessful();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveAndroid.endTransaction();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies all the attributes of the specified habit into this habit
|
||||
*
|
||||
* @param model the model whose attributes should be copied from
|
||||
*/
|
||||
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;
|
||||
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
Habit.updateId(getId(), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the habit and all data associated to it, including checkmarks, repetitions and
|
||||
* scores.
|
||||
*/
|
||||
public void cascadeDelete()
|
||||
{
|
||||
Long id = getId();
|
||||
|
||||
ActiveAndroid.beginTransaction();
|
||||
try
|
||||
{
|
||||
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();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveAndroid.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public URI that identifies this habit
|
||||
* @return the uri
|
||||
*/
|
||||
public Uri getUri()
|
||||
{
|
||||
String s = String.format(Locale.US, "content://org.isoron.uhabits/habit/%d", getId());
|
||||
return Uri.parse(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the habit is archived or not.
|
||||
* @return true if archived
|
||||
*/
|
||||
public boolean isArchived()
|
||||
{
|
||||
return archived != 0;
|
||||
}
|
||||
|
||||
private static void updateAttributes(@NonNull List<Habit> habits, @Nullable Integer color,
|
||||
@Nullable Integer archived)
|
||||
{
|
||||
ActiveAndroid.beginTransaction();
|
||||
|
||||
try
|
||||
{
|
||||
for (Habit h : habits)
|
||||
{
|
||||
if(color != null) h.color = color;
|
||||
if(archived != null) h.archived = archived;
|
||||
h.save();
|
||||
}
|
||||
|
||||
ActiveAndroid.setTransactionSuccessful();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveAndroid.endTransaction();
|
||||
for(Habit h : habits)
|
||||
h.observable.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Archives an entire list of habits
|
||||
*
|
||||
* @param habits the habits to be archived
|
||||
*/
|
||||
public static void archive(@NonNull List<Habit> habits)
|
||||
{
|
||||
updateAttributes(habits, null, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unarchives an entire list of habits
|
||||
*
|
||||
* @param habits the habits to be unarchived
|
||||
*/
|
||||
public static void unarchive(@NonNull List<Habit> habits)
|
||||
{
|
||||
updateAttributes(habits, null, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the color for an entire list of habits.
|
||||
*
|
||||
* @param habits the habits to be modified
|
||||
* @param color the new color to be set
|
||||
*/
|
||||
public static void setColor(@NonNull List<Habit> habits, int color)
|
||||
{
|
||||
updateAttributes(habits, color, null);
|
||||
for(Habit h : habits)
|
||||
h.observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the habit has a reminder set.
|
||||
*
|
||||
* @return true if habit has reminder
|
||||
*/
|
||||
public boolean hasReminder()
|
||||
{
|
||||
return (reminderHour != null && reminderMin != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the reminder for a habit. This sets all the related fields to null.
|
||||
* Clears the reminder for a habit. This sets all the related fields to
|
||||
* null.
|
||||
*/
|
||||
public void clearReminder()
|
||||
{
|
||||
@@ -488,36 +144,269 @@ public class Habit extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* Copies all the attributes of the specified habit into this habit
|
||||
*
|
||||
* @param habits the list of habits to write
|
||||
* @param out the writer that will receive the result
|
||||
* @throws IOException if write operations fail
|
||||
* @param model the model whose attributes should be copied from
|
||||
*/
|
||||
public static void writeCSV(List<Habit> habits, Writer out) throws IOException
|
||||
public void copyFrom(@NonNull Habit model)
|
||||
{
|
||||
String header[] = { "Position", "Name", "Description", "NumRepetitions", "Interval", "Color" };
|
||||
|
||||
CSVWriter csv = new CSVWriter(out);
|
||||
csv.writeNext(header, false);
|
||||
|
||||
for(Habit habit : habits)
|
||||
{
|
||||
String[] cols =
|
||||
{
|
||||
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.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();
|
||||
}
|
||||
|
||||
csv.close();
|
||||
/**
|
||||
* Flag that indicates whether the habit is archived. Archived habits are
|
||||
* usually omitted from listings, unless explicitly included.
|
||||
*/
|
||||
public Integer getArchived()
|
||||
{
|
||||
return archived;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of checkmarks belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public CheckmarkList getCheckmarks()
|
||||
{
|
||||
return checkmarks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Color of the habit.
|
||||
* <p>
|
||||
* This number is not an android.graphics.Color, but an index to the
|
||||
* activity color palette, which changes according to the theme. To convert
|
||||
* this color into an android.graphics.Color, use ColorHelper.getColor(context,
|
||||
* habit.color).
|
||||
*/
|
||||
public Integer getColor()
|
||||
{
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(Integer color)
|
||||
{
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of the habit
|
||||
*/
|
||||
public String getDescription()
|
||||
{
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description)
|
||||
{
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frequency denominator. If a habit is performed 3 times in 7 days, this
|
||||
* field equals 7.
|
||||
*/
|
||||
public Integer getFreqDen()
|
||||
{
|
||||
return freqDen;
|
||||
}
|
||||
|
||||
public void setFreqDen(Integer freqDen)
|
||||
{
|
||||
this.freqDen = freqDen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frequency numerator. If a habit is performed 3 times in 7 days, this
|
||||
* field equals 3.
|
||||
*/
|
||||
public Integer getFreqNum()
|
||||
{
|
||||
return freqNum;
|
||||
}
|
||||
|
||||
public void setFreqNum(Integer freqNum)
|
||||
{
|
||||
this.freqNum = freqNum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Not currently used.
|
||||
*/
|
||||
public Integer getHighlight()
|
||||
{
|
||||
return highlight;
|
||||
}
|
||||
|
||||
public void setHighlight(Integer highlight)
|
||||
{
|
||||
this.highlight = highlight;
|
||||
}
|
||||
|
||||
public Long getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of the habit
|
||||
*/
|
||||
public String getName()
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
return observable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public Integer getReminderDays()
|
||||
{
|
||||
return reminderDays;
|
||||
}
|
||||
|
||||
public void setReminderDays(@NonNull Integer reminderDays)
|
||||
{
|
||||
this.reminderDays = reminderDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hour of the day the reminder should be shown. If there is no reminder,
|
||||
* this equals to null.
|
||||
*/
|
||||
@Nullable
|
||||
public Integer getReminderHour()
|
||||
{
|
||||
return reminderHour;
|
||||
}
|
||||
|
||||
public void setReminderHour(@Nullable Integer reminderHour)
|
||||
{
|
||||
this.reminderHour = reminderHour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minute the reminder should be shown. If there is no reminder, this equals
|
||||
* to null.
|
||||
*/
|
||||
@Nullable
|
||||
public Integer getReminderMin()
|
||||
{
|
||||
return reminderMin;
|
||||
}
|
||||
|
||||
public void setReminderMin(@Nullable Integer reminderMin)
|
||||
{
|
||||
this.reminderMin = reminderMin;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of repetitions belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public RepetitionList getRepetitions()
|
||||
{
|
||||
return repetitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of scores belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public ScoreList getScores()
|
||||
{
|
||||
return scores;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of streaks belonging to this habit.
|
||||
*/
|
||||
@NonNull
|
||||
public StreakList getStreaks()
|
||||
{
|
||||
return streaks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public URI that identifies this habit
|
||||
*
|
||||
* @return the uri
|
||||
*/
|
||||
public Uri getUri()
|
||||
{
|
||||
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, false otherwise
|
||||
*/
|
||||
public boolean hasReminder()
|
||||
{
|
||||
return (reminderHour != null && reminderMin != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the habit is archived or not.
|
||||
*
|
||||
* @return true if archived
|
||||
*/
|
||||
public boolean isArchived()
|
||||
{
|
||||
return archived != 0;
|
||||
}
|
||||
|
||||
public void setArchived(Integer archived)
|
||||
{
|
||||
this.archived = archived;
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
235
app/src/main/java/org/isoron/uhabits/models/HabitList.java
Normal file
235
app/src/main/java/org/isoron/uhabits/models/HabitList.java
Normal file
@@ -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
|
||||
{
|
||||
void onModelChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given listener to the observable.
|
||||
*
|
||||
* @param l the listener to be added.
|
||||
*/
|
||||
public void addListener(Listener l)
|
||||
{
|
||||
listeners.add(l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies every listener that the model has changed.
|
||||
* <p>
|
||||
* Only models should call this method.
|
||||
*/
|
||||
public void notifyListeners()
|
||||
{
|
||||
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
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
|
||||
/**
|
||||
* Represents a record that the user has performed a certain habit at a certain
|
||||
* date.
|
||||
*/
|
||||
public class Repetition
|
||||
{
|
||||
/**
|
||||
* Habit to which this repetition belong.
|
||||
*/
|
||||
@Column(name = "habit")
|
||||
public Habit habit;
|
||||
@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();
|
||||
protected final Habit habit;
|
||||
|
||||
@NonNull
|
||||
protected final ModelObservable observable;
|
||||
|
||||
public RepetitionList(@NonNull Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected From select()
|
||||
{
|
||||
return new Select().from(Repetition.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp <= ?", DateUtils.getStartOfToday())
|
||||
.orderBy("timestamp");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected From selectFromTo(long timeFrom, long timeTo)
|
||||
{
|
||||
return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo);
|
||||
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.
|
||||
*
|
||||
* @param timestamp the timestamp of the repetition to toggle
|
||||
* 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 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
|
||||
/**
|
||||
* Represents how strong a habit is at a certain date.
|
||||
*/
|
||||
public class Score
|
||||
{
|
||||
/**
|
||||
* Minimum score value required to earn half a star.
|
||||
* Habit to which this score belongs to.
|
||||
*/
|
||||
public static final int HALF_STAR_CUTOFF = 9629750;
|
||||
private Habit habit;
|
||||
|
||||
/**
|
||||
* Minimum score value required to earn 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_CUTOFF = 15407600;
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* Value of the score.
|
||||
*/
|
||||
private Integer value;
|
||||
|
||||
/**
|
||||
* Maximum score value attainable by any habit.
|
||||
*/
|
||||
public static final int MAX_VALUE = 19259478;
|
||||
|
||||
/**
|
||||
* Status indicating that the habit has not earned any star.
|
||||
*/
|
||||
public static final int EMPTY_STAR = 0;
|
||||
public Score(Habit habit, Long timestamp, Integer value)
|
||||
{
|
||||
this.habit = habit;
|
||||
this.timestamp = timestamp;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status indicating that the habit has earned half a star.
|
||||
*/
|
||||
public static final int HALF_STAR = 1;
|
||||
|
||||
/**
|
||||
* Status indicating that the habit has earned a full star.
|
||||
*/
|
||||
public static final int FULL_STAR = 2;
|
||||
|
||||
/**
|
||||
* Habit to which this score belongs to.
|
||||
*/
|
||||
@Column(name = "habit")
|
||||
public Habit 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;
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
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 habit;
|
||||
}
|
||||
|
||||
public Long getTimestamp()
|
||||
{
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Integer getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return new ToStringBuilder(this)
|
||||
.append("timestamp", timestamp)
|
||||
.append("value", value)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,215 +21,59 @@ 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.
|
||||
*
|
||||
* @param timestamp the oldest timestamp that should be invalidated
|
||||
*/
|
||||
public void invalidateNewerThan(long timestamp)
|
||||
{
|
||||
new Delete().from(Score.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp >= ?", timestamp)
|
||||
.execute();
|
||||
|
||||
observable.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and saves the scores that are missing since the first repetition of the habit.
|
||||
*/
|
||||
private void computeAll()
|
||||
{
|
||||
long fromTimestamp = habit.repetitions.getOldestTimestamp();
|
||||
if(fromTimestamp == 0) return;
|
||||
|
||||
long toTimestamp = DateUtils.getStartOfToday();
|
||||
compute(fromTimestamp, toTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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 from timestamp of the beginning of the interval
|
||||
* @param to timestamp of the end of the time interval
|
||||
*/
|
||||
protected void compute(long from, long to)
|
||||
{
|
||||
InterfaceUtils.throwIfMainThread();
|
||||
|
||||
final long day = DateUtils.millisecondsInOneDay;
|
||||
final double freq = ((double) habit.freqNum) / habit.freqDen;
|
||||
|
||||
int newestScoreValue = findNewestValue();
|
||||
long newestTimestamp = findNewestTimestamp();
|
||||
|
||||
if(newestTimestamp > 0)
|
||||
from = newestTimestamp + day;
|
||||
|
||||
final int checkmarkValues[] = habit.checkmarks.getValues(from, to);
|
||||
final long beginning = from;
|
||||
|
||||
int lastScore = newestScoreValue;
|
||||
int size = checkmarkValues.length;
|
||||
|
||||
long timestamps[] = new long[size];
|
||||
long values[] = new long[size];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
insert(timestamps, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the most recent score that was already computed. If no score has been
|
||||
* computed yet, returns zero.
|
||||
*
|
||||
* @return value of newest score, or zero if none exist
|
||||
*/
|
||||
protected int findNewestValue()
|
||||
{
|
||||
String args[] = { habit.getId().toString() };
|
||||
String query = "select score from Score where habit = ? order by timestamp desc limit 1";
|
||||
return SQLiteUtils.intQuery(query, args);
|
||||
}
|
||||
|
||||
private long findNewestTimestamp()
|
||||
{
|
||||
String args[] = { habit.getId().toString() };
|
||||
String query = "select timestamp from Score where habit = ? order by timestamp desc limit 1";
|
||||
return DatabaseUtils.longQuery(query, args);
|
||||
}
|
||||
|
||||
private void insert(long timestamps[], long values[])
|
||||
{
|
||||
String query = "insert into Score(habit, timestamp, score) 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 the score for a certain day.
|
||||
*
|
||||
* @param timestamp the timestamp for the day
|
||||
* @return the score for the day
|
||||
*/
|
||||
@Nullable
|
||||
protected Score get(long timestamp)
|
||||
{
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if(oldestRep == null) return null;
|
||||
|
||||
compute(oldestRep.timestamp, timestamp);
|
||||
|
||||
return select().where("timestamp = ?", timestamp).executeSingle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the score for a given day.
|
||||
*
|
||||
* @param timestamp the timestamp of a day
|
||||
* @return score for that 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 divisor the size of the groups
|
||||
* @return array of values, with one entry for each group of days
|
||||
@@ -237,64 +81,17 @@ public class ScoreList
|
||||
@NonNull
|
||||
public int[] getAllValues(long divisor)
|
||||
{
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if(oldestRep == null) return new int[0];
|
||||
Repetition oldestRep = habit.getRepetitions().getOldest();
|
||||
if (oldestRep == null) return new int[0];
|
||||
|
||||
long fromTimestamp = oldestRep.timestamp;
|
||||
long fromTimestamp = oldestRep.getTimestamp();
|
||||
long toTimestamp = DateUtils.getStartOfToday();
|
||||
return getValues(fromTimestamp, toTimestamp, divisor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as getAllValues(long), but using a specified interval.
|
||||
*
|
||||
* @param from beginning of the interval (included)
|
||||
* @param to end of the interval (included)
|
||||
* @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)
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
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());
|
||||
return observable;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,17 +105,21 @@ public class ScoreList
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the star status for today. The returned value is either Score.EMPTY_STAR,
|
||||
* Score.HALF_STAR or Score.FULL_STAR.
|
||||
* Returns the value of the score for a given day.
|
||||
*
|
||||
* @return star status for today
|
||||
* @param timestamp the timestamp of a day
|
||||
* @return score for that day
|
||||
*/
|
||||
public int getTodayStarStatus()
|
||||
{
|
||||
Score score = getToday();
|
||||
if(score != null) return score.getStarStatus();
|
||||
else return Score.EMPTY_STAR;
|
||||
}
|
||||
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 timestamp the oldest timestamp that should be invalidated
|
||||
*/
|
||||
public abstract void invalidateNewerThan(long timestamp);
|
||||
|
||||
public void writeCSV(Writer out) throws IOException
|
||||
{
|
||||
@@ -326,23 +127,116 @@ public class ScoreList
|
||||
|
||||
SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat();
|
||||
|
||||
String query = "select timestamp, score from score where habit = ? order by timestamp";
|
||||
String params[] = { habit.getId().toString() };
|
||||
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;
|
||||
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);
|
||||
String score = String.format("%.4f",
|
||||
((float) cursor.getInt(1)) / Score.MAX_VALUE);
|
||||
out.write(String.format("%s,%s\n", timestamp, score));
|
||||
|
||||
} while(cursor.moveToNext());
|
||||
} while (cursor.moveToNext());
|
||||
|
||||
cursor.close();
|
||||
out.close();
|
||||
}
|
||||
|
||||
protected abstract void add(List<Score> scores);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param from timestamp of the beginning of the interval
|
||||
* @param to timestamp of the end of the time interval
|
||||
*/
|
||||
protected void compute(long from, long to)
|
||||
{
|
||||
final long day = DateUtils.millisecondsInOneDay;
|
||||
final double freq = ((double) habit.getFreqNum()) / habit.getFreqDen();
|
||||
|
||||
int newestValue = 0;
|
||||
long newestTimestamp = 0;
|
||||
|
||||
Score newest = getNewestComputed();
|
||||
if(newest != null)
|
||||
{
|
||||
newestValue = newest.getValue();
|
||||
newestTimestamp = newest.getTimestamp();
|
||||
}
|
||||
|
||||
if (newestTimestamp > 0) from = newestTimestamp + day;
|
||||
|
||||
final int checkmarkValues[] = habit.getCheckmarks().getValues(from, to);
|
||||
final long beginning = from;
|
||||
|
||||
int lastScore = newestValue;
|
||||
List<Score> scores = new LinkedList<>();
|
||||
|
||||
for (int i = 0; i < checkmarkValues.length; i++)
|
||||
{
|
||||
int value = checkmarkValues[checkmarkValues.length - i - 1];
|
||||
lastScore = Score.compute(freq, lastScore, value);
|
||||
scores.add(new Score(habit, beginning + day * i, lastScore));
|
||||
}
|
||||
|
||||
add(scores);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and saves the scores that are missing since the first repetition
|
||||
* of the habit.
|
||||
*/
|
||||
protected void computeAll()
|
||||
{
|
||||
Repetition oldestRep = habit.getRepetitions().getOldest();
|
||||
if (oldestRep == null) return;
|
||||
|
||||
long toTimestamp = DateUtils.getStartOfToday();
|
||||
compute(oldestRep.getTimestamp(), toTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the score for a certain day.
|
||||
*
|
||||
* @param timestamp the timestamp for the day
|
||||
* @return the score for the day
|
||||
*/
|
||||
protected abstract Score get(long timestamp);
|
||||
|
||||
/**
|
||||
* Returns the most recent score that was already computed.
|
||||
* <p>
|
||||
* If no score has been computed yet, returns null.
|
||||
*
|
||||
* @return the newest score computed, or null if none exist
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Score getNewestComputed();
|
||||
|
||||
/**
|
||||
* Same as getAllValues(long), but using a specified interval.
|
||||
*
|
||||
* @param from beginning of the interval (included)
|
||||
* @param to end of the interval (included)
|
||||
* @param divisor size of the groups
|
||||
* @return array of values, with one entry for each group of days
|
||||
*/
|
||||
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;
|
||||
|
||||
public StreakList(Habit habit)
|
||||
protected ModelObservable observable;
|
||||
|
||||
protected StreakList(Habit habit)
|
||||
{
|
||||
this.habit = habit;
|
||||
observable = new ModelObservable();
|
||||
}
|
||||
|
||||
public List<Streak> getAll(int limit)
|
||||
public abstract List<Streak> getAll();
|
||||
|
||||
public List<Streak> getBest(int limit)
|
||||
{
|
||||
rebuild();
|
||||
|
||||
String query = "select * from (select * from streak where habit=? " +
|
||||
"order by end <> ?, length desc, end desc limit ?) order by end desc";
|
||||
|
||||
String params[] = {habit.getId().toString(), Long.toString(DateUtils.getStartOfToday()),
|
||||
Integer.toString(limit)};
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
Cursor cursor = db.rawQuery(query, params);
|
||||
|
||||
if(!cursor.moveToFirst())
|
||||
{
|
||||
cursor.close();
|
||||
return new LinkedList<>();
|
||||
}
|
||||
|
||||
List<Streak> streaks = new LinkedList<>();
|
||||
|
||||
do
|
||||
{
|
||||
Streak s = Streak.load(Streak.class, cursor.getInt(0));
|
||||
streaks.add(s);
|
||||
}
|
||||
while (cursor.moveToNext());
|
||||
|
||||
cursor.close();
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
public Streak getNewest()
|
||||
public abstract Streak getNewestComputed();
|
||||
|
||||
public ModelObservable getObservable()
|
||||
{
|
||||
return new Select().from(Streak.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.orderBy("end desc")
|
||||
.limit(1)
|
||||
.executeSingle();
|
||||
return observable;
|
||||
}
|
||||
|
||||
public abstract void invalidateNewerThan(long timestamp);
|
||||
|
||||
public void rebuild()
|
||||
{
|
||||
InterfaceUtils.throwIfMainThread();
|
||||
|
||||
long beginning;
|
||||
long today = DateUtils.getStartOfToday();
|
||||
|
||||
Long beginning = findBeginning();
|
||||
if (beginning == null || beginning > today) return;
|
||||
|
||||
int checks[] = habit.getCheckmarks().getValues(beginning, today);
|
||||
List<Streak> streaks = checkmarksToStreaks(beginning, checks);
|
||||
|
||||
removeNewestComputed();
|
||||
insert(streaks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a list of checkmark values to a list of streaks.
|
||||
*
|
||||
* @param beginning the timestamp corresponding to the first checkmark
|
||||
* value.
|
||||
* @param checks the checkmarks values, ordered by decreasing timestamp.
|
||||
* @return the list of streaks.
|
||||
*/
|
||||
@NonNull
|
||||
protected List<Streak> checkmarksToStreaks(Long beginning, int[] checks)
|
||||
{
|
||||
ArrayList<Long> transitions = getTransitions(beginning, checks);
|
||||
|
||||
List<Streak> streaks = new LinkedList<>();
|
||||
for (int i = 0; i < transitions.size(); i += 2)
|
||||
{
|
||||
long start = transitions.get(i);
|
||||
long end = transitions.get(i + 1);
|
||||
streaks.add(new Streak(habit, start, end));
|
||||
}
|
||||
|
||||
return streaks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the place where we should start when recomputing the streaks.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
protected Long findBeginning()
|
||||
{
|
||||
Streak newestStreak = getNewestComputed();
|
||||
if (newestStreak != null) return newestStreak.getStart();
|
||||
|
||||
Repetition oldestRep = habit.getRepetitions().getOldest();
|
||||
if (oldestRep != null) return oldestRep.getTimestamp();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamps where there was a transition from performing a
|
||||
* habit to not performing a habit, and vice-versa.
|
||||
*
|
||||
* @param beginning the timestamp for the first checkmark
|
||||
* @param checks the checkmarks, ordered by decresing timestamp
|
||||
* @return the list of transitions
|
||||
*/
|
||||
@NonNull
|
||||
protected ArrayList<Long> getTransitions(Long beginning, int[] checks)
|
||||
{
|
||||
long day = DateUtils.millisecondsInOneDay;
|
||||
|
||||
Streak newestStreak = getNewest();
|
||||
if (newestStreak != null)
|
||||
{
|
||||
beginning = newestStreak.start;
|
||||
}
|
||||
else
|
||||
{
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if (oldestRep == null) return;
|
||||
|
||||
beginning = oldestRep.timestamp;
|
||||
}
|
||||
|
||||
if (beginning > today) return;
|
||||
|
||||
int checks[] = habit.checkmarks.getValues(beginning, today);
|
||||
ArrayList<Long> list = new ArrayList<>();
|
||||
|
||||
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();
|
||||
return list;
|
||||
}
|
||||
|
||||
ActiveAndroid.setTransactionSuccessful();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveAndroid.endTransaction();
|
||||
}
|
||||
}
|
||||
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;
|
||||
23
app/src/main/java/org/isoron/uhabits/package-info.java
Normal file
23
app/src/main/java/org/isoron/uhabits/package-info.java
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides classes for the Loop Habit Tracker app.
|
||||
*/
|
||||
package org.isoron.uhabits;
|
||||
24
app/src/main/java/org/isoron/uhabits/tasks/package-info.java
Normal file
24
app/src/main/java/org/isoron/uhabits/tasks/package-info.java
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public 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
Reference in New Issue
Block a user