From eee2605f741723e4861ecbee5bb74e9a10a3117e Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 12 Mar 2016 20:40:43 -0500 Subject: [PATCH 01/12] Add first unit tests for habit --- app/build.gradle | 2 +- .../uhabits/{ => ui}/HabitMatchers.java | 2 +- .../uhabits/{ => ui}/HabitViewActions.java | 3 +- .../uhabits/{ => ui}/MainActivityActions.java | 10 +- .../org/isoron/uhabits/{ => ui}/MainTest.java | 32 +++--- .../{ => ui}/ShowHabitActivityActions.java | 9 +- .../isoron/uhabits/unit/models/HabitTest.java | 98 +++++++++++++++++++ 7 files changed, 130 insertions(+), 26 deletions(-) rename app/src/androidTest/java/org/isoron/uhabits/{ => ui}/HabitMatchers.java (98%) rename app/src/androidTest/java/org/isoron/uhabits/{ => ui}/HabitViewActions.java (98%) rename app/src/androidTest/java/org/isoron/uhabits/{ => ui}/MainActivityActions.java (95%) rename app/src/androidTest/java/org/isoron/uhabits/{ => ui}/MainTest.java (84%) rename app/src/androidTest/java/org/isoron/uhabits/{ => ui}/ShowHabitActivityActions.java (86%) create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java diff --git a/app/build.gradle b/app/build.gradle index a1a1bb4bf..d0a194735 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,7 +8,6 @@ android { applicationId "org.isoron.uhabits" minSdkVersion 15 targetSdkVersion 23 - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } @@ -36,6 +35,7 @@ dependencies { androidTestCompile 'com.android.support:support-annotations:23.1.1' androidTestCompile 'com.android.support.test:runner:0.4.1' androidTestCompile 'com.android.support.test:rules:0.4.1' + androidTestCompile 'org.hamcrest:hamcrest-library:1.3' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1' } diff --git a/app/src/androidTest/java/org/isoron/uhabits/HabitMatchers.java b/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java similarity index 98% rename from app/src/androidTest/java/org/isoron/uhabits/HabitMatchers.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java index c96b24c16..0fbb13f34 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/HabitMatchers.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits; +package org.isoron.uhabits.ui; import android.view.View; import android.widget.Adapter; diff --git a/app/src/androidTest/java/org/isoron/uhabits/HabitViewActions.java b/app/src/androidTest/java/org/isoron/uhabits/ui/HabitViewActions.java similarity index 98% rename from app/src/androidTest/java/org/isoron/uhabits/HabitViewActions.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/HabitViewActions.java index 7c9a7e0a2..afa630ea0 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/HabitViewActions.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/HabitViewActions.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -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; diff --git a/app/src/androidTest/java/org/isoron/uhabits/MainActivityActions.java b/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java similarity index 95% rename from app/src/androidTest/java/org/isoron/uhabits/MainActivityActions.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java index 3ce26da37..412bee5f9 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/MainActivityActions.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java @@ -17,11 +17,11 @@ * with this program. If not, see . */ -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 { diff --git a/app/src/androidTest/java/org/isoron/uhabits/MainTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java similarity index 84% rename from app/src/androidTest/java/org/isoron/uhabits/MainTest.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java index b6047c69a..ab7119f3d 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/MainTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java @@ -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 diff --git a/app/src/androidTest/java/org/isoron/uhabits/ShowHabitActivityActions.java b/app/src/androidTest/java/org/isoron/uhabits/ui/ShowHabitActivityActions.java similarity index 86% rename from app/src/androidTest/java/org/isoron/uhabits/ShowHabitActivityActions.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/ShowHabitActivityActions.java index cacc9f21f..31a89c397 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ShowHabitActivityActions.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/ShowHabitActivityActions.java @@ -17,18 +17,21 @@ * with this program. If not, see . */ -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()); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java new file mode 100644 index 000000000..4715396b8 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +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() + { + for(Habit h : Habit.getAll(true)) + h.cascadeDelete(); + } + + @Test + public void reorderTest() + { + List 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 ids = new LinkedList<>(); + + int originalPositions[] = { 0, 1, 1, 4, 6, 8, 10, 10, 13}; + int length = originalPositions.length; + + for (int i = 0; i < length; i++) + { + Habit h = new Habit(); + h.position = originalPositions[i]; + h.save(); + ids.add(h.getId()); + } + + Habit.rebuildOrder(); + + for (int i = 0; i < length; i++) + { + Habit h = Habit.get(ids.get(i)); + assertThat(h.position, is(i)); + } + } +} From a2c2a5531a877c634d66646d6957180669604bd4 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sun, 13 Mar 2016 07:35:55 -0400 Subject: [PATCH 02/12] Use temporary database for tests --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 8 +- .../org/isoron/uhabits/HabitsApplication.java | 78 +++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/HabitsApplication.java diff --git a/app/build.gradle b/app/build.gradle index d0a194735..c042caeb7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,6 +8,10 @@ android { applicationId "org.isoron.uhabits" minSdkVersion 15 targetSdkVersion 23 + + buildConfigField "Integer", "databaseVersion", "12" + buildConfigField "String", "databaseFilename", "\"uhabits.db\"" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } @@ -35,7 +39,6 @@ dependencies { androidTestCompile 'com.android.support:support-annotations:23.1.1' androidTestCompile 'com.android.support.test:runner:0.4.1' androidTestCompile 'com.android.support.test:rules:0.4.1' - androidTestCompile 'org.hamcrest:hamcrest-library:1.3' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 545c02988..6bbdfba86 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,19 +35,13 @@ android:maxSdkVersion="18"/> - - diff --git a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java new file mode 100644 index 000000000..a7573d946 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +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(); + } +} From 1930db3cd1b633618b2927788a55655d43a86f73 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sun, 13 Mar 2016 08:02:07 -0400 Subject: [PATCH 03/12] Wait after toggling checkmarks --- app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java index ab7119f3d..0adfa290c 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java @@ -120,7 +120,7 @@ public class MainTest } @Test - public void testAddHabitAndViewStats() + public void testAddHabitAndViewStats() throws InterruptedException { String name = addHabit(true); @@ -128,6 +128,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()); From 3d1c53396cd466f0ec1610e87c286a0b83a8bd5a Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sun, 13 Mar 2016 10:37:22 -0400 Subject: [PATCH 04/12] Allow date to be fixed at a certain timestamp --- .../main/java/org/isoron/helpers/DateHelper.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/isoron/helpers/DateHelper.java b/app/src/main/java/org/isoron/helpers/DateHelper.java index 0aef8c0c6..dbc20b35b 100644 --- a/app/src/main/java/org/isoron/helpers/DateHelper.java +++ b/app/src/main/java/org/isoron/helpers/DateHelper.java @@ -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; } - } From 144524e53b06d9b925413ed4f76cf3a26888663e Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sun, 13 Mar 2016 10:37:42 -0400 Subject: [PATCH 05/12] Refactor and write tests for checkmarks --- .../unit/models/CheckmarkListTest.java | 167 ++++++++++++++++++ .../isoron/uhabits/unit/models/HabitTest.java | 8 +- .../uhabits/fragments/ShowHabitFragment.java | 2 - .../org/isoron/uhabits/models/Checkmark.java | 21 ++- .../isoron/uhabits/models/CheckmarkList.java | 114 ++++++++---- .../isoron/uhabits/models/RepetitionList.java | 1 - .../isoron/uhabits/views/CheckmarkView.java | 2 +- 7 files changed, 264 insertions(+), 51 deletions(-) create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java new file mode 100644 index 000000000..a952f2887 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +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; + + public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC) + + @Before + public void prepare() + { + DateHelper.setFixedLocalTime(FIXED_LOCAL_TIME); + createNonDailyHabit(); + + emptyHabit = new Habit(); + emptyHabit.save(); + } + + private void createNonDailyHabit() + { + nonDailyHabit = new Habit(); + nonDailyHabit.freqNum = 2; + nonDailyHabit.freqDen = 3; + nonDailyHabit.save(); + + boolean check[] = { true, false, false, true, true, true, false, false, true, true }; + + long timestamp = DateHelper.getStartOfToday(); + for(boolean c : check) + { + if(c) nonDailyHabit.repetitions.toggle(timestamp); + timestamp -= DateHelper.millisecondsInOneDay; + } + } + + @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(FIXED_LOCAL_TIME + days * DateHelper.millisecondsInOneDay); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java index 4715396b8..7d2f2b3c3 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java @@ -75,21 +75,19 @@ public class HabitTest public void rebuildOrderTest() { List ids = new LinkedList<>(); - int originalPositions[] = { 0, 1, 1, 4, 6, 8, 10, 10, 13}; - int length = originalPositions.length; - for (int i = 0; i < length; i++) + for (int p : originalPositions) { Habit h = new Habit(); - h.position = originalPositions[i]; + h.position = p; h.save(); ids.add(h.getId()); } Habit.rebuildOrder(); - for (int i = 0; i < length; i++) + for (int i = 0; i < originalPositions.length; i++) { Habit h = Habit.get(ids.get(i)); assertThat(h.position, is(i)); diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java index e1dde9c66..6f57abc9c 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java @@ -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); diff --git a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java index e1007a26f..089008efd 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java +++ b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java @@ -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; diff --git a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java index 04c8fe458..af638e903 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -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 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 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; } } diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index f21038748..fe7b72879 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -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; diff --git a/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java b/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java index d6b6fcd5c..ba310c70f 100644 --- a/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java +++ b/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java @@ -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; From 1a18bb939d3cf20f81e5dcb886213e04db883a7f Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sun, 13 Mar 2016 13:55:16 -0400 Subject: [PATCH 06/12] Refactor and write unit tests for RepetitionList --- .../unit/models/CheckmarkListTest.java | 31 +--- .../uhabits/unit/models/HabitFixtures.java | 60 +++++++ .../isoron/uhabits/unit/models/HabitTest.java | 3 +- .../unit/models/RepetitionListTest.java | 162 ++++++++++++++++++ .../uhabits/HabitBroadcastReceiver.java | 3 +- .../isoron/uhabits/models/RepetitionList.java | 51 ++++-- 6 files changed, 268 insertions(+), 42 deletions(-) create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java index a952f2887..5c017da75 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java @@ -42,33 +42,13 @@ public class CheckmarkListTest Habit nonDailyHabit; private Habit emptyHabit; - public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC) - @Before public void prepare() { - DateHelper.setFixedLocalTime(FIXED_LOCAL_TIME); - createNonDailyHabit(); - - emptyHabit = new Habit(); - emptyHabit.save(); - } - - private void createNonDailyHabit() - { - nonDailyHabit = new Habit(); - nonDailyHabit.freqNum = 2; - nonDailyHabit.freqDen = 3; - nonDailyHabit.save(); - - boolean check[] = { true, false, false, true, true, true, false, false, true, true }; - - long timestamp = DateHelper.getStartOfToday(); - for(boolean c : check) - { - if(c) nonDailyHabit.repetitions.toggle(timestamp); - timestamp -= DateHelper.millisecondsInOneDay; - } + HabitFixtures.purgeHabits(); + DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME); + nonDailyHabit = HabitFixtures.createNonDailyHabit(); + emptyHabit = HabitFixtures.createEmptyHabit(); } @After @@ -162,6 +142,7 @@ public class CheckmarkListTest private void travelInTime(int days) { - DateHelper.setFixedLocalTime(FIXED_LOCAL_TIME + days * DateHelper.millisecondsInOneDay); + DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME + + days * DateHelper.millisecondsInOneDay); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java new file mode 100644 index 000000000..3cbea7a0d --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +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(); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java index 7d2f2b3c3..d46826974 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java @@ -40,8 +40,7 @@ public class HabitTest @Before public void prepare() { - for(Habit h : Habit.getAll(true)) - h.cascadeDelete(); + HabitFixtures.purgeHabits(); } @Test diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java new file mode 100644 index 000000000..1c30c28d0 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * 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 . + */ + +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 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)); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java index a43757c84..c4969b835 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java +++ b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java @@ -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(); diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index fe7b72879..ebdf35c66 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -47,6 +47,7 @@ public class RepetitionList { return new Select().from(Repetition.class) .where("habit = ?", habit.getId()) + .and("timestamp <= ?", DateHelper.getStartOfToday()) .orderBy("timestamp"); } @@ -55,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) @@ -69,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); @@ -95,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 getWeekdayFrequency() { Repetition oldestRep = getOldest(); @@ -116,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); From 9156bba267c5afa0e23bd179102cf86f37e682a2 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sun, 13 Mar 2016 14:12:13 -0400 Subject: [PATCH 07/12] Disable animations when testing --- .travis.yml | 20 ++++++ app/build.gradle | 12 +++- .../java/org/isoron/uhabits/ui/MainTest.java | 12 ++++ .../isoron/uhabits/ui/SystemAnimations.java | 68 +++++++++++++++++++ app/src/debug/AndroidManifest.xml | 26 +++++++ 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 .travis.yml create mode 100644 app/src/androidTest/java/org/isoron/uhabits/ui/SystemAnimations.java create mode 100644 app/src/debug/AndroidManifest.xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..173b3f416 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: android +jdk: openjdk7 +env: + global: + - ADB_INSTALL_TIMEOUT=8 +android: + components: + - build-tools-23.0.1 + - android-23 + - extra + - addon + - sys-img-armeabi-v7a-android-19 +before_script: + - echo no | android create avd --force -n test -t android-19 --abi armeabi-v7a -s "480x800" + - emulator -avd test -no-skin -no-audio -no-window & + - android-wait-for-emulator + - adb shell input keyevent 82 & +script: + - ./gradlew connectedAndroidTest + - cat app/build/reports/androidTests/connected/*html | awk '/
/ { on=1 } /<\/pre>/ { on = 0 } { if(on) print }'
diff --git a/app/build.gradle b/app/build.gradle
index c042caeb7..14e4da3d0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -2,7 +2,7 @@ apply plugin: 'com.android.application'
 
 android {
     compileSdkVersion 23
-    buildToolsVersion "21.1.2"
+    buildToolsVersion "23.0.1"
 
     defaultConfig {
         applicationId "org.isoron.uhabits"
@@ -43,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
+    }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
index 0adfa290c..e76e4d783 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
@@ -63,6 +63,18 @@ public class MainTest
             MainActivity.class);
 
     @Before
+    public void setup()
+    {
+        disableAnimations();
+        skipTutorial();
+    }
+
+    public void disableAnimations()
+    {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        new SystemAnimations(context).disableAll();
+    }
+
     public void skipTutorial()
     {
         try
diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/SystemAnimations.java b/app/src/androidTest/java/org/isoron/uhabits/ui/SystemAnimations.java
new file mode 100644
index 000000000..56138290e
--- /dev/null
+++ b/app/src/androidTest/java/org/isoron/uhabits/ui/SystemAnimations.java
@@ -0,0 +1,68 @@
+package org.isoron.uhabits.ui;
+
+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;
+
+/**
+ * Disable animations so that they do not interfere with Espresso tests.
+ *
+ * Source: https://code.google.com/p/android-test-kit/wiki/DisablingAnimations
+ */
+public final class SystemAnimations 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;
+
+    SystemAnimations(Context context) {
+        this.context = context;
+    }
+
+    void disableAll() {
+        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 enableAll() {
+        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 + " :'(");
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..0829dbe45
--- /dev/null
+++ b/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+    
+
+

From 4b7e2e79a785830e5fbd9d2477da28072f34ab41 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 05:12:03 -0400
Subject: [PATCH 08/12] Update tools versions

---
 .travis.yml                              | 10 +++++-----
 build.gradle                             |  2 +-
 gradle/wrapper/gradle-wrapper.properties |  2 +-
 libs/drag-sort-listview                  |  2 +-
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 173b3f416..1361075ec 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,20 +1,20 @@
 language: android
 jdk: openjdk7
-env:
-  global:
-    - ADB_INSTALL_TIMEOUT=8
+
 android:
   components:
     - build-tools-23.0.1
     - android-23
     - extra
     - addon
-    - sys-img-armeabi-v7a-android-19
+    - sys-img-armeabi-v7a-android-21
+
 before_script:
-  - echo no | android create avd --force -n test -t android-19 --abi armeabi-v7a -s "480x800"
+  - echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a -s "480x800"
   - emulator -avd test -no-skin -no-audio -no-window &
   - android-wait-for-emulator
   - adb shell input keyevent 82 &
+
 script:
   - ./gradlew connectedAndroidTest
   - cat app/build/reports/androidTests/connected/*html | awk '/
/ { on=1 } /<\/pre>/ { on = 0 } { if(on) print }'
diff --git a/build.gradle b/build.gradle
index f4d8c542e..5e38f58a7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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'
     }
 }
 
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0c71e760d..d57051703 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -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
diff --git a/libs/drag-sort-listview b/libs/drag-sort-listview
index 318d69cf6..54ca667d4 160000
--- a/libs/drag-sort-listview
+++ b/libs/drag-sort-listview
@@ -1 +1 @@
-Subproject commit 318d69cf6b2adc287cf8944bb847dd7139c60376
+Subproject commit 54ca667d4cfb0e38d0c9df816360059ac0675afe

From 196a79a88c216fb5316e224c3c94b13d88f97b26 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 06:32:47 -0400
Subject: [PATCH 09/12] Add CircleCI config file

---
 circle.yml | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
 create mode 100644 circle.yml

diff --git a/circle.yml b/circle.yml
new file mode 100644
index 000000000..87496af48
--- /dev/null
+++ b/circle.yml
@@ -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 my-project/build/outputs $CIRCLE_ARTIFACTS
+    - cp -r my-project/build/outputs/androidTest-results/* $CIRCLE_TEST_REPORTS
\ No newline at end of file

From 4cf2b8072b3c5aed288fb959e20f576463a0f4eb Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 06:51:58 -0400
Subject: [PATCH 10/12] Unlock screen before running UI tests

---
 .../java/org/isoron/uhabits/ui/MainTest.java  | 12 ++--
 ...ystemAnimations.java => SystemHelper.java} | 63 ++++++++++++-------
 app/src/debug/AndroidManifest.xml             |  1 +
 3 files changed, 48 insertions(+), 28 deletions(-)
 rename app/src/androidTest/java/org/isoron/uhabits/ui/{SystemAnimations.java => SystemHelper.java} (58%)

diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
index e76e4d783..05877fcb0 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
@@ -64,15 +64,13 @@ public class MainTest
 
     @Before
     public void setup()
-    {
-        disableAnimations();
-        skipTutorial();
-    }
-
-    public void disableAnimations()
     {
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        new SystemAnimations(context).disableAll();
+        SystemHelper sys = new SystemHelper(context);
+        sys.disableAllAnimations();
+        sys.unlockScreen();
+
+        skipTutorial();
     }
 
     public void skipTutorial()
diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/SystemAnimations.java b/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java
similarity index 58%
rename from app/src/androidTest/java/org/isoron/uhabits/ui/SystemAnimations.java
rename to app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java
index 56138290e..5fa69e80e 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/ui/SystemAnimations.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java
@@ -1,5 +1,6 @@
 package org.isoron.uhabits.ui;
 
+import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.IBinder;
@@ -8,12 +9,7 @@ import android.util.Log;
 
 import java.lang.reflect.Method;
 
-/**
- * Disable animations so that they do not interfere with Espresso tests.
- *
- * Source: https://code.google.com/p/android-test-kit/wiki/DisablingAnimations
- */
-public final class SystemAnimations extends AndroidJUnitRunner
+public final class SystemHelper extends AndroidJUnitRunner
 {
     private static final String ANIMATION_PERMISSION = "android.permission.SET_ANIMATION_SCALE";
     private static final float DISABLED = 0.0f;
@@ -21,48 +17,73 @@ public final class SystemAnimations extends AndroidJUnitRunner
 
     private final Context context;
 
-    SystemAnimations(Context context) {
+    SystemHelper(Context context)
+    {
         this.context = context;
     }
 
-    void disableAll() {
+    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) {
+        if (permStatus == PackageManager.PERMISSION_GRANTED)
             setSystemAnimationsScale(DISABLED);
-        } else {
+        else
             Log.e("SystemAnimations", "Permission denied");
-        }
 
     }
 
-    void enableAll() {
+    void enableAllAnimations()
+    {
         int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
-        if (permStatus == PackageManager.PERMISSION_GRANTED) {
+        if (permStatus == PackageManager.PERMISSION_GRANTED)
+        {
             setSystemAnimationsScale(DEFAULT);
         }
     }
 
-    private void setSystemAnimationsScale(float animationScale) {
-        try {
+    private void setSystemAnimationsScale(float animationScale)
+    {
+        try
+        {
             Class windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
-            Method asInterface = windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
+            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 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++) {
+            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 + " :'(");
+        }
+        catch (Exception e)
+        {
+            Log.e("SystemAnimations",
+                    "Could not change animation scale to " + animationScale + " :'(");
         }
     }
 }
\ No newline at end of file
diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
index 0829dbe45..10d453a48 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -22,5 +22,6 @@
     xmlns:android="http://schemas.android.com/apk/res/android">
 
     
+    
 
 

From 012fed01eb47379dacc7aca699daec2e87327961 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 07:11:45 -0400
Subject: [PATCH 11/12] Fix CircleCI; remove Travis-CI

---
 .travis.yml | 20 --------------------
 circle.yml  |  4 ++--
 2 files changed, 2 insertions(+), 22 deletions(-)
 delete mode 100644 .travis.yml

diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 1361075ec..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-language: android
-jdk: openjdk7
-
-android:
-  components:
-    - build-tools-23.0.1
-    - android-23
-    - extra
-    - addon
-    - sys-img-armeabi-v7a-android-21
-
-before_script:
-  - echo no | android create avd --force -n test -t android-21 --abi armeabi-v7a -s "480x800"
-  - emulator -avd test -no-skin -no-audio -no-window &
-  - android-wait-for-emulator
-  - adb shell input keyevent 82 &
-
-script:
-  - ./gradlew connectedAndroidTest
-  - cat app/build/reports/androidTests/connected/*html | awk '/
/ { on=1 } /<\/pre>/ { on = 0 } { if(on) print }'
diff --git a/circle.yml b/circle.yml
index 87496af48..368dbe466 100644
--- a/circle.yml
+++ b/circle.yml
@@ -11,5 +11,5 @@ test:
     - circle-android wait-for-boot
     - adb shell input keyevent 82
     - ./gradlew connectedAndroidTest
-    - cp -r my-project/build/outputs $CIRCLE_ARTIFACTS
-    - cp -r my-project/build/outputs/androidTest-results/* $CIRCLE_TEST_REPORTS
\ No newline at end of file
+    - cp -r app/build/outputs $CIRCLE_ARTIFACTS || echo ok
+    - cp -r app/build/reports/androidTests/connected/* $CIRCLE_TEST_REPORTS || echo ok
\ No newline at end of file

From e95adab42202be60f6292a2c9edd1d25ca619e7e Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 07:37:08 -0400
Subject: [PATCH 12/12] Include CircleCI badge

---
 README.md | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/README.md b/README.md
index e3c7796c8..271d6aa9a 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
+
+  
+
+
 # Loop Habit Tracker
 
 Loop is a simple Android app that helps you create and maintain good habits,