mirror of
https://github.com/iSoron/uhabits.git
synced 2025-12-06 09:08:52 -06:00
Merge branch 'feature/unit-tests' into dev
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
<a href="https://circleci.com/gh/iSoron/uhabits/tree/dev">
|
||||
<img src="https://circleci.com/gh/iSoron/uhabits/tree/dev.svg?style=shield" align="right">
|
||||
</a>
|
||||
|
||||
# Loop Habit Tracker
|
||||
|
||||
Loop is a simple Android app that helps you create and maintain good habits,
|
||||
|
||||
@@ -2,13 +2,16 @@ apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "21.1.2"
|
||||
buildToolsVersion "23.0.1"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.isoron.uhabits"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 23
|
||||
|
||||
buildConfigField "Integer", "databaseVersion", "12"
|
||||
buildConfigField "String", "databaseFilename", "\"uhabits.db\""
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -40,3 +43,13 @@ dependencies {
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1'
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits;
|
||||
package org.isoron.uhabits.ui;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.Adapter;
|
||||
@@ -17,7 +17,7 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits;
|
||||
package org.isoron.uhabits.ui;
|
||||
|
||||
import android.support.test.espresso.UiController;
|
||||
import android.support.test.espresso.ViewAction;
|
||||
@@ -32,6 +32,7 @@ import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.Random;
|
||||
@@ -17,11 +17,11 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits;
|
||||
package org.isoron.uhabits.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.espresso.matcher.ViewMatchers;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -45,8 +45,8 @@ import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.isoron.uhabits.HabitMatchers.containsHabit;
|
||||
import static org.isoron.uhabits.HabitMatchers.withName;
|
||||
import static org.isoron.uhabits.ui.HabitMatchers.containsHabit;
|
||||
import static org.isoron.uhabits.ui.HabitMatchers.withName;
|
||||
|
||||
public class MainActivityActions
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.isoron.uhabits;
|
||||
package org.isoron.uhabits.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
@@ -7,6 +7,8 @@ import android.support.test.espresso.intent.rule.IntentsTestRule;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.test.suitebuilder.annotation.LargeTest;
|
||||
|
||||
import org.isoron.uhabits.MainActivity;
|
||||
import org.isoron.uhabits.R;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -37,20 +39,20 @@ import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.endsWith;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.isoron.uhabits.HabitMatchers.withName;
|
||||
import static org.isoron.uhabits.HabitViewActions.clickAtRandomLocations;
|
||||
import static org.isoron.uhabits.HabitViewActions.toggleAllCheckmarks;
|
||||
import static org.isoron.uhabits.MainActivityActions.addHabit;
|
||||
import static org.isoron.uhabits.MainActivityActions.assertHabitExists;
|
||||
import static org.isoron.uhabits.MainActivityActions.assertHabitsDontExist;
|
||||
import static org.isoron.uhabits.MainActivityActions.assertHabitsExist;
|
||||
import static org.isoron.uhabits.MainActivityActions.clickActionModeMenuItem;
|
||||
import static org.isoron.uhabits.MainActivityActions.deleteHabit;
|
||||
import static org.isoron.uhabits.MainActivityActions.deleteHabits;
|
||||
import static org.isoron.uhabits.MainActivityActions.selectHabit;
|
||||
import static org.isoron.uhabits.MainActivityActions.selectHabits;
|
||||
import static org.isoron.uhabits.MainActivityActions.typeHabitData;
|
||||
import static org.isoron.uhabits.ShowHabitActivityActions.openHistoryEditor;
|
||||
import static org.isoron.uhabits.ui.HabitMatchers.withName;
|
||||
import static org.isoron.uhabits.ui.HabitViewActions.clickAtRandomLocations;
|
||||
import static org.isoron.uhabits.ui.HabitViewActions.toggleAllCheckmarks;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.addHabit;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.assertHabitExists;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.assertHabitsDontExist;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.assertHabitsExist;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.clickActionModeMenuItem;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.deleteHabit;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.deleteHabits;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.selectHabit;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.selectHabits;
|
||||
import static org.isoron.uhabits.ui.MainActivityActions.typeHabitData;
|
||||
import static org.isoron.uhabits.ui.ShowHabitActivityActions.openHistoryEditor;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
@@ -61,6 +63,16 @@ public class MainTest
|
||||
MainActivity.class);
|
||||
|
||||
@Before
|
||||
public void setup()
|
||||
{
|
||||
Context context = InstrumentationRegistry.getInstrumentation().getContext();
|
||||
SystemHelper sys = new SystemHelper(context);
|
||||
sys.disableAllAnimations();
|
||||
sys.unlockScreen();
|
||||
|
||||
skipTutorial();
|
||||
}
|
||||
|
||||
public void skipTutorial()
|
||||
{
|
||||
try
|
||||
@@ -118,7 +130,7 @@ public class MainTest
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddHabitAndViewStats()
|
||||
public void testAddHabitAndViewStats() throws InterruptedException
|
||||
{
|
||||
String name = addHabit(true);
|
||||
|
||||
@@ -126,6 +138,8 @@ public class MainTest
|
||||
.onChildView(withId(R.id.llButtons))
|
||||
.perform(toggleAllCheckmarks());
|
||||
|
||||
Thread.sleep(1200);
|
||||
|
||||
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
|
||||
.onChildView(withId(R.id.label))
|
||||
.perform(click());
|
||||
@@ -17,18 +17,21 @@
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits;
|
||||
package org.isoron.uhabits.ui;
|
||||
|
||||
import android.support.test.espresso.matcher.ViewMatchers;
|
||||
|
||||
import org.isoron.uhabits.R;
|
||||
|
||||
import static android.support.test.espresso.Espresso.onView;
|
||||
import static android.support.test.espresso.action.ViewActions.click;
|
||||
import static android.support.test.espresso.action.ViewActions.scrollTo;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.withId;
|
||||
|
||||
public class ShowHabitActivityActions
|
||||
{
|
||||
public static void openHistoryEditor()
|
||||
{
|
||||
onView(withId(R.id.btEditHistory))
|
||||
onView(ViewMatchers.withId(R.id.btEditHistory))
|
||||
.perform(scrollTo(), click());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.isoron.uhabits.ui;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.IBinder;
|
||||
import android.support.test.runner.AndroidJUnitRunner;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public final class SystemHelper extends AndroidJUnitRunner
|
||||
{
|
||||
private static final String ANIMATION_PERMISSION = "android.permission.SET_ANIMATION_SCALE";
|
||||
private static final float DISABLED = 0.0f;
|
||||
private static final float DEFAULT = 1.0f;
|
||||
|
||||
private final Context context;
|
||||
|
||||
SystemHelper(Context context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
void unlockScreen()
|
||||
{
|
||||
try
|
||||
{
|
||||
KeyguardManager mKeyGuardManager = (KeyguardManager) context
|
||||
.getSystemService(Context.KEYGUARD_SERVICE);
|
||||
KeyguardManager.KeyguardLock mLock = mKeyGuardManager.newKeyguardLock("lock");
|
||||
mLock.disableKeyguard();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
void disableAllAnimations()
|
||||
{
|
||||
Log.i("SystemAnimations", "Trying to disable animations");
|
||||
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
|
||||
if (permStatus == PackageManager.PERMISSION_GRANTED)
|
||||
setSystemAnimationsScale(DISABLED);
|
||||
else
|
||||
Log.e("SystemAnimations", "Permission denied");
|
||||
|
||||
}
|
||||
|
||||
void enableAllAnimations()
|
||||
{
|
||||
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
|
||||
if (permStatus == PackageManager.PERMISSION_GRANTED)
|
||||
{
|
||||
setSystemAnimationsScale(DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSystemAnimationsScale(float animationScale)
|
||||
{
|
||||
try
|
||||
{
|
||||
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
|
||||
Method asInterface =
|
||||
windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
|
||||
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
|
||||
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
|
||||
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
|
||||
Method setAnimationScales =
|
||||
windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
|
||||
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
|
||||
|
||||
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
|
||||
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
|
||||
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
|
||||
for (int i = 0; i < currentScales.length; i++)
|
||||
currentScales[i] = animationScale;
|
||||
|
||||
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
|
||||
Log.i("SystemAnimations", "All animations successfully disabled");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.e("SystemAnimations",
|
||||
"Could not change animation scale to " + animationScale + " :'(");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.helpers.DateHelper;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
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
|
||||
{
|
||||
Habit nonDailyHabit;
|
||||
private Habit emptyHabit;
|
||||
|
||||
@Before
|
||||
public void prepare()
|
||||
{
|
||||
HabitFixtures.purgeHabits();
|
||||
DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME);
|
||||
nonDailyHabit = HabitFixtures.createNonDailyHabit();
|
||||
emptyHabit = HabitFixtures.createEmptyHabit();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown()
|
||||
{
|
||||
DateHelper.setFixedLocalTime(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAllValues_testNonDailyHabit()
|
||||
{
|
||||
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 getAllValues_testMoveForwardInTime()
|
||||
{
|
||||
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 getAllValues_testMoveBackwardsInTime()
|
||||
{
|
||||
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 getAllValues_testEmptyHabit()
|
||||
{
|
||||
int[] expectedValues = new int[0];
|
||||
int[] actualValues = emptyHabit.checkmarks.getAllValues();
|
||||
|
||||
assertThat(actualValues, equalTo(expectedValues));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getValues_testInvalidInterval()
|
||||
{
|
||||
int values[] = nonDailyHabit.checkmarks.getValues(100L, -100L);
|
||||
assertThat(values, equalTo(new int[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getValues_testValidInterval()
|
||||
{
|
||||
long from = DateHelper.getStartOfToday() - 15 * DateHelper.millisecondsInOneDay;
|
||||
long to = DateHelper.getStartOfToday() - 5 * DateHelper.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 getTodayValue_testNonDailyHabit()
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
private void travelInTime(int days)
|
||||
{
|
||||
DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME +
|
||||
days * DateHelper.millisecondsInOneDay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 org.isoron.helpers.DateHelper;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
public class HabitFixtures
|
||||
{
|
||||
public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC)
|
||||
public static boolean NON_DAILY_HABIT_CHECKS[] = { true, false, false, true, true, true, false,
|
||||
false, true, true };
|
||||
|
||||
static Habit createNonDailyHabit()
|
||||
{
|
||||
Habit habit = new Habit();
|
||||
habit.freqNum = 2;
|
||||
habit.freqDen = 3;
|
||||
habit.save();
|
||||
|
||||
long timestamp = DateHelper.getStartOfToday();
|
||||
for(boolean c : NON_DAILY_HABIT_CHECKS)
|
||||
{
|
||||
if(c) habit.repetitions.toggle(timestamp);
|
||||
timestamp -= DateHelper.millisecondsInOneDay;
|
||||
}
|
||||
|
||||
return habit;
|
||||
}
|
||||
|
||||
static Habit createEmptyHabit()
|
||||
{
|
||||
Habit habit = new Habit();
|
||||
habit.save();
|
||||
return habit;
|
||||
}
|
||||
|
||||
static void purgeHabits()
|
||||
{
|
||||
for(Habit h : Habit.getAll(true))
|
||||
h.cascadeDelete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.models.Habit;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class HabitTest
|
||||
{
|
||||
@Before
|
||||
public void prepare()
|
||||
{
|
||||
HabitFixtures.purgeHabits();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reorderTest()
|
||||
{
|
||||
List<Long> ids = new LinkedList<>();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Habit h = new Habit();
|
||||
h.save();
|
||||
ids.add(h.getId());
|
||||
assertThat(h.position, is(i));
|
||||
}
|
||||
|
||||
int from = 5, to = 2;
|
||||
int expectedPosition[] = {0, 1, 3, 4, 5, 2, 6, 7, 8, 9};
|
||||
|
||||
Habit fromHabit = Habit.get(ids.get(from));
|
||||
Habit toHabit = Habit.get(ids.get(to));
|
||||
Habit.reorder(fromHabit, toHabit);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
Habit h = Habit.get(ids.get(i));
|
||||
assertThat(h.position, is(expectedPosition[i]));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rebuildOrderTest()
|
||||
{
|
||||
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));
|
||||
assertThat(h.position, is(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* 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.helpers.DateHelper;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
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 org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class RepetitionListTest
|
||||
{
|
||||
Habit habit;
|
||||
private Habit emptyHabit;
|
||||
|
||||
@Before
|
||||
public void prepare()
|
||||
{
|
||||
HabitFixtures.purgeHabits();
|
||||
DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME);
|
||||
habit = HabitFixtures.createNonDailyHabit();
|
||||
emptyHabit = HabitFixtures.createEmptyHabit();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown()
|
||||
{
|
||||
DateHelper.setFixedLocalTime(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contains_testNonDailyHabit()
|
||||
{
|
||||
long current = DateHelper.getStartOfToday();
|
||||
|
||||
for(boolean b : HabitFixtures.NON_DAILY_HABIT_CHECKS)
|
||||
{
|
||||
assertThat(habit.repetitions.contains(current), equalTo(b));
|
||||
current -= DateHelper.millisecondsInOneDay;
|
||||
}
|
||||
|
||||
for(int i = 0; i < 3; i++)
|
||||
{
|
||||
assertThat(habit.repetitions.contains(current), equalTo(false));
|
||||
current -= DateHelper.millisecondsInOneDay;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delete_test()
|
||||
{
|
||||
long timestamp = DateHelper.getStartOfToday();
|
||||
assertThat(habit.repetitions.contains(timestamp), equalTo(true));
|
||||
|
||||
habit.repetitions.delete(timestamp);
|
||||
assertThat(habit.repetitions.contains(timestamp), equalTo(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void toggle_test()
|
||||
{
|
||||
long timestamp = DateHelper.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 getWeekDayFrequency_test()
|
||||
{
|
||||
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 = DateHelper.getStartOfTodayCalendar();
|
||||
|
||||
// Sets the current date to the end of November
|
||||
day.set(2015, 10, 30);
|
||||
DateHelper.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));
|
||||
}
|
||||
}
|
||||
27
app/src/debug/AndroidManifest.xml
Normal file
27
app/src/debug/AndroidManifest.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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/>.
|
||||
-->
|
||||
<manifest
|
||||
package="org.isoron.uhabits"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE"/>
|
||||
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
|
||||
|
||||
</manifest>
|
||||
@@ -35,19 +35,13 @@
|
||||
android:maxSdkVersion="18"/>
|
||||
|
||||
<application
|
||||
android:name="com.activeandroid.app.Application"
|
||||
android:name="HabitsApplication"
|
||||
android:allowBackup="true"
|
||||
android:backupAgent=".HabitsBackupAgent"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/main_activity_title"
|
||||
android:theme="@style/AppBaseTheme">
|
||||
|
||||
<meta-data
|
||||
android:name="AA_DB_NAME"
|
||||
android:value="uhabits.db"/>
|
||||
<meta-data
|
||||
android:name="AA_DB_VERSION"
|
||||
android:value="12"/>
|
||||
<meta-data
|
||||
android:name="com.google.android.backup.api_key"
|
||||
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw"/>
|
||||
|
||||
@@ -32,14 +32,22 @@ import java.util.TimeZone;
|
||||
public class DateHelper
|
||||
{
|
||||
public static int millisecondsInOneDay = 24 * 60 * 60 * 1000;
|
||||
private static Long fixedLocalTime = null;
|
||||
|
||||
public static long getLocalTime()
|
||||
{
|
||||
if(fixedLocalTime != null) return fixedLocalTime;
|
||||
|
||||
TimeZone tz = TimeZone.getDefault();
|
||||
long now = new Date().getTime();
|
||||
return now + tz.getOffset(now);
|
||||
}
|
||||
|
||||
public static void setFixedLocalTime(Long timestamp)
|
||||
{
|
||||
fixedLocalTime = timestamp;
|
||||
}
|
||||
|
||||
public static long toLocalTime(long timestamp)
|
||||
{
|
||||
TimeZone tz = TimeZone.getDefault();
|
||||
@@ -54,9 +62,7 @@ public class DateHelper
|
||||
|
||||
public static GregorianCalendar getStartOfTodayCalendar()
|
||||
{
|
||||
GregorianCalendar day = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
|
||||
day.setTimeInMillis(DateHelper.getStartOfDay(DateHelper.getLocalTime()));
|
||||
return day;
|
||||
return getCalendar(getStartOfToday());
|
||||
}
|
||||
|
||||
public static GregorianCalendar getCalendar(long timestamp)
|
||||
@@ -187,5 +193,4 @@ public class DateHelper
|
||||
|
||||
return weekday;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import android.support.v4.content.LocalBroadcastManager;
|
||||
|
||||
import org.isoron.helpers.DateHelper;
|
||||
import org.isoron.uhabits.helpers.ReminderHelper;
|
||||
import org.isoron.uhabits.models.Checkmark;
|
||||
import org.isoron.uhabits.models.Habit;
|
||||
|
||||
import java.util.Date;
|
||||
@@ -145,7 +146,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
|
||||
Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());
|
||||
Long reminderTime = intent.getLongExtra("reminderTime", DateHelper.getStartOfToday());
|
||||
|
||||
if (habit.repetitions.hasImplicitRepToday()) return;
|
||||
if (habit.checkmarks.getTodayValue() != Checkmark.UNCHECKED) return;
|
||||
|
||||
habit.highlight = 1;
|
||||
habit.save();
|
||||
|
||||
78
app/src/main/java/org/isoron/uhabits/HabitsApplication.java
Normal file
78
app/src/main/java/org/isoron/uhabits/HabitsApplication.java
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import com.activeandroid.ActiveAndroid;
|
||||
import com.activeandroid.Configuration;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class HabitsApplication extends Application
|
||||
{
|
||||
private boolean isTestMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest");
|
||||
return true;
|
||||
}
|
||||
catch (final Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDB(String databaseFilename)
|
||||
{
|
||||
File databaseFile = new File(String.format("%s/../databases/%s",
|
||||
getApplicationContext().getFilesDir().getPath(), databaseFilename));
|
||||
|
||||
if(databaseFile.exists()) databaseFile.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate()
|
||||
{
|
||||
super.onCreate();
|
||||
String databaseFilename = BuildConfig.databaseFilename;
|
||||
|
||||
if (isTestMode())
|
||||
{
|
||||
databaseFilename = "test.db";
|
||||
deleteDB(databaseFilename);
|
||||
}
|
||||
|
||||
Configuration dbConfig = new Configuration.Builder(this)
|
||||
.setDatabaseName(databaseFilename)
|
||||
.setDatabaseVersion(BuildConfig.databaseVersion)
|
||||
.create();
|
||||
|
||||
ActiveAndroid.initialize(dbConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate()
|
||||
{
|
||||
ActiveAndroid.dispose();
|
||||
super.onTerminate();
|
||||
}
|
||||
}
|
||||
@@ -71,8 +71,6 @@ public class ShowHabitFragment extends Fragment
|
||||
activity = (ShowHabitActivity) getActivity();
|
||||
habit = activity.habit;
|
||||
|
||||
habit.checkmarks.rebuild();
|
||||
|
||||
Button btEditHistory = (Button) view.findViewById(R.id.btEditHistory);
|
||||
streakView = (HabitStreakView) view.findViewById(R.id.streakView);
|
||||
scoreView = (HabitScoreView) view.findViewById(R.id.scoreView);
|
||||
|
||||
@@ -26,9 +26,21 @@ import com.activeandroid.annotation.Table;
|
||||
@Table(name = "Checkmarks")
|
||||
public class Checkmark extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@Column(name = "habit")
|
||||
@@ -38,10 +50,9 @@ public class Checkmark extends Model
|
||||
public Long timestamp;
|
||||
|
||||
/**
|
||||
* Indicates whether there is a checkmark at the given timestamp or not, and whether the
|
||||
* checkmark is explicit or implicit. An explicit checkmark indicates that there is a
|
||||
* repetition at that day. An implicit checkmark indicates that there is no repetition at that
|
||||
* day, but a repetition was not needed, due to the frequency of the habit.
|
||||
* 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;
|
||||
|
||||
@@ -40,6 +40,12 @@ public class CheckmarkList
|
||||
this.habit = habit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@@ -48,10 +54,21 @@ public class CheckmarkList
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public int[] getValues(Long fromTimestamp, Long toTimestamp)
|
||||
{
|
||||
rebuild();
|
||||
|
||||
buildCache(fromTimestamp, toTimestamp);
|
||||
if(fromTimestamp > toTimestamp) return new int[0];
|
||||
|
||||
String query = "select value, timestamp from Checkmarks where " +
|
||||
@@ -81,53 +98,59 @@ public class CheckmarkList
|
||||
return checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes and 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.
|
||||
*
|
||||
* @return values for the checkmarks in the interval
|
||||
*/
|
||||
public int[] getAllValues()
|
||||
{
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if(oldestRep == null) return new int[0];
|
||||
|
||||
Long toTimestamp = DateHelper.getStartOfToday();
|
||||
Long fromTimestamp = oldestRep.timestamp;
|
||||
Long toTimestamp = DateHelper.getStartOfToday();
|
||||
|
||||
return getValues(fromTimestamp, toTimestamp);
|
||||
}
|
||||
|
||||
public void rebuild()
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public void buildCache(long from, long to)
|
||||
{
|
||||
long beginning;
|
||||
long today = DateHelper.getStartOfToday();
|
||||
long day = DateHelper.millisecondsInOneDay;
|
||||
|
||||
Checkmark newestCheckmark = getNewest();
|
||||
if (newestCheckmark == null)
|
||||
{
|
||||
Repetition oldestRep = habit.repetitions.getOldest();
|
||||
if (oldestRep == null) return;
|
||||
Checkmark newestCheckmark = findNewest();
|
||||
if(newestCheckmark != null)
|
||||
from = Math.max(from, newestCheckmark.timestamp + day);
|
||||
|
||||
beginning = oldestRep.timestamp;
|
||||
}
|
||||
else
|
||||
{
|
||||
beginning = newestCheckmark.timestamp + day;
|
||||
}
|
||||
if(from > to) return;
|
||||
|
||||
if (beginning > today) return;
|
||||
|
||||
long beginningExtended = beginning - (long) (habit.freqDen) * day;
|
||||
List<Repetition> reps = habit.repetitions.selectFromTo(beginningExtended, today).execute();
|
||||
|
||||
int nDays = (int) ((today - beginning) / day) + 1;
|
||||
int nDaysExtended = (int) ((today - beginningExtended) / day) + 1;
|
||||
long fromExtended = from - (long) (habit.freqDen) * day;
|
||||
List<Repetition> reps = habit.repetitions
|
||||
.selectFromTo(fromExtended, to)
|
||||
.execute();
|
||||
|
||||
int nDays = (int) ((to - from) / day) + 1;
|
||||
int nDaysExtended = (int) ((to - fromExtended) / day) + 1;
|
||||
int checks[] = new int[nDaysExtended];
|
||||
|
||||
// explicit checks
|
||||
for (Repetition rep : reps)
|
||||
{
|
||||
int offset = (int) ((rep.timestamp - beginningExtended) / day);
|
||||
checks[nDaysExtended - offset - 1] = 2;
|
||||
int offset = (int) ((rep.timestamp - fromExtended) / day);
|
||||
checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY;
|
||||
}
|
||||
|
||||
// implicit checks
|
||||
for (int i = 0; i < nDays; i++)
|
||||
{
|
||||
int counter = 0;
|
||||
@@ -135,7 +158,9 @@ public class CheckmarkList
|
||||
for (int j = 0; j < habit.freqDen; j++)
|
||||
if (checks[i + j] == 2) counter++;
|
||||
|
||||
if (counter >= habit.freqNum) checks[i] = Math.max(checks[i], 1);
|
||||
if (counter >= habit.freqNum)
|
||||
if(checks[i] != Checkmark.CHECKED_EXPLICITLY)
|
||||
checks[i] = Checkmark.CHECKED_IMPLICITLY;
|
||||
}
|
||||
|
||||
ActiveAndroid.beginTransaction();
|
||||
@@ -146,33 +171,48 @@ public class CheckmarkList
|
||||
{
|
||||
Checkmark c = new Checkmark();
|
||||
c.habit = habit;
|
||||
c.timestamp = today - i * day;
|
||||
c.timestamp = to - i * day;
|
||||
c.value = checks[i];
|
||||
c.save();
|
||||
}
|
||||
|
||||
ActiveAndroid.setTransactionSuccessful();
|
||||
} finally
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActiveAndroid.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public Checkmark getNewest()
|
||||
/**
|
||||
* Returns newest checkmark that has already been computed. Ignores any checkmark that has
|
||||
* timestamp in the future. This does not update the cache.
|
||||
*/
|
||||
private Checkmark findNewest()
|
||||
{
|
||||
return new Select().from(Checkmark.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp <= ?", DateHelper.getStartOfToday())
|
||||
.orderBy("timestamp desc")
|
||||
.limit(1)
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
public int getCurrentValue()
|
||||
/**
|
||||
* Returns the checkmark for today.
|
||||
*/
|
||||
public Checkmark getToday()
|
||||
{
|
||||
rebuild();
|
||||
Checkmark c = getNewest();
|
||||
long today = DateHelper.getStartOfToday();
|
||||
buildCache(today, today);
|
||||
return findNewest();
|
||||
}
|
||||
|
||||
if(c != null) return c.value;
|
||||
else return 0;
|
||||
/**
|
||||
* Returns the value of today's checkmark.
|
||||
*/
|
||||
public int getTodayValue()
|
||||
{
|
||||
return getToday().value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import com.activeandroid.query.Select;
|
||||
import org.isoron.helpers.DateHelper;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.HashMap;
|
||||
|
||||
@@ -48,6 +47,7 @@ public class RepetitionList
|
||||
{
|
||||
return new Select().from(Repetition.class)
|
||||
.where("habit = ?", habit.getId())
|
||||
.and("timestamp <= ?", DateHelper.getStartOfToday())
|
||||
.orderBy("timestamp");
|
||||
}
|
||||
|
||||
@@ -56,12 +56,23 @@ public class RepetitionList
|
||||
return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there is a repetition at a given timestamp.
|
||||
*
|
||||
* @param timestamp the timestamp to check
|
||||
* @return true if there is a repetition
|
||||
*/
|
||||
public boolean contains(long timestamp)
|
||||
{
|
||||
int count = select().where("timestamp = ?", timestamp).count();
|
||||
return (count > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the repetition at a given timestamp, if it exists.
|
||||
*
|
||||
* @param timestamp the timestamp of the repetition to delete
|
||||
*/
|
||||
public void delete(long timestamp)
|
||||
{
|
||||
new Delete().from(Repetition.class)
|
||||
@@ -70,11 +81,12 @@ public class RepetitionList
|
||||
.execute();
|
||||
}
|
||||
|
||||
public Repetition getOldestNewerThan(long timestamp)
|
||||
{
|
||||
return select().where("timestamp > ?", timestamp).limit(1).executeSingle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the repetition at a certain timestamp. That is, deletes the repetition if it exists
|
||||
* or creates one if it does not.
|
||||
*
|
||||
* @param timestamp the timestamp of the repetition to toggle
|
||||
*/
|
||||
public void toggle(long timestamp)
|
||||
{
|
||||
timestamp = DateHelper.getStartOfDay(timestamp);
|
||||
@@ -96,18 +108,27 @@ public class RepetitionList
|
||||
habit.streaks.deleteNewerThan(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the oldest repetition for the habit. If there is no repetition, returns null.
|
||||
* Repetitions in the future are discarded.
|
||||
*
|
||||
* @return oldest repetition for the habit
|
||||
*/
|
||||
public Repetition getOldest()
|
||||
{
|
||||
return (Repetition) select().limit(1).executeSingle();
|
||||
}
|
||||
|
||||
public boolean hasImplicitRepToday()
|
||||
{
|
||||
long today = DateHelper.getStartOfToday();
|
||||
int reps[] = habit.checkmarks.getValues(today - DateHelper.millisecondsInOneDay, today);
|
||||
return (reps[0] > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return total number of repetitions by month versus day of week
|
||||
*/
|
||||
public HashMap<Long, Integer[]> getWeekdayFrequency()
|
||||
{
|
||||
Repetition oldestRep = getOldest();
|
||||
@@ -117,10 +138,11 @@ public class RepetitionList
|
||||
"strftime('%m', timestamp / 1000, 'unixepoch') as month," +
|
||||
"strftime('%w', timestamp / 1000, 'unixepoch') as weekday, " +
|
||||
"count(*) from repetitions " +
|
||||
"where habit = ? " +
|
||||
"where habit = ? and timestamp <= ? " +
|
||||
"group by year, month, weekday";
|
||||
|
||||
String[] params = { habit.getId().toString() };
|
||||
String[] params = { habit.getId().toString(),
|
||||
Long.toString(DateHelper.getStartOfToday()) };
|
||||
|
||||
SQLiteDatabase db = Cache.openDatabase();
|
||||
Cursor cursor = db.rawQuery(query, params);
|
||||
|
||||
@@ -114,7 +114,7 @@ public class CheckmarkView extends View
|
||||
|
||||
public void setHabit(Habit habit)
|
||||
{
|
||||
this.check_status = habit.checkmarks.getCurrentValue();
|
||||
this.check_status = habit.checkmarks.getTodayValue();
|
||||
this.star_status = habit.scores.getCurrentStarStatus();
|
||||
this.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color), Color.blue(habit.color));
|
||||
this.label = habit.name;
|
||||
|
||||
@@ -4,7 +4,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.5.0'
|
||||
classpath 'com.android.tools.build:gradle:2.1.0-alpha2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
circle.yml
Normal file
15
circle.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
checkout:
|
||||
post:
|
||||
- git submodule sync
|
||||
- git submodule update --init
|
||||
|
||||
test:
|
||||
override:
|
||||
- emulator -avd circleci-android22 -no-audio -no-window:
|
||||
background: true
|
||||
parallel: true
|
||||
- circle-android wait-for-boot
|
||||
- adb shell input keyevent 82
|
||||
- ./gradlew connectedAndroidTest
|
||||
- cp -r app/build/outputs $CIRCLE_ARTIFACTS || echo ok
|
||||
- cp -r app/build/reports/androidTests/connected/* $CIRCLE_TEST_REPORTS || echo ok
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
|
||||
|
||||
Submodule libs/drag-sort-listview updated: 318d69cf6b...54ca667d4c
Reference in New Issue
Block a user