Merge branch 'feature/unit-tests' into dev

pull/69/head
Alinson S. Xavier 10 years ago
commit a0803966f9

@ -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));
}
}

@ -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();

@ -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;
beginning = oldestRep.timestamp;
}
else
{
beginning = newestCheckmark.timestamp + day;
}
if (beginning > today) return;
Checkmark newestCheckmark = findNewest();
if(newestCheckmark != null)
from = Math.max(from, newestCheckmark.timestamp + day);
long beginningExtended = beginning - (long) (habit.freqDen) * day;
List<Repetition> reps = habit.repetitions.selectFromTo(beginningExtended, today).execute();
if(from > to) return;
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'
}
}

@ -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

@ -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

@ -1 +1 @@
Subproject commit 318d69cf6b2adc287cf8944bb847dd7139c60376
Subproject commit 54ca667d4cfb0e38d0c9df816360059ac0675afe
Loading…
Cancel
Save