From 9061182301fae81262893727979fe1b67270365b Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 12 Mar 2016 19:39:03 -0500 Subject: [PATCH 001/175] Update translators' names --- app/src/main/res/layout/about.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml index c183cb477..2d8a24615 100644 --- a/app/src/main/res/layout/about.xml +++ b/app/src/main/res/layout/about.xml @@ -115,11 +115,11 @@ + android:text="Naofumi F (日本語)"/> + android:text="Matthias Meisser (Deutsch)"/> Date: Sun, 13 Mar 2016 23:46:29 +0100 Subject: [PATCH 002/175] Update EditText of Edit Habit to capitalize sentences Basically when entering a word the first letter will be capitalized. Something minor that bugs me always a bit. --- app/src/main/res/layout/edit_habit.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/edit_habit.xml b/app/src/main/res/layout/edit_habit.xml index 2dbbeab8d..46cba4a36 100644 --- a/app/src/main/res/layout/edit_habit.xml +++ b/app/src/main/res/layout/edit_habit.xml @@ -35,6 +35,7 @@ @@ -124,4 +125,4 @@ android:layout_height="wrap_content" android:text="@string/save" /> - \ No newline at end of file + From 9df0c9ae9ed3d7b1e09d3ceca204eaaeec3961da Mon Sep 17 00:00:00 2001 From: Niklas Baudy Date: Mon, 14 Mar 2016 10:19:47 +0100 Subject: [PATCH 003/175] Update EditText of Edit Habit Description to capitalize sentences --- app/src/main/res/layout/edit_habit.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/edit_habit.xml b/app/src/main/res/layout/edit_habit.xml index 2dbbeab8d..4465c6994 100644 --- a/app/src/main/res/layout/edit_habit.xml +++ b/app/src/main/res/layout/edit_habit.xml @@ -50,6 +50,7 @@ - \ No newline at end of file + From eee2605f741723e4861ecbee5bb74e9a10a3117e Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Sat, 12 Mar 2016 20:40:43 -0500 Subject: [PATCH 004/175] 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 005/175] 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 006/175] 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 007/175] 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 008/175] 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 009/175] 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 010/175] 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 011/175] 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 012/175] 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 013/175] 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 014/175] 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 015/175] 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,

From 988b39f2e5e7da1fb24fba2eaf433e3d0fe65362 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 08:33:46 -0400
Subject: [PATCH 016/175] Add missing debug messages to SystemHelper

---
 .../java/org/isoron/uhabits/ui/SystemHelper.java      | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java b/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java
index 5fa69e80e..718ac4ba3 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java
@@ -24,27 +24,30 @@ public final class SystemHelper extends AndroidJUnitRunner
 
     void unlockScreen()
     {
+        Log.i("SystemHelper", "Trying to unlock screen");
         try
         {
             KeyguardManager mKeyGuardManager = (KeyguardManager) context
                     .getSystemService(Context.KEYGUARD_SERVICE);
             KeyguardManager.KeyguardLock mLock = mKeyGuardManager.newKeyguardLock("lock");
             mLock.disableKeyguard();
+            Log.e("SystemHelper", "Successfully unlocked screen");
         }
         catch (Exception e)
         {
+            Log.e("SystemHelper", "Could not unlock screen");
             e.printStackTrace();
         }
     }
 
     void disableAllAnimations()
     {
-        Log.i("SystemAnimations", "Trying to disable animations");
+        Log.i("SystemHelper", "Trying to disable animations");
         int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
         if (permStatus == PackageManager.PERMISSION_GRANTED)
             setSystemAnimationsScale(DISABLED);
         else
-            Log.e("SystemAnimations", "Permission denied");
+            Log.e("SystemHelper", "Permission denied");
 
     }
 
@@ -78,11 +81,11 @@ public final class SystemHelper extends AndroidJUnitRunner
                 currentScales[i] = animationScale;
 
             setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
-            Log.i("SystemAnimations", "All animations successfully disabled");
+            Log.i("SystemHelper", "All animations successfully disabled");
         }
         catch (Exception e)
         {
-            Log.e("SystemAnimations",
+            Log.e("SystemHelper",
                     "Could not change animation scale to " + animationScale + " :'(");
         }
     }

From 40420c3a7759d5349f490dd6ed1a0504642b8ac9 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 08:37:02 -0400
Subject: [PATCH 017/175] Save logcat to reports

---
 circle.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/circle.yml b/circle.yml
index 368dbe466..3b88e723c 100644
--- a/circle.yml
+++ b/circle.yml
@@ -12,4 +12,5 @@ test:
     - 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
\ No newline at end of file
+    - cp -r app/build/reports/androidTests/connected/* $CIRCLE_TEST_REPORTS || echo ok
+    - adb logcat -d > $CIRCLE_TEST_REPORTS/logcat.txt
\ No newline at end of file

From 45a743377378b915cc2b384c5075cdcae2bc6ef0 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 13:34:05 -0400
Subject: [PATCH 018/175] Use StaticLayout to draw RingView label

Fixes #29
---
 .../org/isoron/uhabits/views/RingView.java    | 28 ++++++++++++++-----
 1 file changed, 21 insertions(+), 7 deletions(-)

diff --git a/app/src/main/java/org/isoron/uhabits/views/RingView.java b/app/src/main/java/org/isoron/uhabits/views/RingView.java
index 80c04c095..1e05e00d3 100644
--- a/app/src/main/java/org/isoron/uhabits/views/RingView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/RingView.java
@@ -24,6 +24,9 @@ import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.RectF;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
 import android.util.AttributeSet;
 import android.view.View;
 
@@ -37,10 +40,11 @@ public class RingView extends View
     private int size;
     private int color;
     private float percentage;
-    private Paint pRing;
-    private float lineHeight;
+    private float labelMarginTop;
+    private TextPaint pRing;
     private String label;
     private RectF rect;
+    private StaticLayout labelLayout;
 
     public RingView(Context context, AttributeSet attrs)
     {
@@ -68,12 +72,16 @@ public class RingView extends View
 
     private void init()
     {
-        pRing = new Paint();
+        pRing = new TextPaint();
         pRing.setAntiAlias(true);
         pRing.setColor(color);
         pRing.setTextAlign(Paint.Align.CENTER);
-        pRing.setTextSize(size * 0.2f);
-        lineHeight = pRing.getFontSpacing();
+
+        pRing.setTextSize(size * 0.15f);
+        labelMarginTop = size * 0.10f;
+        labelLayout = new StaticLayout(label, pRing, size, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f,
+                false);
+
         rect = new RectF();
     }
 
@@ -81,7 +89,11 @@ public class RingView extends View
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
     {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-        setMeasuredDimension(size, size + (int) (2 * lineHeight));
+
+        int width = Math.max(size, labelLayout.getWidth());
+        int height = (int) (size + labelLayout.getHeight() + labelMarginTop);
+
+        setMeasuredDimension(width, height);
     }
 
     @Override
@@ -101,12 +113,14 @@ public class RingView extends View
         rect.inset(thickness, thickness);
         canvas.drawArc(rect, -90, 360, true, pRing);
 
+        float lineHeight = pRing.getFontSpacing();
         pRing.setColor(Color.GRAY);
         pRing.setTextSize(size * 0.2f);
         canvas.drawText(String.format("%.0f%%", percentage * 100), rect.centerX(),
                 rect.centerY() + lineHeight / 3, pRing);
 
         pRing.setTextSize(size * 0.15f);
-        canvas.drawText(label, size / 2, size + lineHeight * 1.2f, pRing);
+        canvas.translate(size / 2, size + labelMarginTop);
+        labelLayout.draw(canvas);
     }
 }

From f7f4b5eeb08d7f12f8cd18d923d3424d9609ac37 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 14:35:57 -0400
Subject: [PATCH 019/175] Simplify code for drawing header

---
 .../uhabits/views/HabitHistoryView.java       | 42 +++++--------------
 1 file changed, 11 insertions(+), 31 deletions(-)

diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java
index 67191b417..18bbc1e0e 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java
@@ -240,7 +240,6 @@ public class HabitHistoryView extends ScrollableDataView
 
     private String previousMonth;
     private String previousYear;
-    private boolean justPrintedYear;
 
     @Override
     protected void onDraw(Canvas canvas)
@@ -250,10 +249,9 @@ public class HabitHistoryView extends ScrollableDataView
         baseLocation.set(0, 0, columnWidth - squareSpacing, columnWidth - squareSpacing);
         baseLocation.offset(getPaddingLeft(), getPaddingTop());
 
+        headerOverflow = 0;
         previousMonth = "";
         previousYear = "";
-        justPrintedYear = false;
-
         pTextHeader.setColor(textColor);
 
         updateDate();
@@ -307,44 +305,26 @@ public class HabitHistoryView extends ScrollableDataView
         }
     }
 
-    private boolean justSkippedColumn = false;
+    private float headerOverflow = 0;
 
     private void drawColumnHeader(Canvas canvas, Rect location, GregorianCalendar date)
     {
         String month = dfMonth.format(date.getTime());
         String year = dfYear.format(date.getTime());
 
+        String text = null;
         if (!month.equals(previousMonth))
-        {
-            int offset = 0;
-            if (justPrintedYear)
-            {
-                offset += columnWidth;
-                justSkippedColumn = true;
-            }
+            text = previousMonth = month;
+        else if(!year.equals(previousYear))
+            text = previousYear = year;
 
-            canvas.drawText(month, location.left + offset, location.bottom - headerTextOffset,
-                    pTextHeader);
-
-            previousMonth = month;
-            justPrintedYear = false;
-        }
-        else if (!year.equals(previousYear))
+        if(text != null)
         {
-            if(!justSkippedColumn)
-            {
-                canvas.drawText(year, location.left, location.bottom - headerTextOffset, pTextHeader);
-                previousYear = year;
-                justPrintedYear = true;
-            }
-
-            justSkippedColumn = false;
-        }
-        else
-        {
-            justSkippedColumn = false;
-            justPrintedYear = false;
+            canvas.drawText(text, location.left + headerOverflow, location.bottom - headerTextOffset, pTextHeader);
+            headerOverflow += pTextHeader.measureText(text) + columnWidth * 0.2f;
         }
+
+        headerOverflow = Math.max(0, headerOverflow - columnWidth);
     }
 
     public void setIsBackgroundTransparent(boolean isBackgroundTransparent)

From 18abb2038f78e62736978ed8a45d28a89bb83525 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 14:54:43 -0400
Subject: [PATCH 020/175] Check if habit is null on BaseWidgetProvider

---
 .../java/org/isoron/uhabits/widgets/BaseWidgetProvider.java     | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java
index 6f95d5713..35f7538c1 100644
--- a/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java
+++ b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java
@@ -106,6 +106,8 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider
         if(habitId < 0) return;
 
         Habit habit = Habit.get(habitId);
+        if(habit == null) return;
+
         View widgetView = buildCustomView(context, habit);
         measureCustomView(context, width, height, widgetView);
 

From 866b62987c31cd11575ed9f19bbeb6fa3555c363 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 14:55:47 -0400
Subject: [PATCH 021/175] Wake up device before running UI tests

---
 .../java/org/isoron/uhabits/ui/MainTest.java  | 12 ++++++-
 .../org/isoron/uhabits/ui/SystemHelper.java   | 33 +++++++++++++------
 app/src/debug/AndroidManifest.xml             |  1 +
 3 files changed, 35 insertions(+), 11 deletions(-)

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 05877fcb0..bcd6b6530 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
@@ -10,6 +10,7 @@ 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.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -58,6 +59,8 @@ import static org.isoron.uhabits.ui.ShowHabitActivityActions.openHistoryEditor;
 @LargeTest
 public class MainTest
 {
+    private SystemHelper sys;
+
     @Rule
     public IntentsTestRule activityRule = new IntentsTestRule<>(
             MainActivity.class);
@@ -66,13 +69,20 @@ public class MainTest
     public void setup()
     {
         Context context = InstrumentationRegistry.getInstrumentation().getContext();
-        SystemHelper sys = new SystemHelper(context);
+        sys = new SystemHelper(context);
         sys.disableAllAnimations();
+        sys.acquireWakeLock();
         sys.unlockScreen();
 
         skipTutorial();
     }
 
+    @After
+    public void tearDown()
+    {
+        sys.releaseWakeLock();
+    }
+
     public void skipTutorial()
     {
         try
diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java b/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java
index 718ac4ba3..807e3b36c 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/ui/SystemHelper.java
@@ -4,6 +4,7 @@ import android.app.KeyguardManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.IBinder;
+import android.os.PowerManager;
 import android.support.test.runner.AndroidJUnitRunner;
 import android.util.Log;
 
@@ -16,24 +17,39 @@ public final class SystemHelper extends AndroidJUnitRunner
     private static final float DEFAULT = 1.0f;
 
     private final Context context;
+    private PowerManager.WakeLock wakeLock;
 
     SystemHelper(Context context)
     {
         this.context = context;
     }
 
+    void acquireWakeLock()
+    {
+        PowerManager power = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        wakeLock = power.newWakeLock(PowerManager.FULL_WAKE_LOCK |
+                PowerManager.ACQUIRE_CAUSES_WAKEUP |
+                PowerManager.ON_AFTER_RELEASE, getClass().getSimpleName());
+        wakeLock.acquire();
+    }
+
+    void releaseWakeLock()
+    {
+        if(wakeLock != null)
+            wakeLock.release();
+    }
+
     void unlockScreen()
     {
         Log.i("SystemHelper", "Trying to unlock screen");
         try
         {
-            KeyguardManager mKeyGuardManager = (KeyguardManager) context
-                    .getSystemService(Context.KEYGUARD_SERVICE);
+            KeyguardManager mKeyGuardManager =
+                    (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
             KeyguardManager.KeyguardLock mLock = mKeyGuardManager.newKeyguardLock("lock");
             mLock.disableKeyguard();
             Log.e("SystemHelper", "Successfully unlocked screen");
-        }
-        catch (Exception e)
+        } catch (Exception e)
         {
             Log.e("SystemHelper", "Could not unlock screen");
             e.printStackTrace();
@@ -44,10 +60,8 @@ public final class SystemHelper extends AndroidJUnitRunner
     {
         Log.i("SystemHelper", "Trying to disable animations");
         int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
-        if (permStatus == PackageManager.PERMISSION_GRANTED)
-            setSystemAnimationsScale(DISABLED);
-        else
-            Log.e("SystemHelper", "Permission denied");
+        if (permStatus == PackageManager.PERMISSION_GRANTED) setSystemAnimationsScale(DISABLED);
+        else Log.e("SystemHelper", "Permission denied");
 
     }
 
@@ -85,8 +99,7 @@ public final class SystemHelper extends AndroidJUnitRunner
         }
         catch (Exception e)
         {
-            Log.e("SystemHelper",
-                    "Could not change animation scale to " + animationScale + " :'(");
+            Log.e("SystemHelper", "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 10d453a48..7ba402e56 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -23,5 +23,6 @@
 
     
     
+  
 
 

From 65fd82d88807df1af6c89a15665371e949c7ca52 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 15:04:15 -0400
Subject: [PATCH 022/175] Fix indentation

---
 app/src/debug/AndroidManifest.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml
index 7ba402e56..01dd1532a 100644
--- a/app/src/debug/AndroidManifest.xml
+++ b/app/src/debug/AndroidManifest.xml
@@ -23,6 +23,6 @@
 
     
     
-  
+    
 
 

From 5186ab840abb7a9cf81b251449c02194b40b7340 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 15:11:41 -0400
Subject: [PATCH 023/175] Add Arabic translation

---
 app/src/main/res/layout/about.xml      |   4 +
 app/src/main/res/values-ar/strings.xml | 137 +++++++++++++++++++++++++
 2 files changed, 141 insertions(+)
 create mode 100644 app/src/main/res/values-ar/strings.xml

diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml
index 2d8a24615..3e0e8f713 100644
--- a/app/src/main/res/layout/about.xml
+++ b/app/src/main/res/layout/about.xml
@@ -121,6 +121,10 @@
                 style="@style/aboutItemStyle"
                 android:text="Matthias Meisser (Deutsch)"/>
 
+            
+
             
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
new file mode 100644
index 000000000..5eb8a85c4
--- /dev/null
+++ b/app/src/main/res/values-ar/strings.xml
@@ -0,0 +1,137 @@
+
+
+
+
+    
+    "لوب  ملاحق العادة "
+    "عادات"
+    "إعدادات"
+    "تعديل"
+    "حذف"
+    "أرشيف"
+    "إزالة من الأرشيف"
+    "إضافة العادة"
+    "غير اللون"
+    "تم صنع عادة "
+    "تم حذف عادة "
+    "تم  ترجيع عادة"
+    "لا شيء للتراجع"
+    "لا شيء لتكرار"
+    "تم تغييرعادة"
+
+    
+    "تم ترجيع العادة إلى أصلها"
+    "تم أرشيف العادات"
+    "تم إزالة العادة من الأرشيف "
+    "نظرة عامة"
+    "قوة العادة"
+    "التاريخ"
+    "مسح"
+    "السؤال (هل ... اليوم؟)"
+
+    
+    "كرر"
+    "مرات في"
+    "أيام"
+    "تذكير"
+    "حذف"
+    "حفظ"
+    "تقدم متتالية"
+    " لا يوجد لديك عادات مفعله"
+    "أضغط و إستمر لتحقق أو ازل"
+    "أوقف"
+    "لا يمكن أن يكون الإسم فارغ"
+    "يجب أن يكون الرقم إيجابي"
+    "يمكن أن يكون التكرار واحدة فقط كل يوم "
+    "اخلق عادة"
+    "تعديل العادة"
+    "حقق"
+    "لاحقا"
+
+    
+    "أهلا بك"
+    "لوب يساعدك على خلق والحفاظ على العادات الجيدة."
+    "إنشاء بعض عادات جديدة"
+    "كل يوم، بعد أداء عادتك، وضع علامة على التطبيق."
+    "حافظ على القيام بذلك"
+    "العادة المستمرة لفترات طويلة تكسب نجمة كامله"
+    "تتبع تقدمك"
+    "رسوم بيانية مفصلة تبين لكم كيف تحسن عاداتك مع مرور الوقت."
+    "15 دقيقة"
+    "30 دقيقة"
+    "ساعة واحدة"
+    "ساعتين"
+    "أربع ساعات"
+    "ثماني ساعات"
+    "تبديل بكبسه"
+    "أكثر سهولة، لكنه ممكن يسبب كبسات غير مقصوده"
+    "فترتي الغفوى على التذكير"
+    "تقييم هذا التطبيق على جوجل بلاي"
+    "أرسل الملاحظات إلى المطور"
+    "إفحص التعليمات البرمجية على GitHub"
+    "عرض المقدمه"
+    "روابط"
+    "سلوك"
+    "اسم"
+    "عرض أرشفة"
+    "إعدادات"
+    "فترتي الغفوه"
+    "هل كنت تعلم؟"
+    "لإعادة ترتيب القوائم، أضغط اسم من هذه العادة، ثم اسحبه إلى المكان الصحيح."
+    "يمكنك ان ترى المزيد أيام عن طريق وضع الهاتف في وضع أفقي."
+    "حذف عادات"
+    "سيتم حذف عادات بشكل دائم. هذا العمل لا يمكن التراجع عنه."
+    "عطلة نهاية الأسبوع"
+    "أيام الأسبوع"
+    "أي يوم"
+
+    
+    "إختار أيام "
+    "تصدير البيانات"
+    "منجز"
+    "نظف"
+    "تحديد ساعات"
+    "تحديد دقائق "
+
+    
+    "خلق عادات جيدة وتتبع تقدمك على مر الزمن"
+    "لب يساعدك على خلق والحفاظ على العادات الجيدة، مما يسمح لك لتحقيق أهدافكة. الرسوم البيانية والإحصاءات التفصيلية تبين لكم كيف تحسن عاداتك مع مرور الوقت. هو تماما خالية من الاعلانات ومفتوحة المصدر."
+"<b>واجهة بسيطة، جميلة وحديثة </b>
+لوب يحتوي على واجهة بسيطة وهي سهلة الاستخدام و تتابع نظام تصميم الماتريل دسيجن."
+"<b>نتيجة العادات</b>
+بالإضافة إلى عرض التقدم الحالي، لوب ديه خوارزمية متقدمة لحساب قوة عاداتك. كل التكرار يجعل هذه العادة أقوى، وفي كل يوم غاب يجعلها أضعف. مع ذلك غيب أيام قليلة بعد تقدم طويلة ، لن تدمر تماما تقدمك ."
+"<b>الرسوم البيانية والإحصاءات المفصلة</b>
+نرى بوضوح كيف كنت قد تحسنت عاداتك بمرور الوقت مع الرسوم البيانية  الجميله ومفصلة. انتقل إلى الوراء لنرى التاريخ الكامل لعاداتك."
+"<b>جداول مرنة</b>
+تؤيد كل من العادات اليومية والعادات مع جداول أكثر تعقيدا، مثل 3 مرات كل أسبوع، مرة واحدة كل أسبوعين، أو مرة كل يومين."
+"<b>تذكير</b>
+إنشاء تذكير لكل فرد من عاداتك، في ساعة اختيار من اليوم. تحقق بسهولة، رفض أو غفوة عادتك مباشرة من الإخطار، دون الحاجة إلى فتح التطبيق."
+"<b>خالية تماما من الإعلانات و المصدر المفتوح</b>
+لا توجد على الاطلاق الإعلانات والشعارات المزعجة أو أذونات إضافية في هذا التطبيق، و سوف يكون هناك أبدا."
+"<b>الأمثل للساعات الذكية</b>
+يمكن التحقق من رسائل التذكير، رفض أو غفوة عادتك مباشرة من ساعتك الاندرويد وير. "
+    "معلومات حول"
+    "المترجمين"
+    "المطورين"
+
+    
+    "الإصدار %s"
+    "تردد"
+
\ No newline at end of file

From 6e493f55bcb288375340fb83549078738c2acf89 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 15:18:58 -0400
Subject: [PATCH 024/175] Declare RTL support in the manifest

---
 app/src/main/AndroidManifest.xml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6bbdfba86..6774d6f9b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -40,7 +40,8 @@
         android:backupAgent=".HabitsBackupAgent"
         android:icon="@mipmap/ic_launcher"
         android:label="@string/main_activity_title"
-        android:theme="@style/AppBaseTheme">
+        android:theme="@style/AppBaseTheme"
+        android:supportsRtl="true">
 
         
Date: Mon, 14 Mar 2016 15:33:06 -0400
Subject: [PATCH 025/175] Update French translation

---
 app/src/main/res/values-fr/strings.xml | 51 ++++++++++++++++++--------
 1 file changed, 35 insertions(+), 16 deletions(-)

diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 41bf5f5e0..8427d3da6 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -1,6 +1,7 @@
 
 
-    "Habitude encore changée. "
+    
+    "Habitude non changée."
     "Habitudes archivées."
     "Habitudes désarchivées."
     "Vue d'ensemble"
     "Force de l'habitude"
-    "Histoire"
+    "Historique"
     "Supprimer"
-    "Question (As-tu … aujourd'hui?)"
+    "Question (As-tu ... aujourd'hui?)"
 
     
     "Répéter"
@@ -52,7 +53,7 @@
     "Annuler"
     "Sauvegarder"
     "Séries"
-    "Vous n'avez pas d'habitude active"
+    "Vous n'avez pas d'habitudes actives"
     "Appuyez longtemps pour cocher ou décocher"
     "Off"
     "Le nom ne peut être vide."
@@ -66,12 +67,12 @@
     
     "Bienvenue"
     "Loop - Suivi d'habitudes vous aide à créer et maintenir de bonnes habitudes"
-    "Créer des nouvelles habitudes"
+    "Créer de nouvelles habitudes"
     "Chaque jour, après avoir fait votre habitude, mettez une croix sur l'application"
     "Continuez à le faire"
-    "Les habitudes faites de manière consistente pendant une longue période gagneront une étoile complète"
+    "Les habitudes faites de manière régulière pendant une période de temps étendue gagneront une étoile complète"
     "Suivre votre progrès"
-    "Graphiques détaillés montrant comment vos habitudes évoluent au fil du temps"
+    "Des graphiques détaillés montrant comment vos habitudes évoluent au fil du temps"
     "15 minutes"
     "30 minutes"
     "1 heure"
@@ -80,7 +81,9 @@
     "8 heures"
     "Activer les répétitions avec un appui court"
     "Plus pratique, mais peut causer des activations accidentelles"
-    "Éteindre l'intervalle sur les rappels"
+
+    
+    "Intervalle de report des rappels"
     "Noter cette app sur le Google Play Store"
     "Envoyez un avis au développeur"
     "Voir le code source sur GitHub"
@@ -90,12 +93,12 @@
     "Nom"
     "Montrer les archivées"
     "Paramètres"
-    "Éteindre l'intervalle"
+    "Intervalle de report"
     "Le saviez-vous ? "
-    "Pour réarranger les habitudes, faites un appui long sur le nom de l'habitude et placez là à la bonne place."
-    "Vous pouvez voir plus en jours en mettant votre téléphone en mode paysage."
+    "Pour réarranger les habitudes, faites un appui long sur le nom de l'habitude et placez la à la bonne place."
+    "Vous pouvez voir plus de jours en mettant votre téléphone en mode paysage."
     "Supprimer des habitudes"
-    "Les habitudes seront supprimées pour toujours. Cette action ne peut être refaite."
+    "Les habitudes seront supprimées pour toujours. Cette action ne peut être défaite."
     "Weekends"
     "Jours de la semaine"
     "N'importe quel jour"
@@ -106,12 +109,28 @@
     "Sélectionner les heures"
     "Sélectionner les minutes"
 
-
-    "A propos"
+    
+    "Créez des bonnes habitudes et suivez leurs avancées au fil du temps (sans pub)"
+    "Loop vous aide à créer et maintenir de bonnes habitudes, permettant de réussir vos objectifs à long terme. Des graphiques détaillés et des statistiques vous montrent comment vos habitudes s’améliorent au fil du temps. C'est totalement sans pub et open source."
+"<b>Simple, beau avec une interface moderne</b>
+Loop a une interface minimaliste, facile à utiliser et qui suit les règles de material design."
+"<b>Score d'habitude</b>
+En plus de montrer votre série en cours, Loop a un algorithme pour calculer la force de vos habitudes. Chaque jours réussis augmente la force de l'habitude chaque jours ratés le rend plus faible. Cependant, quelques jours ratés après une longue série ne détruiront pas entièrement votre progrès."
+"<b>Graphiques détaillés et statistiques</b>
+Observez clairement comment vos habitudes s’améliorent au fil du temps avec de beaux graphiques détaillés. Défilez vers les jours passés pour voir l'historique complet de vos habitudes."
+"<b>Calendrier flexible</b>
+Supporte les habitudes quotidiennes et celles avec un calendrier plus complexes, comme 3 fois par semaine, une fois par semaine ou un jour sur deux."
+"<b>Rappel</b>
+Créez un rappel propre pour chaque habitude, à une heure choisie de la journée. Cochez, supprimez ou reportez votre habitude directement à partir de la notification, sans ouvrir l'application."
+"<b>Entièrement sans pub et open-source</b>
+Il n'y a pas de publicités, de notifications embêtantes ou de permissions intrusives avec cette application, et il n'y en aura jamais. L'ensemble du code source est disponible sous GPLv3."
+"<b>Optimisée pour les montres android</b>
+Les rappels peuvent être cochés, reportés ou supprimés directement à partir de votre montre Android"
+    "À propos"
     "Traducteurs"
     "Développeurs"
 
     
     "Version %s"
-    Fréquence
+    "Fréquence"
 
\ No newline at end of file

From f13e9b7362986f062814420c4256d8149d73749c Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 15:38:12 -0400
Subject: [PATCH 026/175] Update German translation

---
 app/src/main/res/values-de/strings.xml | 67 ++++++++++++++------------
 1 file changed, 36 insertions(+), 31 deletions(-)

diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index e11faa834..589ab77b2 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -1,6 +1,7 @@
 
 
-
 
     "Loop Habit Tracker"
-    "Habits"
+    "Gewohnheiten"
     "Einstellung"
     "Bearbeiten"
     "Löschen"
     "Archivieren"
     "Dearchivieren"
-    "Habit hinzufügen"
+    "Gewohnheit hinzufügen"
     "Farbe ändern"
-    "Habit erstellt."
-    "Habits gelöscht."
-    "Habits wiederhergestellt."
+    "Gewohnheit erstellt."
+    "Gewohnheiten gelöscht."
+    "Gewohnheiten wiederhergestellt."
     "Nichts zum rückgängig machen."
     "Nichts zum wiederherstellen"
-    "Habit geändert"
+    "Gewohnheit geändert"
 
     
-    "Habit zurückgeändert"
-    "Habits archiviert."
-    "Habits dearchiviert."
+    "Gewohnheit zurückgeändert"
+    "Gewohnheiten archiviert."
+    "Gewohnheiten dearchiviert."
     "Übersicht"
-    "Habit Stärke"
+    "Stärke"
     "Verlauf"
     "Löschen"
     "Frage (Hast du heute ...?)"
@@ -52,20 +52,23 @@
     "Erinnerung"
     "Verwerfen"
     "Speichern"
-    "Du hast keine aktiven habits"
+    "Serie"
+    "Du hast keine aktiven Gewohnheiten"
     "Berühre und halte um zu (de)markieren"
     "Aus"
     "Name darf nicht leer sein."
     "Zahl muss positiv sein."
     "Du musst wenigstens eine Wiederholung pro Tag haben"
-    "Habit erstellen"
-    "Habit bearbeiten"
+    "Gewohnheit erstellen"
+    "Gewohnheit bearbeiten"
+"prüfen
+"
     "Später"
 
     
     "Willkommen"
     "Loop Habit Tracker hilft dir gute Gewohnheiten anzunehmen."
-    "Erstelle einige neue habits"
+    "Erstelle einige neue Gewohnheiten"
     "Jeden tag nachdem du das habit absolviert hast, hake es ab."
     "Bleib dabei"
     "Habits die du über längere Zeit durchgehalten hast, bekommen einen ganzen Stern."
@@ -79,28 +82,30 @@
     "8 Stunden"
     "Wähle Wiederholungen durch kurzes Drücken"
     "Bequemer,  verursacht evtl. falsche Auswahl"
-    "Pause interval bei Erinerungen"
+    "Pausen-Interval bei Erinnerungen"
     "Bewerte diese App bei Google Play"
-    "Send dem Entwickler Feedback"
-    "Zeige Quellcode auf GitHub"
+    "Sende dem Entwickler Feedback"
+    "Sehe den Quellcode auf GitHub"
     "Zeige Einleitung"
     "Links"
     "Verhalten"
     "Name"
     "Zeige Archivierte"
     "Einstellungen"
-    "Pause interval"
+    "Pausen-Interval"
     "Wusstest du?"
     "Um Einträge umzusortieren, ziehe sie an die richtige Stelle."
     "Du kannst mehr Tage sehen, wenn du dein Phone quer hälst."
-    "habit löschen"
+    "Gewohnheit löschen"
     "Dies habit wird permanent gelöscht. Dies kann nicht rückgängig gemacht werden."
-    "Wochenende"
-    "Arbeitstage"
+    "An Wochenenden"
+"Wochentage
+"
     "Jeden Tag"
     "Wähle die Tage"
     "Daten exportieren"
-    "Ok"
+"Fertig
+"
     "Löschen"
     "Wähle Stunden"
     "Wähle Minuten"
@@ -108,25 +113,25 @@
     
     "Nimm gute Gewohnheiten an und verfolge deinen Fortschritt (ohne Werbung)"
     "Loop hilft dir gute Gewohnheiten anzunehmen und deine langfristigen Ziele zu erreichen. Detailierte Statistiken zeigen dir, wie du dich entwickelt hast. Es ist ohne Werbung und Open Source."
-"<b>Einfaches, schönes und modernes Oberfläche</b>
-Loop hat eine minimale Oberfläche und ist deshalb einfach zu nutzen. Es folgt dem material Design."
+"<b>Einfache, schöne und moderne Oberfläche</b>
+Loop hat eine minimale Oberfläche und ist deshalb einfach zu benutzen. Es folgt den Material Design Richtlinien."
 "<b>Habit Punkte</b>
-Um dir deine Schwächen zu zeigen, hat Loop einen Algorithmus, um deine starken Angewohnheiten zu erkennen. Jede Wiederholung verstärkt diese und jedes Aussetzen schwächt sie. Aber ein paar Verfehlungen nach langem Durchhalten machen natürlich nicht gleich alles zu nichte."
+Um dir deine kleinen Schwächen zu zeigen, hat Loop einen Algorithmus, um deine Angewohnheiten zu erkennen. Jede Wiederholung verstärkt diese und jedes Aussetzen schwächt sie. Aber ein paar Verfehlungen nach langem Durchhalten machen natürlich nicht gleich alles zu nichte."
 "<b>Statistiken</b>
 Schau dir an, wie sich deine Angewohnheiten im Laufe der Zeit gemacht haben. Schau auf die schönen Diagramme und gehe zurück im gesamten Verlauf."
 "<b>Flexible Zeiten</b>
-Unterstützt sowohl tägliche Vorgaben, als auch komplexere Pläne, woe etwa 3 mal pro Woche; ein mal in jeder anderen Woche; oder jeden anderen Tag."
+Unterstützt sowohl tägliche Vorgaben, als auch komplexere Pläne, wie etwa 3 mal pro Woche; ein mal in jeder anderen Woche; oder jeden anderen Tag."
 "<b>Erinnerungen</b>
-Erstelle individuelle Erinnerungen und wann diese dich benachrichtigen sollen. Kontrolliere deine Vorhaben ganz einfach und lehne sie bei Bedarf direkt ab, ohne in die App zu wechseln."
+Erstelle individuelle Erinnerungen und wann diese dich benachrichtigen sollen. Kontrolliere deine Vorhaben ganz einfach und lehne sie bei Bedarf direkt ab, ohne die App zu öffnen."
 "<b>Komplett werbefrei und Open Source</b>
 Es gibt absolut keine Werbung, nervende Einblendungen oder merkwürdige Berechtigungen in dieser App und das wird auch so bleiben. Der komplette Quellcode steht unter der GPLv3."
 "<b>Optimiert für Smartwatches</b>
-Erinnerungen können direkt von der Android Wear watch kontrolliert, pausiert, oder verschoben werden."
+Erinnerungen können direkt von deiner Android Wear Watch kontrolliert, pausiert oder verschoben werden."
     "Über"
     "Übersetzer"
     "Entwickler"
 
     
     "Version %s"
-    Frequenz
+    "Frequenz"
 
\ No newline at end of file

From 0cdde4901efdf45bf768838b045c285ce5626043 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 15:38:29 -0400
Subject: [PATCH 027/175] Add Italian translation

---
 app/src/main/res/values-it/strings.xml | 136 +++++++++++++++++++++++++
 1 file changed, 136 insertions(+)
 create mode 100644 app/src/main/res/values-it/strings.xml

diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
new file mode 100644
index 000000000..1967eec81
--- /dev/null
+++ b/app/src/main/res/values-it/strings.xml
@@ -0,0 +1,136 @@
+
+
+
+
+
+    
+    "Loop"
+    "Abitudine"
+    "Impostazioni"
+    "Modifica"
+    "Elimina"
+    "Archivia"
+    "Ripristina"
+    "Aggiungi abitudine"
+    "Cambia il colore"
+    "Abitudine creata."
+    "Abitudine rimossa."
+    "Abitudine ripristinata."
+    "Niente da annullare."
+    "Niente da ripetere."
+    "Abitudine modificata."
+
+    
+    "Abitudine ripristinata."
+    "Abitudine archiviata."
+    "Abitudine ripristinata."
+    "Panoramica"
+    "Forza dell'abitudine"
+    "Cronologia"
+    "Pulisci"
+    "Domanda (Hai ... oggi?)"
+
+    
+    "Ripetizione"
+    "volte in"
+    "giorni"
+    "Promemoria"
+    "Annulla"
+    "Salva"
+    "Serie"
+    "Non hai abitudini attive"
+
+    
+    "Premi e tieni premuto per completare o annullare"
+    "Off"
+    "Il nome non può essere vuoto."
+    "I numeri devono essere positivi."
+    "Puoi avere al massimo una ripetizione al giorno"
+    "Crea abitudine"
+    "Modifica abitudine"
+
+    
+    "Completa"
+    "Più tardi"
+
+    
+    "Benvenuto"
+    "Loop Habit Tracker ti aiuta a creare e mantenere delle buone abitudini."
+    "Aggiungi qualche nuova abitudine"
+    "Ogni giorno, dopo aver portato a termine la tua abitudine, spuntala nell'app."
+    "Continua così"
+    "Abitudini portate a termine con regolarità per un lungo periodo ti faranno guadagnare una stella intera."
+    "Segui i tuoi progressi"
+    "Grafici dettagliati ti mostrano come le tue abitudini sono migliorate nel corso del tempo."
+    "15 minuti"
+    "30 minuti"
+    "1 ora"
+    "2 ore"
+    "4 ore"
+    "8 ore"
+    "Spunta le ripetizioni velocemente"
+    "Più comodo, ma potrebbe causare delle spunte accidentali."
+    "Intervallo di ritardo dei promemoria"
+    "Valuta quest'app su Google Play"
+    "Manda un feedback allo sviluppatore"
+    "Vedi il codice sorgente su GitHub"
+    "Visualizza l'introduzione dell'app"
+    "Links"
+    "Comportamento"
+    "Nome"
+    "Visualizza archiviati"
+    "Impostazioni"
+    "Snooze"
+    "Lo sapevi?"
+    "Per riordinare la lista, premi e mantieni premuto l'abitudine e spostala nella posizione desiderata."
+    "Puoi vedere più giorni mettendo il tuo telefono orizzontale."
+    "Elimina abitudine"
+    "L'abitudine verrà cancellata definitivamente. Non sarà possibile annullare."
+    "Weekend"
+    "Giorni feriali"
+    "Qualsiasi giorno"
+    "Giorni selezionati"
+    "Esporta i dati"
+    "Fatto"
+    "Pulisci"
+    "Ore selezionate"
+    "Minuti selezionati"
+
+    
+    "Acquisisci nuove abitudini e traccia il tuo progresso nel tempo (senza pubblicità)"
+    "Loop ti aiuta a creare e mantenere buone abitudini, permettendoti di raggiungere i tuoi obbiettivi a lungo termine. Grafici dettagliati e le statistiche ti mostrano come le tue abitudini sono migliorate durante il tempo. E' completamente senza pubblicità ed opensource."
+    "<b>Interfaccia semplice e moderna</b> Loop ha un'interfaccia minimale che è semplice da usare e segue le linee guida del Material Design"
+"<b>Forza dell'abitudine</b>
+In aggiunta al traguardo attuale, Loop ha un algoritmo avanzato per calcolare la forza delle tue abitudini. Ogni ripetizione la rafforza, mentre ogni giorno mancato la indebolisce. Pochi giorni mancati dopo una lunga serie però non vanificherà completamente il tuo progresso totale."
+    "<b>Grafici dettagliati e statistiche</b> Visualizza in modo semplice come le tue abitudini sono migliorate nel tempo con grafici dettagliati. Scorri indietro per vedere la cronologia completa delle tue abitudini."
+    "<b>Programmi flessibili</b> Supporto per abitudini sia giornaliere che con organizzazioni più complesse, come 3 volte alla settimana; una volta ogni 2 settimane; ogni due giorni..."
+"<b>Promemoria</b>
+Crea un promemoria per ogni abitudine, ad una specificata ora del giorno. Completa, ritarda o ignora il promemoria direttamente dalla notifica, senza aprire l'app."
+"<b>Completamente gratuito ed opensource</b>
+Non ci sono pubblicità, notifiche invasive o permessi intrusivi e mai ce ne saranno. Il codice sorgente completo è disponibile sotto licenza GPLv3."
+    "<b>Ottimizzata per gli smartwatch</b> I promemoria possono essere completati, ritardati o ignorati direttamente dal tuo orologio Android Wear."
+    "A proposito di"
+    "Traduttori"
+    "Sviluppatori"
+
+    
+    "Versione %s"
+    "Frequenza"
+
\ No newline at end of file

From ddd10cacd1f68c6dfb0f81a82dd71b31bd10812a Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 15:38:42 -0400
Subject: [PATCH 028/175] Add Polish translation

---
 app/src/main/res/values-pl/strings.xml | 133 +++++++++++++++++++++++++
 1 file changed, 133 insertions(+)
 create mode 100644 app/src/main/res/values-pl/strings.xml

diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
new file mode 100644
index 000000000..b4c9108ad
--- /dev/null
+++ b/app/src/main/res/values-pl/strings.xml
@@ -0,0 +1,133 @@
+
+
+
+    "Śledzenie Nawyków Loop"
+    "Nawyki"
+    "Ustawienia"
+    "Edytuj"
+    "Usuń"
+    "Archiwizuj"
+    "Odarchiwizuj"
+    "Dodaj nawyk"
+    "Zmień kolor"
+    "Utworzono nawyk."
+    "Usunięto nawyki."
+    "Przywrócono nawyki."
+    "Nic do cofnięcia."
+    "Nic do powtórzenia"
+    "Zmieniono nawyk."
+
+    
+    "Zmieniono nawyk spowrotem."
+    "Nawyki zarchiwizowane."
+    "Nawyki odarchiwizowane."
+    "Przegląd"
+    "Siła nawyku"
+    "Historia"
+    "Wyczyść"
+    "Pytanie (Czy zrobiłeś ... dzisiaj?)"
+
+    
+    "Powtórz"
+    "razy w"
+    "dni"
+    "Przypomnienie"
+    "Odrzuć"
+    "Zapisz"
+    "Serie"
+    "Nie masz aktywnych nawyków"
+    "Naciśnij i przytrzymaj aby zaznaczyć lub odznaczyć"
+    "Wyłączony"
+    "Nazwa nie może być pusta."
+    "Liczba musi być dodatnia."
+    "Możesz mieć maksymalnie jedno powtórzenie dziennie."
+    "Utwórz nawyk"
+    "Edytuj nawyk"
+    "Zaznacz"
+    "Później"
+
+    
+    "Witaj"
+    "Śledzenie nawyków Loop pozwala Ci na utworzenie i prowadzenie dobrych nawyków."
+    "Utwórz nowe nawyki"
+    "Codziennie, po wykonaniu swojego nawyku, postaw znaczek w aplikacji."
+    "Kontynuuj swoje nawyki"
+    "Nawyki wykonywane przez dłuższy czas otrzymają pełną gwiazdkę."
+    "Śledź swój postęp"
+    "Szczegółowe grafiki pokazują jak Twoje nawyki polepszyły się z biegiem czasu."
+    "15 minut"
+    "30 minut"
+    "1 godzina"
+    "2 godziny"
+    "4 godziny"
+    "8 godzin"
+    "Przełącz powtarzanie przy krótkim naciśnięciu"
+    "Wygodniejsze ale może spowodować przypadkowe przełączenia."
+    "Czas drzemki między przypomnieniami"
+    "Oceń tą aplikację w Google Play"
+    "Prześlij uwagi do programisty"
+    "Zobacz kod źródłowy na GitHub'ie"
+    "Zobacz wprowadzenie do aplikacji"
+    "Linki"
+    "Zachowanie"
+    "Nazwa"
+    "Pokaż zarchiwizowane"
+    "Ustawienia"
+    "Czas drzemki"
+    "Czy wiesz że?"
+    "Aby zmienić kolejność naciśnij i przytrzymaj na nazwie nawyku i przesuń go na odpowiednie miejsce."
+    "Możesz zobaczyć więcej dni trzymając telefon poziomo."
+    "Usuń Nawyki"
+    "Nawyki zostaną trwale usunięte. Tej operacji nie można cofnąć."
+    "Weekendy"
+    "Dni robocze"
+    "Każdy dzień"
+    "Wybierz dni"
+    "Eksportuj dane"
+    "Gotowe"
+    "Wyczyść"
+    "Wybierz godziny"
+    "Wybierz minuty"
+
+    
+    "Twórz dobre nawyki i śledź ich postęp (bez reklam)"
+    "Loop pozwala Ci na tworzenie i utrzymywanie dobrych nawyków, pozwala na Ci na osiągnięcie Twoich długoterminowych celów. Szczegółowe grafiki i statystyki pozwalają na zobaczenie jak Twoje nawyki polepszyły się. Jest całkowicie wolna od reklam i open source."
+"<b>Prosty, piękny i nowoczesny interfejs</b>
+Loop posiada minimalistyczny interfejs, który jest prosty do użycia i przestrzega zasad material design."
+"<b>Punkty nawyku</b>
+Oprócz pokazywania Twojej aktualnej serii, Loop posiada zaawansowany algorytm obliczania siły Twoich nawyków. Każde powtórzenie nawyku czyni go silniejszymi a każdy opuszczony dzień słabszym. Jednakże kilka opuszczonych dni po dłuższej serii nie zrujnuje Twojego całego postępu."
+"<b>Szczegółowe grafiki i statystyki</b>
+Zobacz jak Twoje nawyki ulepszają się poprzez piękne i szczegółowe wykresy. Przewiń do tyłu aby zobaczyć pełną historię Twoich nawyków."
+"<b>Elastyczne plany</b>
+Wspiera zarówno codzienne nawyki jak i nawyki z bardziej złożonym planem, jakie jak 3 razy co tydzień; raz w ciągu innego tygodnia; lub każdego innego dnia."
+"<b>Przypomnienia</b>
+Utwórz indywidualne przypomnienia dla każdego nawyku, w określonej godzinie dnia. Łatwo sprawdź, usuń i uśpij powiadomienia o nawyku bezpośrednio z powiadomienia, bez otwierania aplikacji."
+"<b>Całkowicie bez reklam i open source</b>
+Absolutnie nie ma żadnych reklam, denerwujących powiadomień czy szpiegujących uprawnień w tej aplikacji i nigdy nie będzie. Cały kod źródłowy jest dostępny pod licencją GPLv3."
+"<b>Zoptymalizowana pod smartwatche</b>
+Przypomnienia można sprawdzić, uśpić czy usunąć bezpośrednio z twojego zegarka Android Wear."
+    "O aplikacji"
+    "Tłumacze"
+    "Programiści"
+
+    
+    "Wersja %s"
+    "Częstotliwość"
+
\ No newline at end of file

From 2eef696027357d7ad2cde358fcc46110720871d0 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 15:46:30 -0400
Subject: [PATCH 029/175] Add Russian translation

---
 app/src/main/res/values-ru/strings.xml | 134 +++++++++++++++++++++++++
 1 file changed, 134 insertions(+)
 create mode 100644 app/src/main/res/values-ru/strings.xml

diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
new file mode 100644
index 000000000..0ae126809
--- /dev/null
+++ b/app/src/main/res/values-ru/strings.xml
@@ -0,0 +1,134 @@
+
+
+
+
+    "Трекер привычек Loop"
+    "Привычки"
+    "Настройки"
+    "Редактировать"
+    "Удалить"
+    "Архивировать"
+    "Вернуть из архива"
+    "Добавить привычку"
+    "Изменить цвет"
+    "Привычка создана."
+    "Привычки удалены."
+    "Привычки восстановлены."
+    "Отменять нечего."
+    "Повторять нечего."
+    "Привычка изменена."
+
+    
+    "Изменения привычки отменены."
+    "Привычки архивированы."
+    "Привычки возвращены из архива."
+    "Обзор"
+    "Сила привычки"
+    "История"
+    "Очистить"
+    "Вопрос (пример: \"Делали ли вы сегодня зарядку?\")"
+
+    
+    "Повторять"
+    "раза за"
+    "дней"
+    "Напоминание"
+    "Отменить"
+    "Сохранить"
+    "Рекорды"
+    "У вас нет активных привычек"
+    "Нажмите и удерживайте, чтобы установить или снять галочку"
+    "Выкл"
+    "Название не может быть пустым."
+    "Число должно быть положительным."
+    "Может быть не более одного повторения в день"
+    "Добавить привычку"
+    "Изменить привычку"
+    "Отметить"
+    "Отложить"
+
+    
+    "Добро пожаловать"
+    "Loop Habit Tracker помогает вам заводить и поддерживать полезные привычки."
+    "Добавьте несколько привычек"
+    "Каждый день, после выполнения вашей привычки, поставьте галочку в приложении."
+    "Продолжайте в том же духе"
+    "Стойко соблюдаемые привычки будут отмечены полной звёздочкой."
+    "Отслеживайте свои успехи"
+    "Детализированные диаграммы демонстрируют, как ваши привычки улучшились со временем."
+    "15 минут"
+    "30 минут"
+    "1 час"
+    "2 часа"
+    "4 часа"
+    "8 часов"
+    "Отмечать повторение коротким нажатием"
+    "Удобнее, но может стать причиной случайных нажатий."
+    "Интервал откладывания напоминаний"
+    "Оценить приложение на Google Play"
+    "Отправить сообщение разработчику"
+    "Посмотреть исходный код на GitHub"
+    "Посмотреть вступительные инструкции"
+    "Ссылки"
+    "Поведение"
+    "Название"
+    "Показать архивированные"
+    "Настройки"
+    "Интервал откладывания"
+    "А вы знали?"
+    "Чтобы поменять порядок записей, нажмите и удерживайте название записи, затем перетащите её на нужное место."
+    "В горизонтальном режиме отображается больше дней."
+    "Удалить привычки"
+    "Привычки будут удалены. Это действие невозможно отменить."
+    "Выходные"
+    "Будни"
+    "Каждый день"
+    "Выберите дни"
+    "Экспортировать данные"
+    "Готово"
+    "Очистить"
+    "Выберите часы"
+    "Выберите минуты"
+
+    
+    "Заводите полезные привычки и отслеживайте свои успехи (без рекламы)"
+    "\"Трекер привычек Loop\" поможет вам завести и поддерживать полезные привычки, позволяя достичь долгосрочных целей. Детализированные диаграммы и статистика покажут, как ваши привычки закрепились со временем. Приложение не содержит рекламы и является ПО с открытым исходным кодом."
+"<b>Простой, красивый и современный интерфейс</b>
+У приложения минималистичный интерфейс, который лёгок в использовании и придерживается руководства по Material Design."
+"<b>Оценка привычек</b>
+В дополнение к отображению текущего рекорда повторений, приложение имеет передовой алгоритм расчёта \"силы\" ваших привычек. Каждое повторение делает вашу привычку \"сильнее\", а каждый пропуск \"слабее\". Несколько пропущенных дней после долгих успешных повторений, однако, не загубят ваши успехи полностью."
+"<b>Детализированные диаграммы и статистика</b>
+При помощи красивых и детализированных диаграмм вы с лёгкостью можете просмотреть, как ваши привычки закрепились со временем. Просмотрите полную историю ваших привычек, пролистав её."
+"<b>Гибкий график</b>
+Поддерживаются как ежедневные привычки, так и привычки с более сложным графиком. Например: 3 раза в неделю; один раз каждую неделю; через день."
+"<b>Напоминания</b>
+Создавайте отдельные для каждой привычки напоминания в выбранное время дня. С лёгкостью отмечайте, пропускайте или откладывайте выполнение привычки прямо из уведомления - без открытия приложения."
+"<b>Полное отсутствие рекламы и открытый исходный код</b>
+В приложении совершенно нет рекламы, назойливых уведомлений или лишних разрешений. И их никогда не будет. Весь исходный код доступен под лицензией GPLv3."
+"<b>Оптимизировано для \"умных\" часов</b>
+Напоминания могут быть отмечены, отложены или пропущены прямо с ваших часов Android Wear."
+    "О приложении"
+    "Переводчики"
+    "Разработчики"
+
+    
+    "Версия %s"
+    "Частота"
+
\ No newline at end of file

From e01c668e4d90541e715420683d0f3f9e4deda6d0 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 15:46:40 -0400
Subject: [PATCH 030/175] Add Swedish translation

---
 app/src/main/res/values-sv/strings.xml | 139 +++++++++++++++++++++++++
 1 file changed, 139 insertions(+)
 create mode 100644 app/src/main/res/values-sv/strings.xml

diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
new file mode 100644
index 000000000..99785590c
--- /dev/null
+++ b/app/src/main/res/values-sv/strings.xml
@@ -0,0 +1,139 @@
+
+
+
+
+    "Loop Habit Tracker"
+    "Vanor"
+    "Inställningar"
+    "Redigera"
+    "Ta bort"
+    "Arkivera"
+    "Avarkivera"
+    "Lägg till vana"
+    "Byt färg"
+    "Vana skapad"
+    "Vana borttagen"
+    "Vana återställd"
+    "Inget att ångra"
+    "Inget att göra om"
+    "Vana ändrad"
+
+    
+    "Vana ändrad tillbaka"
+    "Vanor arkiverade"
+    "Vanor avakriverade"
+    "Översikt"
+    "Vanestyrka"
+    "Historik"
+    "Rensa"
+    "Fråga (Har du ... idag?)"
+
+    
+    "Repetera"
+
+    
+    "gånger varje"
+    "dagar"
+    "Påminnelse"
+    "Kasta bort"
+    "Spara"
+    "Framgångar"
+    "Du har inga aktiva vanor"
+    "Tryck och håll in för att markera och avmarkera"
+    "Av"
+    "Namn får inte vara blank."
+    "Nummer måste vara positiv."
+    "Du har som mest en repetition per dag"
+    "Skapa vana"
+    "Redigera vana"
+    "Markera"
+    "Senare"
+
+    
+    "Välkommen"
+    "Loop Habit Tracker Hjälper dig att skapa och underhålla bra vanor."
+    "Skapa några nya vanor"
+    "Varje dag, efter att du utfört dina vanor, lägg till en markering i appen."
+    "Fortsätt göra det"
+    "Vanor som utförs konstant över en lång period kommer att tjäna dig en full stjärna."
+    "Följ dina framsteg"
+    "Detaljerade grafer visar dig hur dina vanor har utvecklats över tid."
+    "15 minuter"
+    "30 minuter"
+    "1 timme"
+    "2 timmar"
+    "4 timmar"
+    "8 timmar"
+    "Aktivera repetitioner med kort tryckning"
+    "Mer bekvämt, men kan orsaka oavsiktliga aktiveringar. "
+    "Snooza intervall på påminnelsen"
+    "Betygsätt denna app på Google Play"
+    "Skicka feedback till utvecklaren"
+    "Visa källkod på Github"
+    "Visa appintroduktion"
+    "Länkar"
+    "Beteende"
+    "Namn"
+    "Visa arkiverade"
+    "Inställningar"
+    "Snooze intervall"
+    "Visste du? "
+    "För att arrangera inlägg, tryck och håll in på namnet av inlägget, dra den sedan till rätt plats."
+    "Du kan se fler dagar genom att hålla telefon i landskapsläge."
+    "Ta bort vanor"
+    "Vanorna kommer att tas bort permanent. Denna åtgärd kan inte ångras."
+    "Helger"
+    "Veckodagar"
+    "Vilken dag som helst"
+    "Välj dagar"
+    "Exportera data"
+    "Klart"
+    "Rensa"
+    "Välj timmar"
+    "Välj minuter"
+
+    
+    "Skapa bra vanor och följ deras utveckling över tid. (reklamfri)"
+    "Loop Hjälper dig att skapa och underhålla bra vanor, genom att tillåta dig arkivera dina långtidsvanor. Detaljerade grafer och statistik visar dig hur dina vanor har förbättrats över tid. Det är helt reklamfritt och öppen källkod."
+"<b>Enkelt, snyggt och modernt användargränssnitt<b>
+Loop har minimalistiskt gränssnitt som är enkel att använda och följer material design riktlinjer."
+
+    
+"<b>Vanepoäng<b>
+Förutom att visa din nuvarande aktivitet har Loop en avancerad algoritm som räknar ut styrkan i dina vanor. Varje repetition gör din vana starkare och varje missad dag gör den svagare. Ett par missar efter en lång, oavbruten aktivitet kommer dock inte att helt förstöra dina framsteg."
+"<b>Detaljerade grafer och statistik</b>
+Tydligt se hur dina vanor förbättras över tiden med fina och detaljerade grafer. Skrolla tillbaka för att din fullständiga historik av dina vanor."
+"<b>Flexibelt schema</b>
+Stödjer både dagliga vanor och avancerade vanor, som till exempel tre gånger per vecka, en gång varje vecka eller varannan dag."
+"<b>Påminnelser</b>
+Skapa en påminnelse för varje vana. Du kan enkelt bocka av, ta bort eller skjuta upp din vana direkt från notifikationsfältet."
+"<b>Helt reklamfri och öppen källkod</b>
+Det är absolut ingen reklam, störande notifikationer eller onödiga behörighetskrav för den här appen, och det kommer aldrig att bli. Hela källkoden finns tillgänglig under GPLv3."
+"<b>Optimerad för smartklockor<b>
+Påminnelser kan kollas upp, snoozade eller tas bort direkt från din Android Wear enhet."
+    "Om"
+    "Översättare"
+    "Utvecklare"
+
+    
+    "Version %s"
+    "Frekvens"
+
\ No newline at end of file

From dfbcf78dd7332057bc3cde4f51623374e9619d50 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 15:46:52 -0400
Subject: [PATCH 031/175] Update list of translators

---
 app/src/main/res/layout/about.xml | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml
index 3e0e8f713..c72358343 100644
--- a/app/src/main/res/layout/about.xml
+++ b/app/src/main/res/layout/about.xml
@@ -125,6 +125,34 @@
                 style="@style/aboutItemStyle"
                 android:text="Al Alloush (العَرَبِية‎)"/>
 
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
+            
+
             

From babf7d64f0b5e1e0e783d361b5056dcd0c1a66a4 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 19:07:32 -0400
Subject: [PATCH 032/175] Display repetition count for last week, month, etc

Closes #21
---
 .../java/org/isoron/helpers/DialogHelper.java |  30 +++-
 .../uhabits/fragments/ShowHabitFragment.java  |  33 +++-
 .../isoron/uhabits/models/RepetitionList.java |  12 ++
 .../isoron/uhabits/views/HabitDataView.java   |  29 ++++
 .../uhabits/views/HabitFrequencyView.java     |   2 +-
 .../uhabits/views/HabitHistoryView.java       |   2 +-
 .../isoron/uhabits/views/HabitScoreView.java  |   2 +-
 .../isoron/uhabits/views/HabitStreakView.java |   2 +-
 .../org/isoron/uhabits/views/NumberView.java  | 155 ++++++++++++++++++
 .../uhabits/views/RepetitionCountView.java    |  78 +++++++++
 app/src/main/res/layout/show_habit.xml        |  66 +++++++-
 11 files changed, 391 insertions(+), 20 deletions(-)
 create mode 100644 app/src/main/java/org/isoron/uhabits/views/HabitDataView.java
 create mode 100644 app/src/main/java/org/isoron/uhabits/views/NumberView.java
 create mode 100644 app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java

diff --git a/app/src/main/java/org/isoron/helpers/DialogHelper.java b/app/src/main/java/org/isoron/helpers/DialogHelper.java
index e65a50430..ebaf6c9b5 100644
--- a/app/src/main/java/org/isoron/helpers/DialogHelper.java
+++ b/app/src/main/java/org/isoron/helpers/DialogHelper.java
@@ -27,6 +27,7 @@ import android.os.Vibrator;
 import android.preference.PreferenceManager;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
+import android.util.TypedValue;
 import android.view.View;
 import android.view.inputmethod.InputMethodManager;
 
@@ -80,16 +81,35 @@ public abstract class DialogHelper
     {
         int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0);
 
-        if(resId != 0)
-            return context.getResources().getString(resId);
-        else
-            return attrs.getAttributeValue(ISORON_NAMESPACE, name);
+        if (resId != 0) return context.getResources().getString(resId);
+        else return attrs.getAttributeValue(ISORON_NAMESPACE, name);
+    }
+
+    public static int getIntAttribute(Context context, AttributeSet attrs, String name)
+    {
+        String number = getAttribute(context, attrs, name);
+        if(number != null) return Integer.parseInt(number);
+        else return 0;
+    }
+
+    public static float getFloatAttribute(Context context, AttributeSet attrs, String name)
+    {
+        String number = getAttribute(context, attrs, name);
+        if(number != null) return Float.parseFloat(number);
+        else return 0;
     }
 
     public static float dpToPixels(Context context, float dp)
     {
         Resources resources = context.getResources();
         DisplayMetrics metrics = resources.getDisplayMetrics();
-        return dp * (metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics);
+    }
+
+    public static float spToPixels(Context context, float sp)
+    {
+        Resources resources = context.getResources();
+        DisplayMetrics metrics = resources.getDisplayMetrics();
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics);
     }
 }
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 6f57abc9c..1b7817afb 100644
--- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java
+++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java
@@ -29,6 +29,7 @@ import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import org.isoron.helpers.ColorHelper;
@@ -41,12 +42,17 @@ import org.isoron.uhabits.dialogs.HistoryEditorDialog;
 import org.isoron.uhabits.helpers.ReminderHelper;
 import org.isoron.uhabits.models.Habit;
 import org.isoron.uhabits.models.Score;
+import org.isoron.uhabits.views.HabitDataView;
 import org.isoron.uhabits.views.HabitHistoryView;
 import org.isoron.uhabits.views.HabitFrequencyView;
 import org.isoron.uhabits.views.HabitScoreView;
 import org.isoron.uhabits.views.HabitStreakView;
+import org.isoron.uhabits.views.RepetitionCountView;
 import org.isoron.uhabits.views.RingView;
 
+import java.util.LinkedList;
+import java.util.List;
+
 public class ShowHabitFragment extends Fragment
         implements DialogHelper.OnSavedListener, HistoryEditorDialog.Listener
 {
@@ -57,6 +63,8 @@ public class ShowHabitFragment extends Fragment
     private HabitHistoryView historyView;
     private HabitFrequencyView punchcardView;
 
+    private List dataViews;
+
     @Override
     public void onStart()
     {
@@ -71,19 +79,28 @@ public class ShowHabitFragment extends Fragment
         activity = (ShowHabitActivity) getActivity();
         habit = activity.habit;
 
+        dataViews = new LinkedList<>();
+
         Button btEditHistory = (Button) view.findViewById(R.id.btEditHistory);
         streakView = (HabitStreakView) view.findViewById(R.id.streakView);
         scoreView = (HabitScoreView) view.findViewById(R.id.scoreView);
         historyView = (HabitHistoryView) view.findViewById(R.id.historyView);
         punchcardView = (HabitFrequencyView) view.findViewById(R.id.punchcardView);
 
+        dataViews.add((HabitStreakView) view.findViewById(R.id.streakView));
+        dataViews.add((HabitScoreView) view.findViewById(R.id.scoreView));
+        dataViews.add((HabitHistoryView) view.findViewById(R.id.historyView));
+        dataViews.add((HabitFrequencyView) view.findViewById(R.id.punchcardView));
+
+        LinearLayout llRepetition = (LinearLayout) view.findViewById(R.id.llRepetition);
+        for(int i = 0; i < llRepetition.getChildCount(); i++)
+            dataViews.add((RepetitionCountView) llRepetition.getChildAt(i));
+
         updateHeaders(view);
         updateScoreRing(view);
 
-        streakView.setHabit(habit);
-        scoreView.setHabit(habit);
-        historyView.setHabit(habit);
-        punchcardView.setHabit(habit);
+        for(HabitDataView dataView : dataViews)
+            dataView.setHabit(habit);
 
         btEditHistory.setOnClickListener(new View.OnClickListener()
         {
@@ -132,6 +149,7 @@ public class ShowHabitFragment extends Fragment
         updateColor(view, R.id.tvStrength);
         updateColor(view, R.id.tvStreaks);
         updateColor(view, R.id.tvWeekdayFreq);
+        updateColor(view, R.id.tvCount);
     }
 
     private void updateColor(View view, int viewId)
@@ -184,10 +202,7 @@ public class ShowHabitFragment extends Fragment
 
     public void refreshData()
     {
-        streakView.refreshData();
-        historyView.refreshData();
-        scoreView.refreshData();
-        punchcardView.refreshData();
-        updateScoreRing(getView());
+        for(HabitDataView view : dataViews)
+            view.refreshData();
     }
 }
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 ebdf35c66..daa889669 100644
--- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
+++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
@@ -178,4 +178,16 @@ public class RepetitionList
 
         return map;
     }
+
+    /**
+     * Returns the total number of repetitions that happened within the specified interval of time.
+     *
+     * @param from beginning of the interval
+     * @param to end of the interval
+     * @return number of repetition in the given interval
+     */
+    public int count(long from, long to)
+    {
+        return selectFromTo(from, to).count();
+    }
 }
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java b/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java
new file mode 100644
index 000000000..f6d3c712a
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java
@@ -0,0 +1,29 @@
+/*
+ * 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.views;
+
+import org.isoron.uhabits.models.Habit;
+
+public interface HabitDataView
+{
+    void setHabit(Habit habit);
+
+    void refreshData();
+}
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java b/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java
index 42c41b64b..9a64aa44b 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java
@@ -39,7 +39,7 @@ import java.util.Locale;
 import java.util.Random;
 import java.util.TimeZone;
 
-public class HabitFrequencyView extends ScrollableDataView
+public class HabitFrequencyView extends ScrollableDataView implements HabitDataView
 {
 
     private Paint pGrid;
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java
index 18bbc1e0e..8a4416bbc 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java
@@ -40,7 +40,7 @@ import java.util.GregorianCalendar;
 import java.util.Locale;
 import java.util.Random;
 
-public class HabitHistoryView extends ScrollableDataView
+public class HabitHistoryView extends ScrollableDataView implements HabitDataView
 {
     private Habit habit;
     private int[] checkmarks;
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java
index 02f596e0b..c64a6101b 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java
@@ -37,7 +37,7 @@ import java.text.SimpleDateFormat;
 import java.util.Locale;
 import java.util.Random;
 
-public class HabitScoreView extends ScrollableDataView
+public class HabitScoreView extends ScrollableDataView implements HabitDataView
 {
     public static final int BUCKET_SIZE = 7;
     public static final PorterDuffXfermode XFERMODE_CLEAR =
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java
index 1fc61cc0c..c4a5115ce 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java
@@ -36,7 +36,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Random;
 
-public class HabitStreakView extends ScrollableDataView
+public class HabitStreakView extends ScrollableDataView implements HabitDataView
 {
     private Habit habit;
     private Paint pText, pBar;
diff --git a/app/src/main/java/org/isoron/uhabits/views/NumberView.java b/app/src/main/java/org/isoron/uhabits/views/NumberView.java
new file mode 100644
index 000000000..53fa64413
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/views/NumberView.java
@@ -0,0 +1,155 @@
+/*
+ * 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.views;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import org.isoron.helpers.ColorHelper;
+import org.isoron.helpers.DialogHelper;
+
+public class NumberView extends View
+{
+
+    private int size;
+    private int color;
+    private int number;
+    private float labelMarginTop;
+    private TextPaint pText;
+    private String label;
+    private RectF rect;
+    private StaticLayout labelLayout;
+
+    private int width;
+    private int height;
+    private float textSize;
+    private float labelTextSize;
+    private float numberTextSize;
+    private StaticLayout numberLayout;
+
+    public NumberView(Context context, AttributeSet attrs)
+    {
+        super(context, attrs);
+
+        this.label = DialogHelper.getAttribute(context, attrs, "label");
+        this.number = DialogHelper.getIntAttribute(context, attrs, "number");
+        this.textSize = DialogHelper.getFloatAttribute(context, attrs, "textSize");
+        this.textSize = DialogHelper.spToPixels(getContext(), textSize);
+        this.color = ColorHelper.palette[7];
+        init();
+    }
+
+    public void setColor(int color)
+    {
+        this.color = color;
+        pText.setColor(color);
+        postInvalidate();
+    }
+
+    public void setLabel(String label)
+    {
+        this.label = label;
+    }
+
+    public void setNumber(int number)
+    {
+        this.number = number;
+        createNumberLayout();
+        postInvalidate();
+    }
+
+    private void init()
+    {
+        pText = new TextPaint();
+        pText.setAntiAlias(true);
+        pText.setTextAlign(Paint.Align.CENTER);
+
+        rect = new RectF();
+    }
+
+    @Override
+    @SuppressLint("DrawAllocation")
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
+    {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        width = MeasureSpec.getSize(widthMeasureSpec);
+        height = MeasureSpec.getSize(heightMeasureSpec);
+
+        labelTextSize = textSize * 0.35f;
+        labelMarginTop = textSize * 0.125f;
+        numberTextSize = textSize;
+
+        createNumberLayout();
+        int numberWidth = numberLayout.getWidth();
+        int numberHeight = numberLayout.getHeight();
+
+        pText.setTextSize(labelTextSize);
+        labelLayout = new StaticLayout(label, pText, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f,
+                false);
+        int labelWidth = labelLayout.getWidth();
+        int labelHeight = labelLayout.getHeight();
+
+        width = Math.max(numberWidth, labelWidth);
+        height = (int) (numberHeight + labelHeight + labelMarginTop);
+
+        setMeasuredDimension(width, height);
+    }
+
+    private void createNumberLayout()
+    {
+        pText.setTextSize(numberTextSize);
+        numberLayout = new StaticLayout(Integer.toString(number), pText, width,
+                Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, false);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas)
+    {
+        super.onDraw(canvas);
+
+        pText.setColor(color);
+        pText.setTextSize(size * 0.4f);
+        rect.set(0, 0, width, height);
+
+        canvas.save();
+        canvas.translate(rect.centerX(), 0);
+        pText.setColor(color);
+        pText.setTextSize(numberTextSize);
+        numberLayout.draw(canvas);
+        canvas.restore();
+
+        canvas.save();
+        pText.setColor(Color.GRAY);
+        pText.setTextSize(labelTextSize);
+        canvas.translate(rect.centerX(), numberLayout.getHeight() + labelMarginTop);
+        labelLayout.draw(canvas);
+        canvas.restore();
+    }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java b/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java
new file mode 100644
index 000000000..f99a2221a
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.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.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import org.isoron.helpers.DateHelper;
+import org.isoron.helpers.DialogHelper;
+import org.isoron.uhabits.models.Habit;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+public class RepetitionCountView extends NumberView implements HabitDataView
+{
+    private int interval;
+    private Habit habit;
+
+    public RepetitionCountView(Context context, AttributeSet attrs)
+    {
+        super(context, attrs);
+        this.interval = DialogHelper.getIntAttribute(context, attrs, "interval");
+        refreshData();
+    }
+
+    @Override
+    public void refreshData()
+    {
+        if(isInEditMode())
+        {
+            setNumber(interval);
+            return;
+        }
+
+        long to = DateHelper.getStartOfToday();
+        long from;
+
+        if(interval == 0)
+        {
+            from = 0;
+        }
+        else
+        {
+            GregorianCalendar fromCalendar = DateHelper.getStartOfTodayCalendar();
+            fromCalendar.add(Calendar.DAY_OF_YEAR, -interval + 1);
+            from = fromCalendar.getTimeInMillis();
+        }
+
+        if(habit != null)
+            setNumber(habit.repetitions.count(from, to));
+    }
+
+    @Override
+    public void setHabit(Habit habit)
+    {
+        this.habit = habit;
+        setColor(habit.color);
+        refreshData();
+    }
+}
diff --git a/app/src/main/res/layout/show_habit.xml b/app/src/main/res/layout/show_habit.xml
index d603b891a..11b5a9194 100644
--- a/app/src/main/res/layout/show_habit.xml
+++ b/app/src/main/res/layout/show_habit.xml
@@ -40,12 +40,74 @@
 
             
 
         
 
+        
+
+            
+
+            
+
+                
+
+                
+
+                
+
+                
+
+            
+
+        
+
         
 
             
Date: Mon, 14 Mar 2016 19:13:33 -0400
Subject: [PATCH 033/175] Externalize strings

---
 app/src/main/res/layout/show_habit.xml | 10 +++++-----
 app/src/main/res/values/strings.xml    |  5 +++++
 2 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/app/src/main/res/layout/show_habit.xml b/app/src/main/res/layout/show_habit.xml
index 11b5a9194..9cd6bbb40 100644
--- a/app/src/main/res/layout/show_habit.xml
+++ b/app/src/main/res/layout/show_habit.xml
@@ -55,7 +55,7 @@
             
+                android:text="@string/repetitions"/>
 
             
 
@@ -80,7 +80,7 @@
                     android:layout_weight="1"
                     android:layout_marginLeft="8dp"
                     android:layout_marginRight="8dp"
-                    app:label="Quarter"
+                    app:label="@string/quarter"
                     app:interval="92"
                     app:textSize="34"/>
 
@@ -90,7 +90,7 @@
                     android:layout_weight="1"
                     android:layout_marginLeft="8dp"
                     android:layout_marginRight="8dp"
-                    app:label="Year"
+                    app:label="@string/year"
                     app:interval="365"
                     app:textSize="34"/>
 
@@ -100,7 +100,7 @@
                     android:layout_weight="1"
                     android:layout_marginLeft="8dp"
                     android:layout_marginRight="8dp"
-                    app:label="All time"
+                    app:label="@string/all_time"
                     app:interval="0"
                     app:textSize="34"/>
 
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 793dec774..75c3bbaa1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -125,4 +125,9 @@
     Frequency
     Checkmark
 
+    Repetitions
+    Month
+    Year
+    Quarter
+    All time
 
\ No newline at end of file

From ded880001710e493d905dea52047556ca4b6697b Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 19:36:05 -0400
Subject: [PATCH 034/175] Extract common style

---
 app/src/main/res/layout/show_habit.xml | 32 +++++++-------------------
 app/src/main/res/values/styles.xml     |  8 +++++++
 2 files changed, 16 insertions(+), 24 deletions(-)

diff --git a/app/src/main/res/layout/show_habit.xml b/app/src/main/res/layout/show_habit.xml
index 9cd6bbb40..80ac17515 100644
--- a/app/src/main/res/layout/show_habit.xml
+++ b/app/src/main/res/layout/show_habit.xml
@@ -65,44 +65,28 @@
                 android:orientation="horizontal">
 
                 
+                    app:textSize="34"
+                    style="@style/repetitionCountStyle"/>
 
                 
+                    app:textSize="34"
+                    style="@style/repetitionCountStyle"/>
 
                 
+                    app:textSize="34"
+                    style="@style/repetitionCountStyle"/>
 
                 
+                    app:textSize="34"
+                    style="@style/repetitionCountStyle"/>
 
             
 
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 628b4226a..b1bd0f44f 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -93,4 +93,12 @@
         12dp
     
 
+    
+
 

From b20fd44cbc04e3c6069c1de8eb1b2ad97269578d Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Mon, 14 Mar 2016 20:48:39 -0400
Subject: [PATCH 035/175] Scroll to view before clicking

---
 app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

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 bcd6b6530..c73e1bc30 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java
@@ -155,10 +155,10 @@ public class MainTest
                 .perform(click());
 
         onView(withId(R.id.scoreView))
-                .perform(swipeRight());
+                .perform(scrollTo(), swipeRight());
 
         onView(withId(R.id.punchcardView))
-                .perform(scrollTo());
+                .perform(scrollTo(), swipeRight());
     }
 
     @Test

From 9232378d04e53d2298d2a66bbbc9df4db6691eb3 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Tue, 15 Mar 2016 05:30:27 -0400
Subject: [PATCH 036/175] Refactor RingView; make text size consistent

---
 .../org/isoron/uhabits/views/NumberView.java  |  6 +--
 .../org/isoron/uhabits/views/RingView.java    | 49 ++++++++++++-------
 app/src/main/res/layout/show_habit.xml        | 44 ++++++++++-------
 app/src/main/res/values/styles.xml            |  2 +-
 4 files changed, 63 insertions(+), 38 deletions(-)

diff --git a/app/src/main/java/org/isoron/uhabits/views/NumberView.java b/app/src/main/java/org/isoron/uhabits/views/NumberView.java
index 53fa64413..32cf2f3aa 100644
--- a/app/src/main/java/org/isoron/uhabits/views/NumberView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/NumberView.java
@@ -102,9 +102,9 @@ public class NumberView extends View
         width = MeasureSpec.getSize(widthMeasureSpec);
         height = MeasureSpec.getSize(heightMeasureSpec);
 
-        labelTextSize = textSize * 0.35f;
-        labelMarginTop = textSize * 0.125f;
-        numberTextSize = textSize;
+        labelTextSize = textSize;
+        labelMarginTop = textSize * 0.35f;
+        numberTextSize = textSize * 2.85f;
 
         createNumberLayout();
         int numberWidth = numberLayout.getWidth();
diff --git a/app/src/main/java/org/isoron/uhabits/views/RingView.java b/app/src/main/java/org/isoron/uhabits/views/RingView.java
index 1e05e00d3..9e54c88ff 100644
--- a/app/src/main/java/org/isoron/uhabits/views/RingView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/RingView.java
@@ -19,6 +19,7 @@
 
 package org.isoron.uhabits.views;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -32,12 +33,9 @@ import android.view.View;
 
 import org.isoron.helpers.ColorHelper;
 import org.isoron.helpers.DialogHelper;
-import org.isoron.uhabits.R;
 
 public class RingView extends View
 {
-
-    private int size;
     private int color;
     private float percentage;
     private float labelMarginTop;
@@ -46,12 +44,22 @@ public class RingView extends View
     private RectF rect;
     private StaticLayout labelLayout;
 
+    private int width;
+    private int height;
+    private float diameter;
+    private float maxDiameter;
+    private float textSize;
+
     public RingView(Context context, AttributeSet attrs)
     {
         super(context, attrs);
 
-        this.size = (int) context.getResources().getDimension(R.dimen.small_square_size) * 4;
         this.label = DialogHelper.getAttribute(context, attrs, "label");
+        this.maxDiameter = DialogHelper.getFloatAttribute(context, attrs, "maxDiameter");
+        this.textSize = DialogHelper.getFloatAttribute(context, attrs, "textSize");
+
+        this.maxDiameter = DialogHelper.dpToPixels(context, maxDiameter);
+        this.textSize = DialogHelper.spToPixels(context, textSize);
         this.color = ColorHelper.palette[7];
         this.percentage = 0.75f;
         init();
@@ -77,21 +85,27 @@ public class RingView extends View
         pRing.setColor(color);
         pRing.setTextAlign(Paint.Align.CENTER);
 
-        pRing.setTextSize(size * 0.15f);
-        labelMarginTop = size * 0.10f;
-        labelLayout = new StaticLayout(label, pRing, size, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f,
-                false);
-
         rect = new RectF();
     }
 
     @Override
+    @SuppressLint("DrawAllocation")
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
     {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
-        int width = Math.max(size, labelLayout.getWidth());
-        int height = (int) (size + labelLayout.getHeight() + labelMarginTop);
+        width = MeasureSpec.getSize(widthMeasureSpec);
+        height = MeasureSpec.getSize(heightMeasureSpec);
+
+        diameter = Math.min(maxDiameter, width);
+
+        pRing.setTextSize(textSize);
+        labelMarginTop = textSize * 0.80f;
+        labelLayout = new StaticLayout(label, pRing, width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f,
+                false);
+
+        width = Math.max(width, labelLayout.getWidth());
+        height = (int) (diameter + labelLayout.getHeight() + labelMarginTop);
 
         setMeasuredDimension(width, height);
     }
@@ -100,10 +114,11 @@ public class RingView extends View
     protected void onDraw(Canvas canvas)
     {
         super.onDraw(canvas);
-        float thickness = size * 0.15f;
+        float thickness = diameter * 0.15f;
 
         pRing.setColor(color);
-        rect.set(0, 0, size, size);
+        rect.set(0, 0, diameter, diameter);
+        rect.offset((width - diameter) / 2, 0);
         canvas.drawArc(rect, -90, 360 * percentage, true, pRing);
 
         pRing.setColor(Color.rgb(230, 230, 230));
@@ -113,14 +128,14 @@ public class RingView extends View
         rect.inset(thickness, thickness);
         canvas.drawArc(rect, -90, 360, true, pRing);
 
-        float lineHeight = pRing.getFontSpacing();
         pRing.setColor(Color.GRAY);
-        pRing.setTextSize(size * 0.2f);
+        pRing.setTextSize(diameter * 0.2f);
+        float lineHeight = pRing.getFontSpacing();
         canvas.drawText(String.format("%.0f%%", percentage * 100), rect.centerX(),
                 rect.centerY() + lineHeight / 3, pRing);
 
-        pRing.setTextSize(size * 0.15f);
-        canvas.translate(size / 2, size + labelMarginTop);
+        pRing.setTextSize(textSize);
+        canvas.translate(width / 2, diameter + labelMarginTop);
         labelLayout.draw(canvas);
     }
 }
diff --git a/app/src/main/res/layout/show_habit.xml b/app/src/main/res/layout/show_habit.xml
index 80ac17515..83eae1e52 100644
--- a/app/src/main/res/layout/show_habit.xml
+++ b/app/src/main/res/layout/show_habit.xml
@@ -31,18 +31,28 @@
 
         
+            android:gravity="start">
 
             
 
-            
+                android:layout_gravity="center"
+                android:orientation="horizontal">
+
+                
+
+            
 
         
 
@@ -65,28 +75,28 @@
                 android:orientation="horizontal">
 
                 
+                    app:label="@string/month"
+                    app:textSize="12"/>
 
                 
+                    app:label="@string/quarter"
+                    app:textSize="12"/>
 
                 
+                    app:label="@string/year"
+                    app:textSize="12"/>
 
                 
+                    app:label="@string/all_time"
+                    app:textSize="12"/>
 
             
 
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index b1bd0f44f..f897ed590 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -93,7 +93,7 @@
         12dp
     
 
-    

From 075b7812eb275ec4f84d29a136484c7d72d008f2 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Wed, 16 Mar 2016 07:29:10 -0400
Subject: [PATCH 038/175] Refactor and write documentation for Habit

---
 .../isoron/uhabits/unit/models/HabitTest.java |  46 ++++
 .../java/org/isoron/helpers/DateHelper.java   |   2 +
 .../java/org/isoron/helpers/DialogHelper.java |   1 +
 .../isoron/helpers/ReplayableActivity.java    |   1 +
 .../commands/ArchiveHabitsCommand.java        |   7 +-
 .../commands/ChangeHabitColorCommand.java     |  18 +-
 .../commands}/Command.java                    |   2 +-
 .../commands/CommandFailedException.java      |  33 +++
 .../uhabits/commands/CreateHabitCommand.java  |   6 +-
 .../uhabits/commands/DeleteHabitsCommand.java |   1 -
 .../uhabits/commands/EditHabitCommand.java    |   5 +-
 .../commands/ToggleRepetitionCommand.java     |   1 -
 .../commands/UnarchiveHabitsCommand.java      |   7 +-
 .../uhabits/fragments/EditHabitFragment.java  |  31 ++-
 .../uhabits/fragments/ListHabitsFragment.java |   2 +-
 .../uhabits/fragments/ShowHabitFragment.java  |   2 +-
 .../uhabits/helpers/ReminderHelper.java       |   7 +-
 .../java/org/isoron/uhabits/models/Habit.java | 215 +++++++++++++++++-
 18 files changed, 329 insertions(+), 58 deletions(-)
 rename app/src/main/java/org/isoron/{helpers => uhabits/commands}/Command.java (96%)
 create mode 100644 app/src/main/java/org/isoron/uhabits/commands/CommandFailedException.java

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 d46826974..f31db5f35 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
@@ -19,6 +19,7 @@
 
 package org.isoron.uhabits.unit.models;
 
+import android.graphics.Color;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
 
@@ -31,6 +32,8 @@ import java.util.LinkedList;
 import java.util.List;
 
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.core.IsNot.not;
 import static org.junit.Assert.assertThat;
 
 @RunWith(AndroidJUnit4.class)
@@ -43,6 +46,49 @@ public class HabitTest
         HabitFixtures.purgeHabits();
     }
 
+    @Test
+    public void constructor_default()
+    {
+        Habit habit = new Habit();
+        assertThat(habit.archived, is(0));
+        assertThat(habit.highlight, is(0));
+
+        assertThat(habit.reminderDays, is(nullValue()));
+        assertThat(habit.reminderHour, is(nullValue()));
+        assertThat(habit.reminderMin, is(nullValue()));
+
+        assertThat(habit.streaks, is(not(nullValue())));
+        assertThat(habit.scores, is(not(nullValue())));
+        assertThat(habit.repetitions, is(not(nullValue())));
+        assertThat(habit.checkmarks, is(not(nullValue())));
+    }
+
+    @Test
+    public void constructor_habit()
+    {
+        Habit model = new Habit();
+        model.archived = 1;
+        model.highlight = 1;
+        model.color = Color.BLACK;
+        model.freqNum = 10;
+        model.freqDen = 20;
+        model.reminderDays = 1;
+        model.reminderHour = 8;
+        model.reminderMin = 30;
+        model.position = 0;
+
+        Habit habit = new Habit(model);
+        assertThat(habit.archived, is(model.archived));
+        assertThat(habit.highlight, is(model.highlight));
+        assertThat(habit.color, is(model.color));
+        assertThat(habit.freqNum, is(model.freqNum));
+        assertThat(habit.freqDen, is(model.freqDen));
+        assertThat(habit.reminderDays, is(model.reminderDays));
+        assertThat(habit.reminderHour, is(model.reminderHour));
+        assertThat(habit.reminderMin, is(model.reminderMin));
+        assertThat(habit.position, is(model.position));
+    }
+
     @Test
     public void reorderTest()
     {
diff --git a/app/src/main/java/org/isoron/helpers/DateHelper.java b/app/src/main/java/org/isoron/helpers/DateHelper.java
index 361b18cac..f89a3ef7b 100644
--- a/app/src/main/java/org/isoron/helpers/DateHelper.java
+++ b/app/src/main/java/org/isoron/helpers/DateHelper.java
@@ -32,6 +32,8 @@ import java.util.TimeZone;
 public class DateHelper
 {
     public static long millisecondsInOneDay = 24 * 60 * 60 * 1000;
+    public static int ALL_WEEK_DAYS = 127;
+
     private static Long fixedLocalTime = null;
 
     public static long getLocalTime()
diff --git a/app/src/main/java/org/isoron/helpers/DialogHelper.java b/app/src/main/java/org/isoron/helpers/DialogHelper.java
index ebaf6c9b5..c24083a7b 100644
--- a/app/src/main/java/org/isoron/helpers/DialogHelper.java
+++ b/app/src/main/java/org/isoron/helpers/DialogHelper.java
@@ -32,6 +32,7 @@ import android.view.View;
 import android.view.inputmethod.InputMethodManager;
 
 import org.isoron.uhabits.BuildConfig;
+import org.isoron.uhabits.commands.Command;
 
 public abstract class DialogHelper
 {
diff --git a/app/src/main/java/org/isoron/helpers/ReplayableActivity.java b/app/src/main/java/org/isoron/helpers/ReplayableActivity.java
index d2fcdc350..8f35b4bc7 100644
--- a/app/src/main/java/org/isoron/helpers/ReplayableActivity.java
+++ b/app/src/main/java/org/isoron/helpers/ReplayableActivity.java
@@ -26,6 +26,7 @@ import android.os.Bundle;
 import android.widget.Toast;
 
 import org.isoron.uhabits.R;
+import org.isoron.uhabits.commands.Command;
 
 import java.util.LinkedList;
 
diff --git a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java
index 3aee23dc1..5e77c87d1 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java
@@ -19,7 +19,6 @@
 
 package org.isoron.uhabits.commands;
 
-import org.isoron.helpers.Command;
 import org.isoron.uhabits.R;
 import org.isoron.uhabits.models.Habit;
 
@@ -45,15 +44,13 @@ public class ArchiveHabitsCommand extends Command
     @Override
     public void execute()
     {
-        for(Habit h : habits)
-            h.archive();
+        Habit.archive(habits);
     }
 
     @Override
     public void undo()
     {
-        for(Habit h : habits)
-            h.unarchive();
+        Habit.unarchive(habits);
     }
 
     public Integer getExecuteStringId()
diff --git a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java
index f9b00ba89..9abf7d5a7 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java
@@ -21,7 +21,6 @@ package org.isoron.uhabits.commands;
 
 import com.activeandroid.ActiveAndroid;
 
-import org.isoron.helpers.Command;
 import org.isoron.uhabits.R;
 import org.isoron.uhabits.models.Habit;
 
@@ -47,22 +46,7 @@ public class ChangeHabitColorCommand extends Command
     @Override
     public void execute()
     {
-        ActiveAndroid.beginTransaction();
-
-        try
-        {
-            for(Habit h : habits)
-            {
-                h.color = newColor;
-                h.save();
-            }
-
-            ActiveAndroid.setTransactionSuccessful();
-        }
-        finally
-        {
-            ActiveAndroid.endTransaction();
-        }
+        Habit.setColor(habits, newColor);
     }
 
     @Override
diff --git a/app/src/main/java/org/isoron/helpers/Command.java b/app/src/main/java/org/isoron/uhabits/commands/Command.java
similarity index 96%
rename from app/src/main/java/org/isoron/helpers/Command.java
rename to app/src/main/java/org/isoron/uhabits/commands/Command.java
index 7472b096f..b9427e38a 100644
--- a/app/src/main/java/org/isoron/helpers/Command.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/Command.java
@@ -17,7 +17,7 @@
  * with this program. If not, see .
  */
 
-package org.isoron.helpers;
+package org.isoron.uhabits.commands;
 
 public abstract class Command
 {
diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandFailedException.java b/app/src/main/java/org/isoron/uhabits/commands/CommandFailedException.java
new file mode 100644
index 000000000..b64722a88
--- /dev/null
+++ b/app/src/main/java/org/isoron/uhabits/commands/CommandFailedException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.commands;
+
+public class CommandFailedException extends RuntimeException
+{
+    public CommandFailedException()
+    {
+        super();
+    }
+
+    public CommandFailedException(String message)
+    {
+        super(message);
+    }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java
index e3fba3e35..b3fe9499f 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java
@@ -19,7 +19,6 @@
 
 package org.isoron.uhabits.commands;
 
-import org.isoron.helpers.Command;
 import org.isoron.uhabits.R;
 import org.isoron.uhabits.models.Habit;
 
@@ -51,7 +50,10 @@ public class CreateHabitCommand extends Command
     @Override
     public void undo()
     {
-        Habit.get(savedId).delete();
+        Habit habit = Habit.get(savedId);
+        if(habit == null) throw new CommandFailedException("Habit not found");
+
+        habit.delete();
     }
 
     @Override
diff --git a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java
index b1c2ee217..34e26c50c 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java
@@ -19,7 +19,6 @@
 
 package org.isoron.uhabits.commands;
 
-import org.isoron.helpers.Command;
 import org.isoron.uhabits.R;
 import org.isoron.uhabits.models.Habit;
 
diff --git a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java
index ccd641c8b..aaca085d6 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java
@@ -19,7 +19,6 @@
 
 package org.isoron.uhabits.commands;
 
-import org.isoron.helpers.Command;
 import org.isoron.uhabits.R;
 import org.isoron.uhabits.models.Habit;
 
@@ -43,6 +42,8 @@ public class EditHabitCommand extends Command
     public void execute()
     {
         Habit habit = Habit.get(savedId);
+        if(habit == null) throw new CommandFailedException("Habit not found");
+
         habit.copyAttributes(modified);
         habit.save();
         if (hasIntervalChanged)
@@ -56,6 +57,8 @@ public class EditHabitCommand extends Command
     public void undo()
     {
         Habit habit = Habit.get(savedId);
+        if(habit == null) throw new CommandFailedException("Habit not found");
+
         habit.copyAttributes(original);
         habit.save();
         if (hasIntervalChanged)
diff --git a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java
index fe573ddaf..451908433 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java
@@ -19,7 +19,6 @@
 
 package org.isoron.uhabits.commands;
 
-import org.isoron.helpers.Command;
 import org.isoron.uhabits.models.Habit;
 
 public class ToggleRepetitionCommand extends Command
diff --git a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java
index 08d34af69..79bd01d6e 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java
@@ -19,7 +19,6 @@
 
 package org.isoron.uhabits.commands;
 
-import org.isoron.helpers.Command;
 import org.isoron.uhabits.R;
 import org.isoron.uhabits.models.Habit;
 
@@ -45,15 +44,13 @@ public class UnarchiveHabitsCommand extends Command
     @Override
     public void execute()
     {
-        for(Habit h : habits)
-            h.unarchive();
+        Habit.unarchive(habits);
     }
 
     @Override
     public void undo()
     {
-        for(Habit h : habits)
-            h.archive();
+        Habit.archive(habits);
     }
 
     public Integer getExecuteStringId()
diff --git a/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java
index a48087023..eddf1068e 100644
--- a/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java
+++ b/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java
@@ -39,16 +39,17 @@ import com.android.datetimepicker.time.RadialPickerLayout;
 import com.android.datetimepicker.time.TimePickerDialog;
 
 import org.isoron.helpers.ColorHelper;
-import org.isoron.helpers.Command;
 import org.isoron.helpers.DateHelper;
 import org.isoron.helpers.DialogHelper.OnSavedListener;
 import org.isoron.uhabits.R;
+import org.isoron.uhabits.commands.Command;
 import org.isoron.uhabits.commands.CreateHabitCommand;
 import org.isoron.uhabits.commands.EditHabitCommand;
 import org.isoron.uhabits.dialogs.WeekdayPickerDialog;
 import org.isoron.uhabits.models.Habit;
 
 import java.util.Arrays;
+import java.util.Date;
 
 public class EditHabitFragment extends DialogFragment
         implements OnClickListener, WeekdayPickerDialog.OnWeekdaysPickedListener,
@@ -136,7 +137,10 @@ public class EditHabitFragment extends DialogFragment
         }
         else if (mode == EDIT_MODE)
         {
-            originalHabit = Habit.get((Long) args.get("habitId"));
+            Long habitId = (Long) args.get("habitId");
+            if(habitId == null) throw new IllegalArgumentException("habitId must be specified");
+
+            originalHabit = Habit.get(habitId);
             modifiedHabit = new Habit(originalHabit);
 
             getDialog().setTitle(R.string.edit_habit);
@@ -178,14 +182,18 @@ public class EditHabitFragment extends DialogFragment
         editor.apply();
     }
 
+    @SuppressWarnings("ConstantConditions")
     private void updateReminder()
     {
-        if (modifiedHabit.reminderHour != null)
+        if (modifiedHabit.hasReminder())
         {
             tvReminderTime.setTextColor(Color.BLACK);
             tvReminderTime.setText(DateHelper.formatTime(getActivity(), modifiedHabit.reminderHour,
                     modifiedHabit.reminderMin));
             tvReminderDays.setVisibility(View.VISIBLE);
+
+            boolean weekdays[] = DateHelper.unpackWeekdayList(modifiedHabit.reminderDays);
+            tvReminderDays.setText(DateHelper.formatWeekdayList(getActivity(), weekdays));
         }
         else
         {
@@ -193,9 +201,6 @@ public class EditHabitFragment extends DialogFragment
             tvReminderTime.setText(R.string.reminder_off);
             tvReminderDays.setVisibility(View.GONE);
         }
-
-        boolean weekdays[] = DateHelper.unpackWeekdayList(modifiedHabit.reminderDays);
-        tvReminderDays.setText(DateHelper.formatWeekdayList(getActivity(), weekdays));
     }
 
     public void setOnSavedListener(OnSavedListener onSavedListener)
@@ -303,12 +308,13 @@ public class EditHabitFragment extends DialogFragment
         return valid;
     }
 
+    @SuppressWarnings("ConstantConditions")
     private void onDateSpinnerClick()
     {
         int defaultHour = 8;
         int defaultMin = 0;
 
-        if (modifiedHabit.reminderHour != null)
+        if (modifiedHabit.hasReminder())
         {
             defaultHour = modifiedHabit.reminderHour;
             defaultMin = modifiedHabit.reminderMin;
@@ -319,8 +325,11 @@ public class EditHabitFragment extends DialogFragment
         timePicker.show(getFragmentManager(), "timePicker");
     }
 
+    @SuppressWarnings("ConstantConditions")
     private void onWeekdayClick()
     {
+        if(!modifiedHabit.hasReminder()) return;
+
         WeekdayPickerDialog dialog = new WeekdayPickerDialog();
         dialog.setListener(this);
         dialog.setSelectedDays(DateHelper.unpackWeekdayList(modifiedHabit.reminderDays));
@@ -332,14 +341,14 @@ public class EditHabitFragment extends DialogFragment
     {
         modifiedHabit.reminderHour = hour;
         modifiedHabit.reminderMin = minute;
+        modifiedHabit.reminderDays = DateHelper.ALL_WEEK_DAYS;
         updateReminder();
     }
 
     @Override
     public void onTimeCleared(RadialPickerLayout view)
     {
-        modifiedHabit.reminderHour = null;
-        modifiedHabit.reminderMin = null;
+        modifiedHabit.clearReminder();
         updateReminder();
     }
 
@@ -357,12 +366,14 @@ public class EditHabitFragment extends DialogFragment
     }
 
     @Override
+    @SuppressWarnings("ConstantConditions")
     public void onSaveInstanceState(Bundle outState)
     {
         super.onSaveInstanceState(outState);
 
         outState.putInt("color", modifiedHabit.color);
-        if(modifiedHabit.reminderHour != null)
+
+        if(modifiedHabit.hasReminder())
         {
             outState.putInt("reminderMin", modifiedHabit.reminderMin);
             outState.putInt("reminderHour", modifiedHabit.reminderHour);
diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java
index ef11a2301..51b24d696 100644
--- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java
+++ b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java
@@ -46,7 +46,7 @@ import com.mobeta.android.dslv.DragSortController;
 import com.mobeta.android.dslv.DragSortListView;
 import com.mobeta.android.dslv.DragSortListView.DropListener;
 
-import org.isoron.helpers.Command;
+import org.isoron.uhabits.commands.Command;
 import org.isoron.helpers.DateHelper;
 import org.isoron.helpers.DialogHelper;
 import org.isoron.helpers.DialogHelper.OnSavedListener;
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 1b7817afb..ba6c82d41 100644
--- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java
+++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java
@@ -33,7 +33,7 @@ import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import org.isoron.helpers.ColorHelper;
-import org.isoron.helpers.Command;
+import org.isoron.uhabits.commands.Command;
 import org.isoron.helpers.DialogHelper;
 import org.isoron.uhabits.HabitBroadcastReceiver;
 import org.isoron.uhabits.R;
diff --git a/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java
index 9a0e691d0..3f515396b 100644
--- a/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java
+++ b/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java
@@ -25,6 +25,7 @@ import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Build;
+import android.support.annotation.Nullable;
 import android.util.Log;
 
 import org.isoron.helpers.DateHelper;
@@ -43,13 +44,17 @@ public class ReminderHelper
             createReminderAlarm(context, habit, null);
     }
 
-    public static void createReminderAlarm(Context context, Habit habit, Long reminderTime)
+    public static void createReminderAlarm(Context context, Habit habit, @Nullable Long reminderTime)
     {
+        if(!habit.hasReminder()) return;
+
         if (reminderTime == null)
         {
             Calendar calendar = Calendar.getInstance();
             calendar.setTimeInMillis(System.currentTimeMillis());
+            //noinspection ConstantConditions
             calendar.set(Calendar.HOUR_OF_DAY, habit.reminderHour);
+            //noinspection ConstantConditions
             calendar.set(Calendar.MINUTE, habit.reminderMin);
             calendar.set(Calendar.SECOND, 0);
 
diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java
index 02e3e67b6..851077189 100644
--- a/app/src/main/java/org/isoron/uhabits/models/Habit.java
+++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java
@@ -21,6 +21,8 @@ package org.isoron.uhabits.models;
 
 import android.annotation.SuppressLint;
 import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 
 import com.activeandroid.ActiveAndroid;
 import com.activeandroid.Model;
@@ -39,50 +41,113 @@ import java.util.List;
 @Table(name = "Habits")
 public class Habit extends Model
 {
+    /**
+     * Name of the habit
+     */
     @Column(name = "name")
     public String name;
 
+    /**
+     * Description of the habit
+     */
     @Column(name = "description")
     public String description;
 
+    /**
+     * Frequency numerator. If a habit is performed 3 times in 7 days, this field equals 3.
+     */
     @Column(name = "freq_num")
     public Integer freqNum;
 
+    /**
+     * Frequency denominator. If a habit is performed 3 times in 7 days, this field equals 7.
+     */
     @Column(name = "freq_den")
     public Integer freqDen;
 
+    /**
+     * Color of the habit. The format is the same as android.graphics.Color.
+     */
     @Column(name = "color")
     public Integer color;
 
+    /**
+     * Position of the habit. Habits are usually sorted by this field.
+     */
     @Column(name = "position")
     public Integer position;
 
+    /**
+     * Hour of the day the reminder should be shown. If there is no reminder, this equals to null.
+     */
+    @Nullable
     @Column(name = "reminder_hour")
     public Integer reminderHour;
 
+    /**
+     * Minute the reminder should be shown. If there is no reminder, this equals to null.
+     */
+    @Nullable
     @Column(name = "reminder_min")
     public Integer reminderMin;
 
+    /**
+     * Days of the week the reminder should be shown. This field can be converted to a list of
+     * booleans using the method DateHelper.unpackWeekdayList and converted back to an integer by
+     * using the method DateHelper.packWeekdayList. If there is no reminder, it equals null.
+     */
+    @Nullable
     @Column(name = "reminder_days")
     public Integer reminderDays;
 
+    /**
+     * Not currently used.
+     */
     @Column(name = "highlight")
     public Integer highlight;
 
+    /**
+     * Flag that indicates whether the habit is archived. Archived habits are usually omitted from
+     * listings, unless explicitly included.
+     */
     @Column(name = "archived")
     public Integer archived;
 
+    /**
+     * List of streaks belonging to this habit.
+     */
     public StreakList streaks;
+
+    /**
+     * List of scores belonging to this habit.
+     */
     public ScoreList scores;
+
+    /**
+     * List of repetitions belonging to this habit.
+     */
     public RepetitionList repetitions;
+
+    /**
+     * List of checkmarks belonging to this habit.
+     */
     public CheckmarkList checkmarks;
 
+    /**
+     * Constructs a habit with the same attributes as the specified habit.
+     *
+     * @param model the model whose attributes should be copied from
+     */
     public Habit(Habit model)
     {
         copyAttributes(model);
         initializeLists();
     }
 
+    /**
+     * Constructs a habit with default attributes. The habit is not archived, not highlighted, has
+     * no reminders and is placed in the last position of the list of habits.
+     */
     public Habit()
     {
         this.color = ColorHelper.palette[5];
@@ -91,7 +156,6 @@ public class Habit extends Model
         this.archived = 0;
         this.freqDen = 7;
         this.freqNum = 3;
-        this.reminderDays = 127;
         initializeLists();
     }
 
@@ -103,17 +167,36 @@ public class Habit extends Model
         checkmarks = new CheckmarkList(this);
     }
 
-    public static Habit get(Long id)
+    /**
+     * Returns the habit with specified id.
+     *
+     * @param id the id of the habit
+     * @return the habit, or null if none exist
+     */
+    @Nullable
+    public static Habit get(@NonNull Long id)
     {
         return Habit.load(Habit.class, id);
     }
 
+    /**
+     * Returns a list of all habits, optionally including archived habits.
+     *
+     * @param includeArchive whether archived habits should be included the list
+     * @return list of all habits
+     */
     public static List getAll(boolean includeArchive)
     {
         if(includeArchive) return selectWithArchived().execute();
         else return select().execute();
     }
 
+    /**
+     * Changes the id of a habit on the database.
+     *
+     * @param oldId the original id
+     * @param newId the new id
+     */
     @SuppressLint("DefaultLocale")
     public static void updateId(long oldId, long newId)
     {
@@ -125,21 +208,32 @@ public class Habit extends Model
         return new Select().from(Habit.class).where("archived = 0").orderBy("position");
     }
 
-    public static From selectWithArchived()
+    protected static From selectWithArchived()
     {
         return new Select().from(Habit.class).orderBy("position");
     }
 
+    /**
+     * Returns the total number of unarchived habits.
+     *
+     * @return number of unarchived habits
+     */
     public static int count()
     {
         return select().count();
     }
 
+    /**
+     * Returns the total number of habits, including archived habits.
+     *
+     * @return number of habits, including archived
+     */
     public static int countWithArchived()
     {
         return selectWithArchived().count();
     }
 
+
     public static java.util.List getHighlightedHabits()
     {
         return select().where("highlight = 1")
@@ -147,11 +241,22 @@ public class Habit extends Model
                 .execute();
     }
 
-    public static java.util.List getHabitsWithReminder()
+    /**
+     * Returns a list the habits that have a reminder. Does not include archived habits.
+     *
+     * @return list of habits with reminder
+     */
+    public static List getHabitsWithReminder()
     {
         return select().where("reminder_hour is not null").execute();
     }
 
+    /**
+     * Changes the position of a habit in the list.
+     *
+     * @param from the habit that should be moved
+     * @param to the habit that currently occupies the desired position
+     */
     public static void reorder(Habit from, Habit to)
     {
         if(from == to) return;
@@ -173,6 +278,10 @@ public class Habit extends Model
         from.save();
     }
 
+    /**
+     * Recompute the field position for every habit in the database. It should never be necessary
+     * to call this method.
+     */
     public static void rebuildOrder()
     {
         List habits = selectWithArchived().execute();
@@ -196,6 +305,11 @@ public class Habit extends Model
 
     }
 
+    /**
+     * Copies all the attributes of the specified habit into this habit
+     *
+     * @param model the model whose attributes should be copied from
+     */
     public void copyAttributes(Habit model)
     {
         this.name = model.name;
@@ -211,12 +325,21 @@ public class Habit extends Model
         this.archived = model.archived;
     }
 
+    /**
+     * Saves the habit on the database, and assigns the specified id to it.
+     *
+     * @param id the id that the habit should receive
+     */
     public void save(Long id)
     {
         save();
         Habit.updateId(getId(), id);
     }
 
+    /**
+     * Deletes the habit and all data associated to it, including checkmarks, repetitions and
+     * scores.
+     */
     public void cascadeDelete()
     {
         Long id = getId();
@@ -238,25 +361,93 @@ public class Habit extends Model
         }
     }
 
+    /**
+     * Returns the public URI that identifies this habit
+     * @return the uri
+     */
     public Uri getUri()
     {
         return Uri.parse(String.format("content://org.isoron.uhabits/habit/%d", getId()));
     }
 
-    public void archive()
+    /**
+     * Returns whether the habit is archived or not.
+     * @return true if archived
+     */
+    public boolean isArchived()
     {
-        archived = 1;
-        save();
+        return archived != 0;
     }
 
-    public void unarchive()
+    private static void updateAttributes(List habits, Integer color, Integer archived)
     {
-        archived = 0;
-        save();
+        ActiveAndroid.beginTransaction();
+
+        try
+        {
+            for (Habit h : habits)
+            {
+                if(color != null) h.color = color;
+                if(archived != null) h.archived = archived;
+                h.save();
+            }
+
+            ActiveAndroid.setTransactionSuccessful();
+        }
+        finally
+        {
+            ActiveAndroid.endTransaction();
+        }
     }
 
-    public boolean isArchived()
+    /**
+     * Archives an entire list of habits
+     *
+     * @param habits the habits to be archived
+     */
+    public static void archive(List habits)
     {
-        return archived != 0;
+        updateAttributes(habits, null, 1);
+    }
+
+    /**
+     * Unarchives an entire list of habits
+     *
+     * @param habits the habits to be unarchived
+     */
+    public static void unarchive(List habits)
+    {
+        updateAttributes(habits, null, 0);
+    }
+
+    /**
+     * Sets the color for an entire list of habits.
+     *
+     * @param habits the habits to be modified
+     * @param color the new color to be set
+     */
+    public static void setColor(List habits, int color)
+    {
+        updateAttributes(habits, color, null);
+    }
+
+    /**
+     * Checks whether the habit has a reminder set.
+     *
+     * @return true if habit has reminder
+     */
+    public boolean hasReminder()
+    {
+        return (reminderHour != null && reminderMin != null && reminderDays != null);
+    }
+
+    /**
+     * Clears the reminder for a habit. This sets all the related fields to null.
+     */
+    public void clearReminder()
+    {
+        reminderHour = null;
+        reminderMin = null;
+        reminderDays = null;
     }
 }

From f5e4a88415a1ed4669d336e69db35c50ddcd5ed6 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Thu, 17 Mar 2016 06:16:47 -0400
Subject: [PATCH 039/175] Implement missing tests for Habit; remove some dead
 code

---
 .../isoron/uhabits/unit/models/HabitTest.java | 237 +++++++++++++++++-
 .../uhabits/HabitBroadcastReceiver.java       |   6 +-
 .../java/org/isoron/uhabits/models/Habit.java |  43 ++--
 3 files changed, 254 insertions(+), 32 deletions(-)

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 f31db5f35..a4e3d5678 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
@@ -23,6 +23,7 @@ import android.graphics.Color;
 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.Before;
 import org.junit.Test;
@@ -31,10 +32,12 @@ import org.junit.runner.RunWith;
 import java.util.LinkedList;
 import java.util.List;
 
+import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.core.IsNot.not;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
 
 @RunWith(AndroidJUnit4.class)
 @SmallTest
@@ -90,11 +93,116 @@ public class HabitTest
     }
 
     @Test
-    public void reorderTest()
+    public void get_withValidId()
+    {
+        Habit habit = new Habit();
+        habit.save();
+
+        Habit habit2 = Habit.get(habit.getId());
+        assertThat(habit, equalTo(habit2));
+    }
+
+    @Test
+    public void get_withInvalidId()
+    {
+        Habit habit = Habit.get(123456L);
+        assertThat(habit, is(nullValue()));
+    }
+
+    @Test
+    public void getAll_withoutArchived()
+    {
+        List habits = new LinkedList<>();
+        List habitsWithArchived = new LinkedList<>();
+
+        for(int i = 0; i < 10; i++)
+        {
+            Habit h = new Habit();
+
+            if(i % 2 == 0)
+                h.archived = 1;
+            else
+                habits.add(h);
+
+            habitsWithArchived.add(h);
+            h.save();
+        }
+
+        assertThat(habits, equalTo(Habit.getAll(false)));
+        assertThat(habitsWithArchived, equalTo(Habit.getAll(true)));
+    }
+
+    @Test
+    public void getByPosition()
+    {
+        List habits = new LinkedList<>();
+
+        for(int i = 0; i < 10; i++)
+        {
+            Habit h = new Habit();
+            h.save();
+            habits.add(h);
+        }
+
+        for(int i = 0; i < 10; i++)
+        {
+            Habit h = Habit.getByPosition(i);
+            if(h == null) fail();
+            assertThat(h, equalTo(habits.get(i)));
+        }
+    }
+
+    @Test
+    public void count()
+    {
+        for(int i = 0; i < 10; i++)
+        {
+            Habit h = new Habit();
+            if(i % 2 == 0) h.archived = 1;
+            h.save();
+        }
+
+        assertThat(Habit.count(), equalTo(5));
+    }
+
+
+    @Test
+    public void countWithArchived()
+    {
+        for(int i = 0; i < 10; i++)
+        {
+            Habit h = new Habit();
+            if(i % 2 == 0) h.archived = 1;
+            h.save();
+        }
+
+        assertThat(Habit.countWithArchived(), equalTo(10));
+    }
+
+    @Test
+    public void updateId()
+    {
+        Habit habit = new Habit();
+        habit.name = "Hello World";
+        habit.save();
+
+        Long oldId = habit.getId();
+        Long newId = 123456L;
+        Habit.updateId(oldId, newId);
+
+        Habit newHabit = Habit.get(newId);
+        if(newHabit == null) fail();
+        assertThat(newHabit, is(not(nullValue())));
+        assertThat(newHabit.name, equalTo(habit.name));
+    }
+
+    @Test
+    public void reorder()
     {
         List ids = new LinkedList<>();
 
-        for (int i = 0; i < 10; i++)
+        int n = 10;
+        for (int i = 0; i < n; i++)
         {
             Habit h = new Habit();
             h.save();
@@ -102,22 +210,44 @@ public class HabitTest
             assertThat(h.position, is(i));
         }
 
-        int from = 5, to = 2;
-        int expectedPosition[] = {0, 1, 3, 4, 5, 2, 6, 7, 8, 9};
+        int operations[][] = {
+                {5, 2},
+                {3, 7},
+                {4, 4},
+                {3, 2}
+        };
 
-        Habit fromHabit = Habit.get(ids.get(from));
-        Habit toHabit = Habit.get(ids.get(to));
-        Habit.reorder(fromHabit, toHabit);
+        int expectedPosition[][] = {
+                {0, 1, 3, 4, 5, 2, 6, 7, 8, 9},
+                {0, 1, 7, 3, 4, 2, 5, 6, 8, 9},
+                {0, 1, 7, 3, 4, 2, 5, 6, 8, 9},
+                {0, 1, 7, 2, 4, 3, 5, 6, 8, 9},
+        };
 
-        for (int i = 0; i < 10; i++)
+        for(int i = 0; i < operations.length; i++)
         {
-            Habit h = Habit.get(ids.get(i));
-            assertThat(h.position, is(expectedPosition[i]));
+            int from = operations[i][0];
+            int to = operations[i][1];
+
+            Habit fromHabit = Habit.getByPosition(from);
+            Habit toHabit = Habit.getByPosition(to);
+            Habit.reorder(fromHabit, toHabit);
+
+            int actualPositions[] = new int[n];
+
+            for (int j = 0; j < n; j++)
+            {
+                Habit h = Habit.get(ids.get(j));
+                if (h == null) fail();
+                actualPositions[j] = h.position;
+            }
+
+            assertThat(actualPositions, equalTo(expectedPosition[i]));
         }
     }
 
     @Test
-    public  void rebuildOrderTest()
+    public  void rebuildOrder()
     {
         List ids = new LinkedList<>();
         int originalPositions[] = { 0, 1, 1, 4, 6, 8, 10, 10, 13};
@@ -135,7 +265,92 @@ public class HabitTest
         for (int i = 0; i < originalPositions.length; i++)
         {
             Habit h = Habit.get(ids.get(i));
+            if(h == null) fail();
             assertThat(h.position, is(i));
         }
     }
+
+    @Test
+    public void getHabitsWithReminder()
+    {
+        List habitsWithReminder = new LinkedList<>();
+
+        for(int i = 0; i < 10; i++)
+        {
+            Habit habit = new Habit();
+            if(i % 2 == 0)
+            {
+                habit.reminderDays = DateHelper.ALL_WEEK_DAYS;
+                habit.reminderHour = 8;
+                habit.reminderMin = 30;
+                habitsWithReminder.add(habit);
+            }
+            habit.save();
+        }
+
+        assertThat(habitsWithReminder, equalTo(Habit.getHabitsWithReminder()));
+    }
+
+    @Test
+    public void archive_unarchive()
+    {
+        List allHabits = new LinkedList<>();
+        List archivedHabits = new LinkedList<>();
+        List unarchivedHabits = new LinkedList<>();
+
+        for(int i = 0; i < 10; i++)
+        {
+            Habit habit = new Habit();
+            habit.save();
+            allHabits.add(habit);
+
+            if(i % 2 == 0)
+                archivedHabits.add(habit);
+            else
+                unarchivedHabits.add(habit);
+        }
+
+        Habit.archive(archivedHabits);
+        assertThat(Habit.getAll(false), equalTo(unarchivedHabits));
+        assertThat(Habit.getAll(true), equalTo(allHabits));
+
+        Habit.unarchive(archivedHabits);
+        assertThat(Habit.getAll(false), equalTo(allHabits));
+        assertThat(Habit.getAll(true), equalTo(allHabits));
+    }
+
+    @Test
+    public void setColor()
+    {
+        List habits = new LinkedList<>();
+
+        for(int i = 0; i < 10; i++)
+        {
+            Habit habit = new Habit();
+            habit.color = i;
+            habit.save();
+            habits.add(habit);
+        }
+
+        int newColor = 100;
+        Habit.setColor(habits, newColor);
+
+        for(Habit h : habits)
+            assertThat(h.color, equalTo(newColor));
+    }
+
+    @Test
+    public void hasReminder_clearReminder()
+    {
+        Habit h = new Habit();
+        assertThat(h.hasReminder(), is(false));
+
+        h.reminderDays = DateHelper.ALL_WEEK_DAYS;
+        h.reminderHour = 8;
+        h.reminderMin = 30;
+        assertThat(h.hasReminder(), is(true));
+
+        h.clearReminder();
+        assertThat(h.hasReminder(), is(false));
+    }
 }
diff --git a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java
index 1141a6f75..db88d38e9 100644
--- a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java
+++ b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java
@@ -125,11 +125,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
 
     private void dismissAllHabits()
     {
-        for (Habit h : Habit.getHighlightedHabits())
-        {
-            h.highlight = 0;
-            h.save();
-        }
+
     }
 
     private void dismissNotification(Context context, Long habitId)
diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java
index 851077189..362fea02a 100644
--- a/app/src/main/java/org/isoron/uhabits/models/Habit.java
+++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java
@@ -37,6 +37,7 @@ import com.activeandroid.util.SQLiteUtils;
 import org.isoron.helpers.ColorHelper;
 
 import java.util.List;
+import java.util.Locale;
 
 @Table(name = "Habits")
 public class Habit extends Model
@@ -174,7 +175,7 @@ public class Habit extends Model
      * @return the habit, or null if none exist
      */
     @Nullable
-    public static Habit get(@NonNull Long id)
+    public static Habit get(long id)
     {
         return Habit.load(Habit.class, id);
     }
@@ -185,12 +186,25 @@ public class Habit extends Model
      * @param includeArchive whether archived habits should be included the list
      * @return list of all habits
      */
+    @NonNull
     public static List getAll(boolean includeArchive)
     {
         if(includeArchive) return selectWithArchived().execute();
         else return select().execute();
     }
 
+    /**
+     * Returns the habit that occupies a certain position.
+     *
+     * @param position the position of the desired habit
+     * @return the habit at that position, or null if there is none
+     */
+    @Nullable
+    public static Habit getByPosition(int position)
+    {
+        return selectWithArchived().where("position = ?", position).executeSingle();
+    }
+
     /**
      * Changes the id of a habit on the database.
      *
@@ -203,11 +217,13 @@ public class Habit extends Model
         SQLiteUtils.execSql(String.format("update Habits set Id = %d where Id = %d", newId, oldId));
     }
 
+    @NonNull
     protected static From select()
     {
         return new Select().from(Habit.class).where("archived = 0").orderBy("position");
     }
 
+    @NonNull
     protected static From selectWithArchived()
     {
         return new Select().from(Habit.class).orderBy("position");
@@ -233,19 +249,12 @@ public class Habit extends Model
         return selectWithArchived().count();
     }
 
-
-    public static java.util.List getHighlightedHabits()
-    {
-        return select().where("highlight = 1")
-                .orderBy("reminder_hour desc, reminder_min desc")
-                .execute();
-    }
-
     /**
      * Returns a list the habits that have a reminder. Does not include archived habits.
      *
      * @return list of habits with reminder
      */
+    @NonNull
     public static List getHabitsWithReminder()
     {
         return select().where("reminder_hour is not null").execute();
@@ -310,7 +319,7 @@ public class Habit extends Model
      *
      * @param model the model whose attributes should be copied from
      */
-    public void copyAttributes(Habit model)
+    public void copyAttributes(@NonNull Habit model)
     {
         this.name = model.name;
         this.description = model.description;
@@ -330,7 +339,7 @@ public class Habit extends Model
      *
      * @param id the id that the habit should receive
      */
-    public void save(Long id)
+    public void save(long id)
     {
         save();
         Habit.updateId(getId(), id);
@@ -367,7 +376,8 @@ public class Habit extends Model
      */
     public Uri getUri()
     {
-        return Uri.parse(String.format("content://org.isoron.uhabits/habit/%d", getId()));
+        String s = String.format(Locale.US, "content://org.isoron.uhabits/habit/%d", getId());
+        return Uri.parse(s);
     }
 
     /**
@@ -379,7 +389,8 @@ public class Habit extends Model
         return archived != 0;
     }
 
-    private static void updateAttributes(List habits, Integer color, Integer archived)
+    private static void updateAttributes(@NonNull List habits, @Nullable Integer color,
+                                         @Nullable Integer archived)
     {
         ActiveAndroid.beginTransaction();
 
@@ -405,7 +416,7 @@ public class Habit extends Model
      *
      * @param habits the habits to be archived
      */
-    public static void archive(List habits)
+    public static void archive(@NonNull List habits)
     {
         updateAttributes(habits, null, 1);
     }
@@ -415,7 +426,7 @@ public class Habit extends Model
      *
      * @param habits the habits to be unarchived
      */
-    public static void unarchive(List habits)
+    public static void unarchive(@NonNull List habits)
     {
         updateAttributes(habits, null, 0);
     }
@@ -426,7 +437,7 @@ public class Habit extends Model
      * @param habits the habits to be modified
      * @param color the new color to be set
      */
-    public static void setColor(List habits, int color)
+    public static void setColor(@NonNull List habits, int color)
     {
         updateAttributes(habits, color, null);
     }

From 7ba62d6784871fbae0d5d854093ba7102b280576 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Thu, 17 Mar 2016 06:39:12 -0400
Subject: [PATCH 040/175] Minor formatting

---
 .../unit/models/CheckmarkListTest.java        | 30 +++++++++----------
 1 file changed, 15 insertions(+), 15 deletions(-)

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 5c017da75..6413a566b 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
@@ -58,7 +58,7 @@ public class CheckmarkListTest
     }
 
     @Test
-    public void getAllValues_testNonDailyHabit()
+    public void getAllValues_withNonDailyHabit()
     {
         int[] expectedValues = { CHECKED_EXPLICITLY, UNCHECKED, CHECKED_IMPLICITLY,
                 CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, UNCHECKED,
@@ -70,7 +70,16 @@ public class CheckmarkListTest
     }
 
     @Test
-    public void getAllValues_testMoveForwardInTime()
+    public void getAllValues_withEmptyHabit()
+    {
+        int[] expectedValues = new int[0];
+        int[] actualValues = emptyHabit.checkmarks.getAllValues();
+
+        assertThat(actualValues, equalTo(expectedValues));
+    }
+
+    @Test
+    public void getAllValues_moveForwardInTime()
     {
         travelInTime(3);
 
@@ -84,7 +93,7 @@ public class CheckmarkListTest
     }
 
     @Test
-    public void getAllValues_testMoveBackwardsInTime()
+    public void getAllValues_moveBackwardsInTime()
     {
         travelInTime(-3);
 
@@ -97,23 +106,14 @@ public class CheckmarkListTest
     }
 
     @Test
-    public void getAllValues_testEmptyHabit()
-    {
-        int[] expectedValues = new int[0];
-        int[] actualValues = emptyHabit.checkmarks.getAllValues();
-
-        assertThat(actualValues, equalTo(expectedValues));
-    }
-
-    @Test
-    public void getValues_testInvalidInterval()
+    public void getValues_withInvalidInterval()
     {
         int values[] = nonDailyHabit.checkmarks.getValues(100L, -100L);
         assertThat(values, equalTo(new int[0]));
     }
 
     @Test
-    public void getValues_testValidInterval()
+    public void getValues_withValidInterval()
     {
         long from = DateHelper.getStartOfToday() - 15 * DateHelper.millisecondsInOneDay;
         long to = DateHelper.getStartOfToday() - 5 * DateHelper.millisecondsInOneDay;
@@ -128,7 +128,7 @@ public class CheckmarkListTest
     }
 
     @Test
-    public void getTodayValue_testNonDailyHabit()
+    public void getTodayValue()
     {
         travelInTime(-1);
         assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(UNCHECKED));

From 79b6ef820073f9ac1680425c324ce21e5161ef69 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Thu, 17 Mar 2016 06:39:29 -0400
Subject: [PATCH 041/175] Improve null check for Checkmark and CheckmarkList

---
 .../org/isoron/uhabits/models/Checkmark.java   |  7 +++++++
 .../isoron/uhabits/models/CheckmarkList.java   | 18 +++++++++++++-----
 2 files changed, 20 insertions(+), 5 deletions(-)

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 089008efd..a6c5ec06f 100644
--- a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java
+++ b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java
@@ -43,9 +43,16 @@ public class Checkmark extends Model
      */
     public static final int CHECKED_EXPLICITLY = 2;
 
+    /**
+     * The habit to which this checkmark belongs.
+     */
     @Column(name = "habit")
     public Habit habit;
 
+    /**
+     * Timestamp of the day to which this checkmark corresponds. Time of the day must be midnight
+     * (UTC).
+     */
     @Column(name = "timestamp")
     public Long timestamp;
 
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 af638e903..cb96f74af 100644
--- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java
+++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java
@@ -21,6 +21,8 @@ package org.isoron.uhabits.models;
 
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 
 import com.activeandroid.ActiveAndroid;
 import com.activeandroid.Cache;
@@ -66,7 +68,8 @@ public class CheckmarkList
      * @param toTimestamp timestamp for the newest checkmark
      * @return values for the checkmarks inside the given interval
      */
-    public int[] getValues(Long fromTimestamp, Long toTimestamp)
+    @NonNull
+    public int[] getValues(long fromTimestamp, long toTimestamp)
     {
         buildCache(fromTimestamp, toTimestamp);
         if(fromTimestamp > toTimestamp) return new int[0];
@@ -75,8 +78,8 @@ public class CheckmarkList
                 "habit = ? and timestamp >= ? and timestamp <= ?";
 
         SQLiteDatabase db = Cache.openDatabase();
-        String args[] = { habit.getId().toString(), fromTimestamp.toString(),
-                toTimestamp.toString() };
+        String args[] = { habit.getId().toString(), Long.toString(fromTimestamp),
+                Long.toString(toTimestamp) };
         Cursor cursor = db.rawQuery(query, args);
 
         long day = DateHelper.millisecondsInOneDay;
@@ -108,6 +111,7 @@ public class CheckmarkList
      *
      * @return values for the checkmarks in the interval
      */
+    @NonNull
     public int[] getAllValues()
     {
         Repetition oldestRep = habit.repetitions.getOldest();
@@ -188,6 +192,7 @@ public class CheckmarkList
      * Returns newest checkmark that has already been computed. Ignores any checkmark that has
      * timestamp in the future. This does not update the cache.
      */
+    @Nullable
     private Checkmark findNewest()
     {
         return new Select().from(Checkmark.class)
@@ -201,6 +206,7 @@ public class CheckmarkList
     /**
      * Returns the checkmark for today.
      */
+    @Nullable
     public Checkmark getToday()
     {
         long today = DateHelper.getStartOfToday();
@@ -209,10 +215,12 @@ public class CheckmarkList
     }
 
     /**
-     * Returns the value of today's checkmark.
+     * Returns the value of today's checkmark. If there is no checkmark today, returns UNCHECKED.
      */
     public int getTodayValue()
     {
-        return getToday().value;
+        Checkmark today = getToday();
+        if(today != null) return today.value;
+        else return Checkmark.UNCHECKED;
     }
 }

From eb017bf99b9650c90f51dcd3276057676f1a62bd Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Thu, 17 Mar 2016 06:49:11 -0400
Subject: [PATCH 042/175] Write missing tests and docs for RepetitionList

---
 .../unit/models/RepetitionListTest.java       | 20 +++++++++++++++----
 .../org/isoron/uhabits/models/Repetition.java |  7 ++++++-
 .../isoron/uhabits/models/RepetitionList.java | 10 ++++++++--
 3 files changed, 30 insertions(+), 7 deletions(-)

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
index 1c30c28d0..173af0b51 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java
@@ -61,7 +61,7 @@ public class RepetitionListTest
     }
 
     @Test
-    public void contains_testNonDailyHabit()
+    public void contains()
     {
         long current = DateHelper.getStartOfToday();
 
@@ -79,7 +79,7 @@ public class RepetitionListTest
     }
 
     @Test
-    public void delete_test()
+    public void delete()
     {
         long timestamp = DateHelper.getStartOfToday();
         assertThat(habit.repetitions.contains(timestamp), equalTo(true));
@@ -89,7 +89,7 @@ public class RepetitionListTest
     }
 
     @Test
-    public void toggle_test()
+    public void toggle()
     {
         long timestamp = DateHelper.getStartOfToday();
         assertThat(habit.repetitions.contains(timestamp), equalTo(true));
@@ -102,7 +102,7 @@ public class RepetitionListTest
     }
 
     @Test
-    public void getWeekDayFrequency_test()
+    public void getWeekDayFrequency()
     {
         Random random = new Random();
         Integer weekdayCount[][] = new Integer[12][7];
@@ -159,4 +159,16 @@ public class RepetitionListTest
         day.set(2015, 11, 1);
         assertThat(freq.get(day.getTimeInMillis()), equalTo(null));
     }
+
+    @Test
+    public void count()
+    {
+        long to = DateHelper.getStartOfToday();
+        long from = to - 9 * DateHelper.millisecondsInOneDay;
+        assertThat(habit.repetitions.count(from, to), equalTo(6));
+
+        to = DateHelper.getStartOfToday() - DateHelper.millisecondsInOneDay;
+        from = to - 5 * DateHelper.millisecondsInOneDay;
+        assertThat(habit.repetitions.count(from, to), equalTo(3));
+    }
 }
diff --git a/app/src/main/java/org/isoron/uhabits/models/Repetition.java b/app/src/main/java/org/isoron/uhabits/models/Repetition.java
index a2503706f..f43243698 100644
--- a/app/src/main/java/org/isoron/uhabits/models/Repetition.java
+++ b/app/src/main/java/org/isoron/uhabits/models/Repetition.java
@@ -26,10 +26,15 @@ import com.activeandroid.annotation.Table;
 @Table(name = "Repetitions")
 public class Repetition extends Model
 {
-
+    /**
+     * Habit to which this repetition belong.
+     */
     @Column(name = "habit")
     public Habit habit;
 
+    /**
+     * Timestamp of the day this repetition occurred. Time of day should be midnight (UTC).
+     */
     @Column(name = "timestamp")
     public Long timestamp;
 }
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 daa889669..c7b91351a 100644
--- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
+++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
@@ -21,6 +21,8 @@ package org.isoron.uhabits.models;
 
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 
 import com.activeandroid.Cache;
 import com.activeandroid.query.Delete;
@@ -35,14 +37,15 @@ import java.util.HashMap;
 
 public class RepetitionList
 {
-
+    @NonNull
     private Habit habit;
 
-    public RepetitionList(Habit habit)
+    public RepetitionList(@NonNull Habit habit)
     {
         this.habit = habit;
     }
 
+    @NonNull
     protected From select()
     {
         return new Select().from(Repetition.class)
@@ -51,6 +54,7 @@ public class RepetitionList
                 .orderBy("timestamp");
     }
 
+    @NonNull
     protected From selectFromTo(long timeFrom, long timeTo)
     {
         return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo);
@@ -114,6 +118,7 @@ public class RepetitionList
      *
      * @return oldest repetition for the habit
      */
+    @Nullable
     public Repetition getOldest()
     {
         return (Repetition) select().limit(1).executeSingle();
@@ -129,6 +134,7 @@ public class RepetitionList
      *
      * @return total number of repetitions by month versus day of week
      */
+    @NonNull
     public HashMap getWeekdayFrequency()
     {
         Repetition oldestRep = getOldest();

From 0921f9346e033ea1313cec0633dae68c2e93994c Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Fri, 18 Mar 2016 07:16:15 -0400
Subject: [PATCH 043/175] Refactor and write docs for Score and ScoreList

---
 .../uhabits/unit/models/HabitFixtures.java    |   2 +
 .../uhabits/unit/models/ScoreListTest.java    | 146 ++++++++++
 .../isoron/uhabits/unit/models/ScoreTest.java | 113 ++++++++
 .../isoron/helpers/ActiveAndroidHelper.java   |  29 ++
 .../uhabits/commands/EditHabitCommand.java    |   4 +-
 .../uhabits/fragments/ShowHabitFragment.java  |   2 +-
 .../org/isoron/uhabits/io/CSVExporter.java    |   2 +-
 .../uhabits/loaders/HabitListLoader.java      |   4 +-
 .../java/org/isoron/uhabits/models/Habit.java |  16 +-
 .../isoron/uhabits/models/RepetitionList.java |   2 +-
 .../java/org/isoron/uhabits/models/Score.java |  80 +++++-
 .../org/isoron/uhabits/models/ScoreList.java  | 258 +++++++++++++-----
 .../isoron/uhabits/views/CheckmarkView.java   |   2 +-
 .../isoron/uhabits/views/HabitScoreView.java  |  10 +-
 14 files changed, 577 insertions(+), 93 deletions(-)
 create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java
 create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java
 create mode 100644 app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java

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
index 3cbea7a0d..a8c706a2c 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java
@@ -48,6 +48,8 @@ public class HabitFixtures
     static Habit createEmptyHabit()
     {
         Habit habit = new Habit();
+        habit.freqNum = 1;
+        habit.freqDen = 1;
         habit.save();
         return habit;
     }
diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java
new file mode 100644
index 000000000..f9b85b083
--- /dev/null
+++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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.ActiveAndroidHelper;
+import org.isoron.helpers.DateHelper;
+import org.isoron.uhabits.models.Habit;
+import org.isoron.uhabits.models.Score;
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ScoreListTest
+{
+    private Habit habit;
+
+    @Before
+    public void prepare()
+    {
+        HabitFixtures.purgeHabits();
+        DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME);
+        habit = HabitFixtures.createEmptyHabit();
+    }
+
+    @After
+    public void tearDown()
+    {
+        DateHelper.setFixedLocalTime(null);
+    }
+
+    @Test
+    public void invalidateNewerThan()
+    {
+        assertThat(habit.scores.getTodayValue(), equalTo(0));
+
+        toggleRepetitions(0, 2);
+        assertThat(habit.scores.getTodayValue(), equalTo(1948077));
+
+        habit.freqNum = 1;
+        habit.freqDen = 2;
+        habit.scores.invalidateNewerThan(0);
+
+        assertThat(habit.scores.getTodayValue(), equalTo(1974654));
+    }
+
+    @Test
+    public void getTodayStarValue()
+    {
+        assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.EMPTY_STAR));
+
+        int k = 0;
+        while(habit.scores.getTodayValue() < Score.HALF_STAR_CUTOFF) toggleRepetitions(k, ++k);
+        assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.HALF_STAR));
+
+        while(habit.scores.getTodayValue() < Score.FULL_STAR_CUTOFF) toggleRepetitions(k, ++k);
+        assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.FULL_STAR));
+    }
+
+    @Test
+    public void getTodayValue()
+    {
+        toggleRepetitions(0, 20);
+        assertThat(habit.scores.getTodayValue(), equalTo(12629351));
+    }
+
+    @Test
+    public void getValue()
+    {
+        toggleRepetitions(0, 20);
+
+        int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773,
+                10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023,
+                4507040, 3699107, 2846927, 1948077, 1000000 };
+
+        long current = DateHelper.getStartOfToday();
+        for(int expectedValue : expectedValues)
+        {
+            assertThat(habit.scores.getValue(current), equalTo(expectedValue));
+            current -= DateHelper.millisecondsInOneDay;
+        }
+    }
+
+    @Test
+    public void getAllValues_withoutGroups()
+    {
+        toggleRepetitions(0, 20);
+
+        int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773,
+                10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023,
+                4507040, 3699107, 2846927, 1948077, 1000000 };
+
+        int actualValues[] = habit.scores.getAllValues(1);
+        assertThat(actualValues, equalTo(expectedValues));
+    }
+
+    @Test
+    public void getAllValues_withGroups()
+    {
+        toggleRepetitions(0, 20);
+
+        int expectedValues[] = { 12629351, 11006461, 7272612, 2800230 };
+
+        int actualValues[] = habit.scores.getAllValues(7);
+        assertThat(actualValues, equalTo(expectedValues));
+    }
+
+    private void toggleRepetitions(final int from, final int to)
+    {
+        ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command()
+        {
+            @Override
+            public void execute()
+            {
+                long today = DateHelper.getStartOfToday();
+                for (int i = from; i < to; i++)
+                    habit.repetitions.toggle(today - i * DateHelper.millisecondsInOneDay);
+            }
+        });
+    }
+}
diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java
new file mode 100644
index 000000000..d666ff1df
--- /dev/null
+++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.graphics.Color;
+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.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+import org.isoron.uhabits.models.Score;
+import org.isoron.uhabits.models.Repetition;
+import org.isoron.uhabits.models.Checkmark;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ScoreTest
+{
+    @Test
+    public void compute_withDailyHabit()
+    {
+        int checkmark = Checkmark.UNCHECKED;
+        assertThat(Score.compute(1, 0, checkmark), equalTo(0));
+        assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
+        assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775));
+        assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478));
+
+        checkmark = Checkmark.CHECKED_IMPLICITLY;
+        assertThat(Score.compute(1, 0, checkmark), equalTo(0));
+        assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
+        assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775));
+        assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478));
+
+        checkmark = Checkmark.CHECKED_EXPLICITLY;
+        assertThat(Score.compute(1, 0, checkmark), equalTo(1000000));
+        assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387));
+        assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775));
+        assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
+    }
+
+    @Test
+    public void compute_withNonDailyHabit()
+    {
+        int checkmark = Checkmark.CHECKED_EXPLICITLY;
+        assertThat(Score.compute(1/3.0, 0, checkmark), equalTo(1000000));
+        assertThat(Score.compute(1/3.0, 5000000, checkmark), equalTo(5916180));
+        assertThat(Score.compute(1/3.0, 10000000, checkmark), equalTo(10832360));
+        assertThat(Score.compute(1/3.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
+
+        assertThat(Score.compute(1/7.0, 0, checkmark), equalTo(1000000));
+        assertThat(Score.compute(1/7.0, 5000000, checkmark), equalTo(5964398));
+        assertThat(Score.compute(1/7.0, 10000000, checkmark), equalTo(10928796));
+        assertThat(Score.compute(1/7.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
+    }
+
+    @Test
+    public void getStarStatus()
+    {
+        Score s = new Score();
+
+        s.score = Score.FULL_STAR_CUTOFF + 1;
+        assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR));
+
+        s.score = Score.FULL_STAR_CUTOFF;
+        assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR));
+
+        s.score = Score.FULL_STAR_CUTOFF - 1;
+        assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
+        
+        s.score = Score.HALF_STAR_CUTOFF + 1;
+        assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
+
+        s.score = Score.HALF_STAR_CUTOFF;
+        assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR));
+        
+        s.score = Score.HALF_STAR_CUTOFF - 1;
+        assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR));
+
+        s.score = 0;
+        assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR));
+    }
+}
diff --git a/app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java b/app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java
new file mode 100644
index 000000000..8e89bb7e8
--- /dev/null
+++ b/app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java
@@ -0,0 +1,29 @@
+package org.isoron.helpers;
+
+import com.activeandroid.ActiveAndroid;
+
+public class ActiveAndroidHelper
+{
+    public interface Command
+    {
+        void execute();
+    }
+
+    public static void executeAsTransaction(Command command)
+    {
+        ActiveAndroid.beginTransaction();
+        try
+        {
+            command.execute();
+            ActiveAndroid.setTransactionSuccessful();
+        }
+        catch (RuntimeException e)
+        {
+            throw e;
+        }
+        finally
+        {
+            ActiveAndroid.endTransaction();
+        }
+    }
+}
diff --git a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java
index aaca085d6..e816227cc 100644
--- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java
+++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java
@@ -50,7 +50,7 @@ public class EditHabitCommand extends Command
         {
             habit.checkmarks.deleteNewerThan(0);
             habit.streaks.deleteNewerThan(0);
-            habit.scores.deleteNewerThan(0);
+            habit.scores.invalidateNewerThan(0);
         }
     }
 
@@ -65,7 +65,7 @@ public class EditHabitCommand extends Command
         {
             habit.checkmarks.deleteNewerThan(0);
             habit.streaks.deleteNewerThan(0);
-            habit.scores.deleteNewerThan(0);
+            habit.scores.invalidateNewerThan(0);
         }
     }
 
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 ba6c82d41..4be27ec21 100644
--- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java
+++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java
@@ -133,7 +133,7 @@ public class ShowHabitFragment extends Fragment
     {
         RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing);
         scoreRing.setColor(habit.color);
-        scoreRing.setPercentage((float) habit.scores.getNewestValue() / Score.MAX_SCORE);
+        scoreRing.setPercentage((float) habit.scores.getTodayValue() / Score.MAX_VALUE);
     }
 
     private void updateHeaders(View view)
diff --git a/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java
index c1a2c9599..d5690e0a1 100644
--- a/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java
+++ b/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java
@@ -75,7 +75,7 @@ public class CSVExporter
 
     public String formatScore(int score)
     {
-        return String.format("%.2f", ((float) score) / Score.MAX_SCORE);
+        return String.format("%.2f", ((float) score) / Score.MAX_VALUE);
     }
 
     private void writeScores(String dirPath, Habit habit) throws IOException
diff --git a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java b/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java
index 8a0d93661..2890c872d 100644
--- a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java
+++ b/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java
@@ -144,7 +144,7 @@ public class HabitListLoader
                     if (isCancelled()) return null;
 
                     Long id = h.getId();
-                    newScores.put(id, h.scores.getNewestValue());
+                    newScores.put(id, h.scores.getTodayValue());
                     newCheckmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
 
                     publishProgress(current++, newHabits.size());
@@ -213,7 +213,7 @@ public class HabitListLoader
 
                 Habit h = Habit.get(id);
                 habits.put(id, h);
-                scores.put(id, h.scores.getNewestValue());
+                scores.put(id, h.scores.getTodayValue());
                 checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo));
 
                 return null;
diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java
index 362fea02a..d52a63fa0 100644
--- a/app/src/main/java/org/isoron/uhabits/models/Habit.java
+++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java
@@ -117,21 +117,25 @@ public class Habit extends Model
     /**
      * List of streaks belonging to this habit.
      */
+    @NonNull
     public StreakList streaks;
 
     /**
      * List of scores belonging to this habit.
      */
+    @NonNull
     public ScoreList scores;
 
     /**
      * List of repetitions belonging to this habit.
      */
+    @NonNull
     public RepetitionList repetitions;
 
     /**
      * List of checkmarks belonging to this habit.
      */
+    @NonNull
     public CheckmarkList checkmarks;
 
     /**
@@ -142,7 +146,11 @@ public class Habit extends Model
     public Habit(Habit model)
     {
         copyAttributes(model);
-        initializeLists();
+
+        checkmarks = new CheckmarkList(this);
+        streaks = new StreakList(this);
+        scores = new ScoreList(this);
+        repetitions = new RepetitionList(this);
     }
 
     /**
@@ -157,15 +165,11 @@ public class Habit extends Model
         this.archived = 0;
         this.freqDen = 7;
         this.freqNum = 3;
-        initializeLists();
-    }
 
-    private void initializeLists()
-    {
+        checkmarks = new CheckmarkList(this);
         streaks = new StreakList(this);
         scores = new ScoreList(this);
         repetitions = new RepetitionList(this);
-        checkmarks = new CheckmarkList(this);
     }
 
     /**
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 c7b91351a..475f6e1fa 100644
--- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
+++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java
@@ -107,7 +107,7 @@ public class RepetitionList
             rep.save();
         }
 
-        habit.scores.deleteNewerThan(timestamp);
+        habit.scores.invalidateNewerThan(timestamp);
         habit.checkmarks.deleteNewerThan(timestamp);
         habit.streaks.deleteNewerThan(timestamp);
     }
diff --git a/app/src/main/java/org/isoron/uhabits/models/Score.java b/app/src/main/java/org/isoron/uhabits/models/Score.java
index 2c3cd1b9d..5eba480e9 100644
--- a/app/src/main/java/org/isoron/uhabits/models/Score.java
+++ b/app/src/main/java/org/isoron/uhabits/models/Score.java
@@ -26,16 +26,94 @@ import com.activeandroid.annotation.Table;
 @Table(name = "Score")
 public class Score extends Model
 {
+    /**
+     * Minimum score value required to earn half a star.
+     */
     public static final int HALF_STAR_CUTOFF =  9629750;
+
+    /**
+     * Minimum score value required to earn a full star.
+     */
     public static final int FULL_STAR_CUTOFF = 15407600;
-    public static final int MAX_SCORE        = 19259500;
 
+    /**
+     * Maximum score value attainable by any habit.
+     */
+    public static final int MAX_VALUE = 19259478;
+
+    /**
+     * Status indicating that the habit has not earned any star.
+     */
+    public static final int EMPTY_STAR = 0;
+
+    /**
+     * Status indicating that the habit has earned half a star.
+     */
+    public static final int HALF_STAR = 1;
+
+    /**
+     * Status indicating that the habit has earned a full star.
+     */
+    public static final int FULL_STAR = 2;
+
+    /**
+     * Habit to which this score belongs to.
+     */
     @Column(name = "habit")
     public Habit habit;
 
+    /**
+     * Timestamp of the day to which this score applies. Time of day should be midnight (UTC).
+     */
     @Column(name = "timestamp")
     public Long timestamp;
 
+    /**
+     * Value of the score.
+     */
     @Column(name = "score")
     public Integer score;
+
+    /**
+     * Given the frequency of the habit, the previous score, and the value of the current checkmark,
+     * computes the current score for the habit.
+     *
+     * The frequency of the habit is the number of repetitions divided by the length of the
+     * interval. For example, a habit that should be repeated 3 times in 8 days has frequency 3.0 /
+     * 8.0 = 0.375.
+     *
+     * The checkmarkValue should be UNCHECKED, CHECKED_IMPLICITLY or CHECK_EXPLICITLY.
+     *
+     * @param frequency the frequency of the habit
+     * @param previousScore the previous score of the habit
+     * @param checkmarkValue the value of the current checkmark
+     *
+     * @return the current score
+     */
+    public static int compute(double frequency, int previousScore, int checkmarkValue)
+    {
+        double multiplier = Math.pow(0.5, 1.0 / (14.0 / frequency - 1));
+        int score = (int) (previousScore * multiplier);
+
+        if (checkmarkValue == Checkmark.CHECKED_EXPLICITLY)
+        {
+            score += 1000000;
+            score = Math.min(score, Score.MAX_VALUE);
+        }
+
+        return score;
+    }
+
+    /**
+     * Return the current star status for the habit, which can one of EMPTY_STAR, HALF_STAR or
+     * FULL_STAR.
+     *
+     * @return current star status
+     */
+    public int getStarStatus()
+    {
+        if(score >= Score.FULL_STAR_CUTOFF) return Score.FULL_STAR;
+        if(score >= Score.HALF_STAR_CUTOFF) return Score.HALF_STAR;
+        return Score.EMPTY_STAR;
+    }
 }
diff --git a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java
index c703e393a..f432151fb 100644
--- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java
+++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java
@@ -21,42 +21,73 @@ package org.isoron.uhabits.models;
 
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 
 import com.activeandroid.ActiveAndroid;
 import com.activeandroid.Cache;
 import com.activeandroid.query.Delete;
+import com.activeandroid.query.From;
 import com.activeandroid.query.Select;
 
+import org.isoron.helpers.ActiveAndroidHelper;
 import org.isoron.helpers.DateHelper;
 
 public class ScoreList
 {
+    @NonNull
     private Habit habit;
 
-    public ScoreList(Habit habit)
+    /**
+     * Constructs a new ScoreList associated with the given habit.
+     *
+     * @param habit the habit this list should be associated with
+     */
+    public ScoreList(@NonNull Habit habit)
     {
         this.habit = habit;
     }
 
-    public int getCurrentStarStatus()
+    protected From select()
     {
-        int score = getNewestValue();
+        return new Select()
+                .from(Score.class)
+                .where("habit = ?", habit.getId())
+                .orderBy("timestamp desc");
+    }
 
-        if(score >= Score.FULL_STAR_CUTOFF) return 2;
-        else if(score >= Score.HALF_STAR_CUTOFF) return 1;
-        else return 0;
+    /**
+     * Returns the most recent score already computed. If no score has been computed yet, returns
+     * null.
+     *
+     * @return newest score, or null if none exist
+     */
+    @Nullable
+    protected Score findNewest()
+    {
+        return select().limit(1).executeSingle();
     }
 
-    public Score getNewest()
+    /**
+     * Returns the value of the most recent score that was already computed. If no score has been
+     * computed yet, returns zero.
+     *
+     * @return value of newest score, or zero if none exist
+     */
+    protected int findNewestValue()
     {
-        return new Select().from(Score.class)
-                .where("habit = ?", habit.getId())
-                .orderBy("timestamp desc")
-                .limit(1)
-                .executeSingle();
+        Score newest = findNewest();
+        if(newest == null) return 0;
+        else return newest.score;
     }
 
-    public void deleteNewerThan(long timestamp)
+    /**
+     * Marks all scores that have timestamp equal to or newer than the given timestamp as invalid.
+     * Any following getValue calls will trigger the scores to be recomputed.
+     *
+     * @param timestamp the oldest timestamp that should be invalidated
+     */
+    public void invalidateNewerThan(long timestamp)
     {
         new Delete().from(Score.class)
                 .where("habit = ?", habit.getId())
@@ -64,79 +95,137 @@ public class ScoreList
                 .execute();
     }
 
-    public Integer getNewestValue()
+    /**
+     * Computes and saves the scores that are missing inside a given time interval.  Scores that
+     * have already been computed are skipped, therefore there is no harm in calling this function
+     * more times, or with larger intervals, than strictly needed. The endpoints of the interval are
+     * included.
+     *
+     * This function assumes that there are no gaps on the scores. That is, if the newest score has
+     * timestamp t, then every score with timestamp lower than t has already been computed. 
+     *
+     * @param from timestamp of the beginning of the interval
+     * @param to timestamp of the end of the time interval
+     */
+    protected void compute(long from, long to)
     {
-        int beginningScore;
-        long beginningTime;
-
-        long today = DateHelper.getStartOfDay(DateHelper.getLocalTime());
-        long day = DateHelper.millisecondsInOneDay;
-
-        double freq = ((double) habit.freqNum) / habit.freqDen;
-        double multiplier = Math.pow(0.5, 1.0 / (14.0 / freq - 1));
-
-        Score newestScore = getNewest();
-        if (newestScore == null)
-        {
-            Repetition oldestRep = habit.repetitions.getOldest();
-            if (oldestRep == null) return 0;
-            beginningTime = oldestRep.timestamp;
-            beginningScore = 0;
-        }
-        else
-        {
-            beginningTime = newestScore.timestamp + day;
-            beginningScore = newestScore.score;
-        }
+        final long day = DateHelper.millisecondsInOneDay;
+        final double freq = ((double) habit.freqNum) / habit.freqDen;
 
-        long nDays = (today - beginningTime) / day;
-        if (nDays < 0) return newestScore.score;
+        int newestScoreValue = findNewestValue();
+        Score newestScore = findNewest();
 
-        int reps[] = habit.checkmarks.getValues(beginningTime, today);
+        if(newestScore != null)
+            from = newestScore.timestamp + day;
 
-        ActiveAndroid.beginTransaction();
-        int lastScore = beginningScore;
+        final int checkmarkValues[] = habit.checkmarks.getValues(from, to);
+        final int firstScore = newestScoreValue;
+        final long beginning = from;
 
-        try
+        ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command()
         {
-            for (int i = 0; i < reps.length; i++)
+            @Override
+            public void execute()
             {
-                Score s = new Score();
-                s.habit = habit;
-                s.timestamp = beginningTime + day * i;
-                s.score = (int) (lastScore * multiplier);
-                if (reps[reps.length - i - 1] == 2)
+                int lastScore = firstScore;
+
+                for (int i = 0; i < checkmarkValues.length; i++)
                 {
-                    s.score += 1000000;
-                    s.score = Math.min(s.score, Score.MAX_SCORE);
-                }
-                s.save();
+                    int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1];
 
-                lastScore = s.score;
+                    Score s = new Score();
+                    s.habit = habit;
+                    s.timestamp = beginning + day * i;
+                    s.score = lastScore = Score.compute(freq, lastScore, checkmarkValue);
+                    s.save();
+                }
             }
+        });
+    }
 
-            ActiveAndroid.setTransactionSuccessful();
-        } finally
-        {
-            ActiveAndroid.endTransaction();
-        }
+    /**
+     * Returns the score for a certain day.
+     *
+     * @param timestamp the timestamp for the day
+     * @return the score for the day
+     */
+    @Nullable
+    protected Score get(long timestamp)
+    {
+        Repetition oldestRep = habit.repetitions.getOldest();
+        if(oldestRep == null) return null;
 
-        return lastScore;
+        compute(oldestRep.timestamp, timestamp);
+
+        return select().where("timestamp = ?", timestamp).executeSingle();
     }
 
-    public int[] getAllValues(Long fromTimestamp, Long toTimestamp, Long divisor)
+    /**
+     * Returns the value of the score for a given day.
+     *
+     * @param timestamp the timestamp of a day
+     * @return score for that day
+     */
+    public int getValue(long timestamp)
     {
-        // Force rebuild of the score table
-        getNewestValue();
+        Score s = get(timestamp);
+        if(s == null) return 0;
+        else return s.score;
+    }
 
-        Long offset = toTimestamp - (divisor - 1) * DateHelper.millisecondsInOneDay;
+    /**
+     * Returns the values of all the scores, from day of the first repetition until today, grouped
+     * in chunks of specified size.
+     *
+     * If the group size is one, then the value of each score is returned individually. If the group
+     * is, for example, seven, then the days are grouped in groups of seven consecutive days.
+     *
+     * The values are returned in an array of integers, with one entry for each group of days in the
+     * interval. This value corresponds to the average of the scores for the days inside the group.
+     * The first entry corresponds to the ending of the interval (that is, the most recent group of
+     * days). The last entry corresponds to the beginning of the interval. As usual, the time of the
+     * day for the timestamps should be midnight (UTC). The endpoints of the interval are included.
+     *
+     * The values are returned in an integer array. There is one entry for each day inside the
+     * interval. The first entry corresponds to today, while the last entry corresponds to the
+     * day of the oldest repetition.
+     *
+     * @param divisor the size of the groups
+     * @return array of values, with one entry for each group of days
+     */
+    @NonNull
+    public int[] getAllValues(long divisor)
+    {
+        Repetition oldestRep = habit.repetitions.getOldest();
+        if(oldestRep == null) return new int[0];
+
+        long fromTimestamp = oldestRep.timestamp;
+        long toTimestamp = DateHelper.getStartOfToday();
+        return getValues(fromTimestamp, toTimestamp, divisor);
+    }
+
+    /**
+     * Same as getAllValues(long), but using a specified interval.
+     *
+     * @param from beginning of the interval (included)
+     * @param to end of the interval (included)
+     * @param divisor size of the groups
+     * @return array of values, with one entry for each group of days
+     */
+    @NonNull
+    protected int[] getValues(long from, long to, long divisor)
+    {
+        compute(from, to);
+
+        divisor *= DateHelper.millisecondsInOneDay;
+        Long offset = to + divisor - 1;
 
         String query = "select ((timestamp - ?) / ?) as time, avg(score) from Score " +
-                "where habit = ? and timestamp > ? and timestamp <= ? " +
+                "where habit = ? and timestamp >= ? and timestamp <= ? " +
                 "group by time order by time desc";
 
-        String params[] = { offset.toString(), divisor.toString(), habit.getId().toString(),
-                fromTimestamp.toString(), toTimestamp.toString()};
+        String params[] = { offset.toString(), Long.toString(divisor), habit.getId().toString(),
+                Long.toString(from), Long.toString(to) };
 
         SQLiteDatabase db = Cache.openDatabase();
         Cursor cursor = db.rawQuery(query, params);
@@ -148,22 +237,45 @@ public class ScoreList
 
         do
         {
-            scores[k++] = (int) cursor.getLong(1);
+            scores[k++] = (int) cursor.getFloat(1);
         }
         while (cursor.moveToNext());
 
         cursor.close();
         return scores;
+    }
 
+    /**
+     * Returns the score for today.
+     *
+     * @return score for today
+     */
+    @Nullable
+    protected Score getToday()
+    {
+        return get(DateHelper.getStartOfToday());
     }
 
-    public int[] getAllValues(long divisor)
+    /**
+     * Returns the value of the score for today.
+     *
+     * @return value of today's score
+     */
+    public int getTodayValue()
     {
-        Repetition oldestRep = habit.repetitions.getOldest();
-        if(oldestRep == null) return new int[0];
+        return getValue(DateHelper.getStartOfToday());
+    }
 
-        long fromTimestamp = oldestRep.timestamp;
-        long toTimestamp = DateHelper.getStartOfToday();
-        return getAllValues(fromTimestamp, toTimestamp, divisor);
+    /**
+     * Returns the star status for today. The returned value is either Score.EMPTY_STAR,
+     * Score.HALF_STAR or Score.FULL_STAR.
+     *
+     * @return star status for today
+     */
+    public int getTodayStarStatus()
+    {
+        Score score = getToday();
+        if(score != null) return score.getStarStatus();
+        else return Score.EMPTY_STAR;
     }
 }
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 ba310c70f..6de4cffeb 100644
--- a/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java
@@ -115,7 +115,7 @@ public class CheckmarkView extends View
     public void setHabit(Habit habit)
     {
         this.check_status = habit.checkmarks.getTodayValue();
-        this.star_status = habit.scores.getCurrentStarStatus();
+        this.star_status = habit.scores.getTodayStarStatus();
         this.primaryColor = Color.argb(230, Color.red(habit.color), Color.green(habit.color), Color.blue(habit.color));
         this.label = habit.name;
         updateLabel();
diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java
index c64a6101b..355c6ba08 100644
--- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java
+++ b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java
@@ -171,7 +171,7 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
         else
         {
             if (habit == null) return;
-            scores = habit.scores.getAllValues(BUCKET_SIZE * DateHelper.millisecondsInOneDay);
+            scores = habit.scores.getAllValues(BUCKET_SIZE);
         }
 
         invalidate();
@@ -181,13 +181,13 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
     {
         Random random = new Random();
         scores = new int[100];
-        scores[0] = Score.MAX_SCORE / 2;
+        scores[0] = Score.MAX_VALUE / 2;
 
         for(int i = 1; i < 100; i++)
         {
-            int step = Score.MAX_SCORE / 10;
+            int step = Score.MAX_VALUE / 10;
             scores[i] = scores[i - 1] + random.nextInt(step * 2) - step;
-            scores[i] = Math.max(0, Math.min(Score.MAX_SCORE, scores[i]));
+            scores[i] = Math.max(0, Math.min(Score.MAX_VALUE, scores[i]));
         }
     }
 
@@ -224,7 +224,7 @@ public class HabitScoreView extends ScrollableDataView implements HabitDataView
             int offset = nColumns - k - 1 + getDataOffset();
             if(offset < scores.length) score = scores[offset];
 
-            double sRelative = ((double) score) / Score.MAX_SCORE;
+            double sRelative = ((double) score) / Score.MAX_VALUE;
             int height = (int) (columnHeight * sRelative);
 
             rect.set(0, 0, baseSize, baseSize);

From e829bb8109362219bea8508ac8cf375fbcf7b202 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Fri, 18 Mar 2016 07:18:50 -0400
Subject: [PATCH 044/175] Enable parallel and daemon (gradle)

---
 gradle.properties | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 gradle.properties

diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 000000000..0d7f0870e
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.parallel=true
+org.gradle.daemon=true
+org.gradle.jvmargs=-Xms256m -Xmx1024m

From 55c058ff425afa2115abe5ab320a213a03783ced Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Fri, 18 Mar 2016 07:24:26 -0400
Subject: [PATCH 045/175] Minor spelling mistakes

---
 app/src/main/java/org/isoron/uhabits/models/Habit.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java
index d52a63fa0..38269cfc6 100644
--- a/app/src/main/java/org/isoron/uhabits/models/Habit.java
+++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java
@@ -265,7 +265,7 @@ public class Habit extends Model
     }
 
     /**
-     * Changes the position of a habit in the list.
+     * Changes the position of a habit on the list.
      *
      * @param from the habit that should be moved
      * @param to the habit that currently occupies the desired position
@@ -292,7 +292,7 @@ public class Habit extends Model
     }
 
     /**
-     * Recompute the field position for every habit in the database. It should never be necessary
+     * Recomputes the position for every habit in the database. It should never be necessary
      * to call this method.
      */
     public static void rebuildOrder()

From b5bc347624c720e74246a28c10c1deb996182e03 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Fri, 18 Mar 2016 07:42:59 -0400
Subject: [PATCH 046/175] Use default instead of null for reminderDays

---
 .../isoron/uhabits/fragments/EditHabitFragment.java |  6 +-----
 .../main/java/org/isoron/uhabits/models/Habit.java  | 13 +++++++++----
 2 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java
index eddf1068e..a84455cd2 100644
--- a/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java
+++ b/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java
@@ -156,11 +156,7 @@ public class EditHabitFragment extends DialogFragment
             modifiedHabit.reminderDays = savedInstanceState.getInt("reminderDays", -1);
 
             if(modifiedHabit.reminderMin < 0)
-            {
-                modifiedHabit.reminderMin = null;
-                modifiedHabit.reminderHour = null;
-                modifiedHabit.reminderDays = 127;
-            }
+                modifiedHabit.clearReminder();
         }
 
         tvFreqNum.append(modifiedHabit.freqNum.toString());
diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java
index 38269cfc6..cc89521e4 100644
--- a/app/src/main/java/org/isoron/uhabits/models/Habit.java
+++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java
@@ -35,6 +35,7 @@ import com.activeandroid.query.Update;
 import com.activeandroid.util.SQLiteUtils;
 
 import org.isoron.helpers.ColorHelper;
+import org.isoron.helpers.DateHelper;
 
 import java.util.List;
 import java.util.Locale;
@@ -95,9 +96,10 @@ public class Habit extends Model
     /**
      * Days of the week the reminder should be shown. This field can be converted to a list of
      * booleans using the method DateHelper.unpackWeekdayList and converted back to an integer by
-     * using the method DateHelper.packWeekdayList. If there is no reminder, it equals null.
+     * using the method DateHelper.packWeekdayList. If the habit has no reminders, this value
+     * should be ignored.
      */
-    @Nullable
+    @NonNull
     @Column(name = "reminder_days")
     public Integer reminderDays;
 
@@ -145,6 +147,8 @@ public class Habit extends Model
      */
     public Habit(Habit model)
     {
+        reminderDays = DateHelper.ALL_WEEK_DAYS;
+
         copyAttributes(model);
 
         checkmarks = new CheckmarkList(this);
@@ -165,6 +169,7 @@ public class Habit extends Model
         this.archived = 0;
         this.freqDen = 7;
         this.freqNum = 3;
+        this.reminderDays = DateHelper.ALL_WEEK_DAYS;
 
         checkmarks = new CheckmarkList(this);
         streaks = new StreakList(this);
@@ -453,7 +458,7 @@ public class Habit extends Model
      */
     public boolean hasReminder()
     {
-        return (reminderHour != null && reminderMin != null && reminderDays != null);
+        return (reminderHour != null && reminderMin != null);
     }
 
     /**
@@ -463,6 +468,6 @@ public class Habit extends Model
     {
         reminderHour = null;
         reminderMin = null;
-        reminderDays = null;
+        reminderDays = DateHelper.ALL_WEEK_DAYS;
     }
 }

From 6826bd1ddc25a4d124d412dcd76af45e3468dab2 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Fri, 18 Mar 2016 10:36:56 -0400
Subject: [PATCH 047/175] Update test

---
 .../java/org/isoron/uhabits/unit/models/HabitTest.java          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 a4e3d5678..e6be6b150 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
@@ -56,10 +56,10 @@ public class HabitTest
         assertThat(habit.archived, is(0));
         assertThat(habit.highlight, is(0));
 
-        assertThat(habit.reminderDays, is(nullValue()));
         assertThat(habit.reminderHour, is(nullValue()));
         assertThat(habit.reminderMin, is(nullValue()));
 
+        assertThat(habit.reminderDays, is(not(nullValue())));
         assertThat(habit.streaks, is(not(nullValue())));
         assertThat(habit.scores, is(not(nullValue())));
         assertThat(habit.repetitions, is(not(nullValue())));

From 326cb8f73f02fdf0a508a60ac21263769658fd34 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Fri, 18 Mar 2016 10:42:47 -0400
Subject: [PATCH 048/175] Minor changes to javadoc and method visibility

---
 .../isoron/uhabits/models/CheckmarkList.java  | 20 ++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

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 cb96f74af..d4fd67617 100644
--- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java
+++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java
@@ -71,7 +71,7 @@ public class CheckmarkList
     @NonNull
     public int[] getValues(long fromTimestamp, long toTimestamp)
     {
-        buildCache(fromTimestamp, toTimestamp);
+        compute(fromTimestamp, toTimestamp);
         if(fromTimestamp > toTimestamp) return new int[0];
 
         String query = "select value, timestamp from Checkmarks where " +
@@ -102,8 +102,8 @@ public class CheckmarkList
     }
 
     /**
-     * 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.
+     * 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
@@ -130,7 +130,7 @@ public class CheckmarkList
      * @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)
+    protected void compute(long from, long to)
     {
         long day = DateHelper.millisecondsInOneDay;
 
@@ -191,9 +191,11 @@ public class CheckmarkList
     /**
      * Returns newest checkmark that has already been computed. Ignores any checkmark that has
      * timestamp in the future. This does not update the cache.
+     *
+     * @return newest checkmark already computed
      */
     @Nullable
-    private Checkmark findNewest()
+    protected Checkmark findNewest()
     {
         return new Select().from(Checkmark.class)
                 .where("habit = ?", habit.getId())
@@ -205,17 +207,21 @@ public class CheckmarkList
 
     /**
      * Returns the checkmark for today.
+     *
+     * @return checkmark for today
      */
     @Nullable
     public Checkmark getToday()
     {
         long today = DateHelper.getStartOfToday();
-        buildCache(today, today);
+        compute(today, today);
         return findNewest();
     }
 
     /**
-     * Returns the value of today's checkmark. If there is no checkmark today, returns UNCHECKED.
+     * Returns the value of today's checkmark.
+     *
+     * @return value of today's checkmark
      */
     public int getTodayValue()
     {

From 7d9a94ae9e617608d85213e1215d4d5ee35c9630 Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Fri, 18 Mar 2016 10:58:18 -0400
Subject: [PATCH 049/175] Add line to disable large tests

---
 app/build.gradle | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/build.gradle b/app/build.gradle
index 14e4da3d0..9f90fd1ab 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -13,6 +13,7 @@ android {
         buildConfigField "String", "databaseFilename", "\"uhabits.db\""
 
         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+        //testInstrumentationRunnerArgument "size", "small"
     }
 
     buildTypes {

From e693504183d6d04dbf3c5a785e9dbabe7b4a39fe Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Fri, 18 Mar 2016 11:01:25 -0400
Subject: [PATCH 050/175] Update translations

---
 app/src/main/res/values-pl/strings.xml | 2 +-
 app/src/main/res/values-ru/strings.xml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index b4c9108ad..08c881877 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -35,7 +35,7 @@
     "Zmieniono nawyk."
 
     
-    "Zmieniono nawyk spowrotem."
+    "Zmieniono nawyk z powrotem."
     "Nawyki zarchiwizowane."
     "Nawyki odarchiwizowane."
     "Przegląd"
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 0ae126809..824b474af 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -1,6 +1,6 @@
 
 
+
+
+    "Loop Analizador de Hábitos"
+    "Hábitos"
+    "Configuración"
+    "Editar"
+    "Eliminar"
+    "Archivar"
+    "Desarchivar"
+    "Agregar hábito"
+    "Cambiar color"
+    "Hábito creado."
+    "Hábitos eliminados."
+    "Hábitos restaurados."
+    "Nada que deshacer."
+    "Nada que rehacer."
+    "Hábito cambiado."
+
+    
+    "Hábito cambiado devuelta."
+    "Hábitos archivados."
+    "Hábitos desarchivados."
+    "Visión general"
+    "Fuerza del hábito"
+    "Historial"
+    "Borrar"
+    "Pregunta (Has ... hoy?)"
+
+    
+    "Repetir"
+    "veces"
+    "días"
+    "Recordatorio"
+    "Descartar"
+    "Guardar"
+    "Rachas"
+    "No hay ningún hábito activo"
+    "Mantener apretado para"
+    "No"
+    "Nombre no puede estar en blanco."
+    "Número debe ser positivo."
+    "Puedes tener como máximo una repetición por día"
+    "Crear hábito"
+    "Editar hábito"
+    "Marcar"
+    "Posponer"
+
+    
+    "Bienvenido"
+    "Loop Analizador de Hábitos te ayuda a crear y mantener buenos hábitos."
+    "Crea algunos hábitos nuevos"
+    "Cada día, después de realizar tu hábito, pon una marca en la app."
+    "Sigue haciéndolo."
+    "Los hábitos realizados consistentemente por un largo tiempo ganarán una estrella completa."
+    "Haz un seguimiento de tu progreso"
+    "Detallados gráficos muestran como han mejorado tus hábitos con el tiempo."
+    "15 minutos"
+    "30 minutos"
+    "1 hora"
+    "2 horas"
+    "4 horas"
+    "8 horas"
+    "Marca las repeticiones con una corta pulsación."
+    "Más cómodo, pero puede causar marcas accidentales."
+    "Tiempo de espera al posponer recordatorios."
+    "Valora esta app en Google Play"
+    "Enviar sugerencias al desarrollador"
+    "Ver código fuente en GitHub"
+    "Ver la introducción de la app"
+    "Enlaces"
+    "Comportamiento"
+    "Nombre"
+    "Ver archivados"
+    "Configuración"
+    "Intervalo de espera"
+    "¿Sabías qué?"
+    "Para reordenar las entradas, mantén la pulsación sobre el nombre del hábito, después arrástralo a su posición correcta."
+    "Puedes ver más días al poner tu teléfono en modo horizontal."
+    "Eliminar Hábitos"
+    "Los hábitos serán eliminados permanentemente. Esta acción no se puede deshacer."
+    "Fines de semana"
+    "Días laborables"
+    "Cada día"
+    "Seleccionar días"
+    "Exportar datos"
+    "Hecho"
+    "Quitar"
+    "Seleccionar horas"
+    "Seleccionar"
+
+    
+    "Crea buenos hábitos y haz un seguimiento de su progreso a lo largo del tiempo (sin anuncios)"
+    "Loop te ayuda a crear y mantener buenos hábitos, permitiéndote alcanzar tus metas a largo plazo. Detallados gráficos y estadísticas muestran como tus hábitos mejoran con el tiempo. No existe ningún anuncio y es de código abierto."
+"<b>Una interfaz simple, bella y moderna</b>
+Loop tiene una interfaz minimalista que es fácil de usar y sigue los principios del material design."
+"<b>Puntuación del hábito</b>
+Además de mostrar tu racha actual, Loop tiene un algoritmo avanzado para calcular la fuerza de tus hábitos. Cada repetición hace tu hábito más fuerte y cada día fallido lo hace más débil. Sin embargo, unos pocos días después de una larga racha no destruirán completamente todo tu progreso."
+"<b>Detallados gráficos y estadísticas</b>
+Observa claramente como tus hábitos han mejorado con el tiempo con bellos y detallados gráficos. Ve hacia atrás para ver el historial completo del hábito."
+"<b>Horarios flexibles</b>
+Soporta hábitos diarios y hábitos con repeticiones más complejas, como 3 veces por semana; una vez cada varias semanas; o cada día."
+"<b>Recordatorios</b>
+Crea recordatorios individuales para cada hábito a una hora determinada del día. Fácilmente marcables, descartables o posponibles directamente desde la notificación, sin abrir la app."
+"<b>Completamente sin anuncios y de código abierto</b>
+No existe ningún tipo de publicidad, notificaciones molestas o permisos intrusivos, y nunca los habrá. Todo el código está disponible bajo GPLv3."
+"<b>Optimizado para smartwatches</b>
+Los recordatorios se pueden marcar, posponer o descartar directamente desde tu reloj Android Wear."
+    "Acerca de"
+    "Traductores"
+    "Desarrolladores"
+
+    
+    "Versión %s"
+    "Frecuencia"
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
new file mode 100644
index 000000000..fc3704036
--- /dev/null
+++ b/app/src/main/res/values-ko/strings.xml
@@ -0,0 +1,135 @@
+
+
+
+
+    "Loop 습관제조기"
+    "습관"
+    "설정"
+    "수정"
+    "삭제"
+    "보관"
+    "제거"
+    "습관 만들기"
+    "색상 정하기"
+    "습관을 시작합니다."
+    "습관을 지웠습니다."
+    "습관을 복원합니다."
+    "복원할 것이 없습니다."
+    "복원할 것이 없습니다."
+    "습관을 수정했습니다."
+
+    
+    "습관을 복원했습니다."
+    "습관을 보관합니다."
+    "습관을 제거합니다."
+    "홈"
+    "습관 유지정도"
+    "기록"
+    "지우기"
+    "질문 (오늘 ... 했나요?)"
+
+    
+    "반복"
+    "번"
+    "일 동안"
+    "알림"
+    "버리기"
+    "저장"
+    "이어지기"
+    "습관이 없습니다"
+    "길게 눌러 선택/선택제거"
+    "끔"
+    "제목을 적어주세요."
+    "숫자는 0보다 커야합니다."
+    "하루에 한 번 반복만 가능합니다."
+    "습관 만들기"
+    "습관 수정하기"
+    "선택"
+    "나중에"
+
+    
+    "안녕하세요"
+    "Loop은 당신이 좋은 습관을 만들고 유지하도록 도와줍니다."
+    "새로운 습관을 만들어 보세요."
+    "매일매일, 습관을 수행한 뒤에, 앱에 기록하세요."
+    "계속 반복하세요"
+    "일정 시간동안 유지된 습관은 참잘했어요도장을 얻습니다."
+    "습관을 관리하세요"
+    "그래프를 보고 습관이 유지되는지를 체크할 수 있습니다."
+    "15분"
+    "30분"
+    "1시간"
+    "2시간"
+    "4시간"
+    "8시간"
+    "짧게 터치해 반복을 선택/선택제거 하세요."
+    "실수로 체크가 될 수 있지만, 더 편한 체크박스."
+    "반복알림을 미루기"
+    "Google Play에서 평가"
+    "계발자에게 피드백"
+    "Github에서 소스보기"
+    "앱 안내메시지 보기"
+    "링크"
+    "행동"
+    "제목"
+    "보관함 보기"
+    "설정"
+    "미루기"
+    "알고 계시나요?"
+    "습관 순서를 조정하려면, 습관을 길게 눌러 끌어당겨서 다른 위치로 옮길 수 있습니다."
+    "화면을 눕혀서 보면 더 많은 날들을 볼 수 있씁니다."
+    "습관 삭제"
+    "습관을 삭제합니다. 삭제하면 다시 복원할 수 없습니다."
+    "주말"
+    "주중"
+    "매일"
+    "몇일 선택"
+    "정보 내보내기"
+    "완료"
+    "지우기"
+    "시간 선택"
+    "분 선택"
+
+    
+    "좋은 습관을 만들고 관리하세요. (광고 없음)"
+    "Loop은 습관을 만들고 유지하도록 도와주어, 장기간 목표를 달성하도록 합니다. 그래프와 통계를 보고 습관이 만들어지는 과정을 보세요. 오프소스이고, 무광고입니다."
+"<b>아름답고 심플한 디자인</b>
+Loop은 Material디자인을 따라 사용이 편합니다."
+"<b>습관 포인트</b>
+얼마나 오랫동안 습관을 유지했는지 보여줌과 동시에, Loop은 특화된 알고리듬으로 습관의 견고함을 계산합니다. 반복할 수록 강해지고, 빠뜨린 날들이 많아질 수록 포인트는 낮아집니다. 하지만, 오랫동안 유지된 습관은 몇 일을 빠뜨렸다고 해서 포인트가 급강하하지는 않습니다."
+"<b>그래프와 통계</b>
+습관이 어떤 과정을 거쳐서 완성되는지 그래프를 보면 알 수 있습니다.
+스크롤해서 장기간의 과정을 볼 수 있습니다."
+"<b>여러 종류의 스케줄 조정가능</b>
+매일 습관, 혹은 일주일에 3번; 2주에 한번; 같은 여러 종류의 스케줄을 만들 수 있습니다."
+"<b>알림</b>
+"
+"<b>오픈소스, 무광고</b>
+광고, 귀찮은 스팸, 비정상적인 정보요구는 없습니다, 앞으로 쭉. 모든 소스코드는 GPLv3로 라이센스되었습니다."
+"<b>스마트시계에 최적화</B>
+안드로이드 시계에서 바로 알림을 확인, 미루기 혹은 무시할 수 있습니다."
+    "정보"
+    "번역가"
+    "계발자"
+
+    
+    "버전 %s"
+    "반복수"
+
\ No newline at end of file

From c3ff1fbe03258205e7a6a6dfa01fc23fc5c2f40a Mon Sep 17 00:00:00 2001
From: Alinson Xavier 
Date: Sat, 19 Mar 2016 09:46:42 -0400
Subject: [PATCH 052/175] Add quick selection for commonly used habit
 frequencies

Closes #25
---
 NOTICE.md                                     |  19 ++++
 app/build.gradle                              |   1 +
 .../uhabits/ui/MainActivityActions.java       |  20 +++-
 .../uhabits/fragments/EditHabitFragment.java  |  99 +++++++++++++++--
 app/src/main/res/layout/edit_habit.xml        | 104 +++++++++++-------
 app/src/main/res/values-v21/styles.xml        |   2 +
 app/src/main/res/values/colors.xml            |  16 +--
 app/src/main/res/values/dimens.xml            |   8 ++
 app/src/main/res/values/strings.xml           |   6 +
 app/src/main/res/values/styles.xml            |   7 +-
 app/src/main/res/values/styles_dialog.xml     |  39 +++++--
 11 files changed, 255 insertions(+), 66 deletions(-)

diff --git a/NOTICE.md b/NOTICE.md
index 12368d1e6..cea565a0b 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -89,3 +89,22 @@ Material design icons are the official icon set from Google that are designed
 under the material design guidelines. Available under the Creative Common
 Attribution 4.0 International License (CC-BY 4.0).
 
+### Android Flow Layout
+
+
+
+Extended linear layout that wrap its content when there is no place in the current line.
+
+    Copyright 2011, Artem Votincev (apmem.org)
+
+    Licensed under the Apache License, Version 2.0 (the "License"); you may not
+    use this file except in compliance with the License. You may obtain a copy
+    of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+    License for the specific language governing permissions and limitations
+    under the License.
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 9f90fd1ab..2921e8b81 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -34,6 +34,7 @@ android {
 dependencies {
     compile 'com.android.support:support-v4:23.1.1'
     compile 'com.github.paolorotolo:appintro:3.4.0'
+    compile 'org.apmem.tools:layouts:1.10@aar'
     compile project(':libs:drag-sort-listview:library')
     compile files('libs/ActiveAndroid.jar')
 
diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java b/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java
index 412bee5f9..40699aa35 100644
--- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java
+++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java
@@ -19,7 +19,7 @@
 
 package org.isoron.uhabits.ui;
 
-import android.support.test.espresso.matcher.ViewMatchers;
+import android.support.test.espresso.NoMatchingViewException;
 
 import org.isoron.uhabits.R;
 import org.isoron.uhabits.models.Habit;
@@ -37,14 +37,18 @@ import static android.support.test.espresso.action.ViewActions.click;
 import static android.support.test.espresso.action.ViewActions.longClick;
 import static android.support.test.espresso.action.ViewActions.replaceText;
 import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.RootMatchers.isPlatformPopup;
+import static android.support.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE;
 import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
 import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
+import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
 import static android.support.test.espresso.matcher.ViewMatchers.withId;
 import static android.support.test.espresso.matcher.ViewMatchers.withText;
 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.hamcrest.Matchers.startsWith;
 import static org.isoron.uhabits.ui.HabitMatchers.containsHabit;
 import static org.isoron.uhabits.ui.HabitMatchers.withName;
 
@@ -97,6 +101,20 @@ public class MainActivityActions
                 .perform(replaceText(name));
         onView(withId(R.id.input_description))
                 .perform(replaceText(description));
+
+        try
+        {
+            onView(allOf(withId(R.id.sFrequency), withEffectiveVisibility(VISIBLE)))
+                    .perform(click());
+            onData(allOf(instanceOf(String.class), startsWith("Custom")))
+                    .inRoot(isPlatformPopup())
+                    .perform(click());
+        }
+        catch(NoMatchingViewException e)
+        {
+            // ignored
+        }
+
         onView(withId(R.id.input_freq_num))
                 .perform(replaceText(num));
         onView(withId(R.id.input_freq_den))
diff --git a/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java
index 75b93cd8c..54fba7430 100644
--- a/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java
+++ b/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java
@@ -19,9 +19,9 @@
 
 package org.isoron.uhabits.fragments;
 
+import android.annotation.SuppressLint;
 import android.app.DialogFragment;
 import android.content.SharedPreferences;
-import android.graphics.Color;
 import android.os.Bundle;
 import android.preference.PreferenceManager;
 import android.text.format.DateFormat;
@@ -29,8 +29,11 @@ import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
+import android.widget.AdapterView;
 import android.widget.Button;
 import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
 import android.widget.TextView;
 
 import com.android.colorpicker.ColorPickerDialog;
@@ -49,11 +52,10 @@ import org.isoron.uhabits.dialogs.WeekdayPickerDialog;
 import org.isoron.uhabits.models.Habit;
 
 import java.util.Arrays;
-import java.util.Date;
 
 public class EditHabitFragment extends DialogFragment
         implements OnClickListener, WeekdayPickerDialog.OnWeekdaysPickedListener,
-        TimePickerDialog.OnTimeSetListener
+        TimePickerDialog.OnTimeSetListener, Spinner.OnItemSelectedListener
 {
     private Integer mode;
     static final int EDIT_MODE = 0;
@@ -71,6 +73,10 @@ public class EditHabitFragment extends DialogFragment
     private TextView tvReminderTime;
     private TextView tvReminderDays;
 
+    private Spinner sFrequency;
+    private ViewGroup llCustomFrequency;
+    private ViewGroup llReminderDays;
+
     private SharedPreferences prefs;
     private boolean is24HourMode;
 
@@ -105,6 +111,10 @@ public class EditHabitFragment extends DialogFragment
         tvReminderTime = (TextView) view.findViewById(R.id.inputReminderTime);
         tvReminderDays = (TextView) view.findViewById(R.id.inputReminderDays);
 
+        sFrequency = (Spinner) view.findViewById(R.id.sFrequency);
+        llCustomFrequency = (ViewGroup) view.findViewById(R.id.llCustomFrequency);
+        llReminderDays = (ViewGroup) view.findViewById(R.id.llReminderDays);
+
         Button buttonSave = (Button) view.findViewById(R.id.buttonSave);
         Button buttonDiscard = (Button) view.findViewById(R.id.buttonDiscard);
         ImageButton buttonPickColor = (ImageButton) view.findViewById(R.id.buttonPickColor);
@@ -114,6 +124,7 @@ public class EditHabitFragment extends DialogFragment
         tvReminderTime.setOnClickListener(this);
         tvReminderDays.setOnClickListener(this);
         buttonPickColor.setOnClickListener(this);
+        sFrequency.setOnItemSelectedListener(this);
 
         prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
 
@@ -163,6 +174,7 @@ public class EditHabitFragment extends DialogFragment
         tvFreqDen.append(modifiedHabit.freqDen.toString());
 
         changeColor(modifiedHabit.color);
+        updateFrequency();
         updateReminder();
 
         return view;
@@ -183,19 +195,17 @@ public class EditHabitFragment extends DialogFragment
     {
         if (modifiedHabit.hasReminder())
         {
-            tvReminderTime.setTextColor(Color.BLACK);
             tvReminderTime.setText(DateHelper.formatTime(getActivity(), modifiedHabit.reminderHour,
                     modifiedHabit.reminderMin));
-            tvReminderDays.setVisibility(View.VISIBLE);
+            llReminderDays.setVisibility(View.VISIBLE);
 
             boolean weekdays[] = DateHelper.unpackWeekdayList(modifiedHabit.reminderDays);
             tvReminderDays.setText(DateHelper.formatWeekdayList(getActivity(), weekdays));
         }
         else
         {
-            tvReminderTime.setTextColor(Color.GRAY);
             tvReminderTime.setText(R.string.reminder_off);
-            tvReminderDays.setVisibility(View.GONE);
+            llReminderDays.setVisibility(View.GONE);
         }
     }
 
@@ -378,4 +388,79 @@ public class EditHabitFragment extends DialogFragment
             outState.putInt("reminderDays", modifiedHabit.reminderDays);
         }
     }
+
+    @Override
+    public void onItemSelected(AdapterView parent, View view, int position, long id)
+    {
+        if(parent.getId() == R.id.sFrequency)
+        {
+            switch (position)
+            {
+                case 0:
+                    modifiedHabit.freqNum = 1;
+                    modifiedHabit.freqDen = 1;
+                    break;
+
+                case 1:
+                    modifiedHabit.freqNum = 1;
+                    modifiedHabit.freqDen = 7;
+                    break;
+
+                case 2:
+                    modifiedHabit.freqNum = 2;
+                    modifiedHabit.freqDen = 7;
+                    break;
+
+                case 3:
+                    modifiedHabit.freqNum = 5;
+                    modifiedHabit.freqDen = 7;
+                    break;
+
+                case 4:
+                    modifiedHabit.freqNum = 3;
+                    modifiedHabit.freqDen = 7;
+                    break;
+            }
+        }
+
+        updateFrequency();
+    }
+
+    @SuppressLint("SetTextI18n")
+    private void updateFrequency()
+    {
+        int quickSelectPosition = -1;
+
+        if(modifiedHabit.freqNum.equals(modifiedHabit.freqDen))
+            quickSelectPosition = 0;
+
+        else if(modifiedHabit.freqNum == 1 && modifiedHabit.freqDen == 7)
+            quickSelectPosition = 1;
+
+        else if(modifiedHabit.freqNum == 2 && modifiedHabit.freqDen == 7)
+            quickSelectPosition = 2;
+
+        else if(modifiedHabit.freqNum == 5 && modifiedHabit.freqDen == 7)
+            quickSelectPosition = 3;
+
+        if(quickSelectPosition >= 0)
+        {
+            sFrequency.setVisibility(View.VISIBLE);
+            sFrequency.setSelection(quickSelectPosition);
+            llCustomFrequency.setVisibility(View.GONE);
+            tvFreqNum.setText(modifiedHabit.freqNum.toString());
+            tvFreqDen.setText(modifiedHabit.freqDen.toString());
+        }
+        else
+        {
+            sFrequency.setVisibility(View.GONE);
+            llCustomFrequency.setVisibility(View.VISIBLE);
+        }
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView parent)
+    {
+
+    }
 }
diff --git a/app/src/main/res/layout/edit_habit.xml b/app/src/main/res/layout/edit_habit.xml
index 4d4dd8218..493dc1c67 100644
--- a/app/src/main/res/layout/edit_habit.xml
+++ b/app/src/main/res/layout/edit_habit.xml
@@ -17,12 +17,13 @@
   ~ with this program. If not, see .
   -->
 
-
+    tools:ignore="MergeRootFrame">
 
     
+                style="@style/dialogFormInput"
+                android:hint="@string/name">
 
-                
+                
             
 
             
+                android:src="@drawable/ic_action_color_light"/>
         
 
         
+            style="@style/dialogFormInputMultiline"
+            android:hint="@string/description_hint"/>
 
         
+            style="@style/dialogFormRow">
 
             
-
-            
+                android:text="@string/repeat"/>
 
-            
-
-            
+                android:entries="@array/frequencyQuickSelect"
+                android:visibility="gone"/>
 
-            
+                android:visibility="visible"
+                android:gravity="fill">
+
+                
+
+                
+
+                
+
+                
+
+            
         
 
         
 
             
+                android:text="@string/reminder"/>
 
             
+                style="@style/dialogFormSpinner"/>
+        
+
+        
+
+            
 
             
+                style="@style/dialogFormSpinner"/>
 
         
     
@@ -110,20 +136,22 @@
         android:layout_height="wrap_content"
         android:gravity="end"
         android:paddingEnd="16dp"
-        android:paddingRight="16dp">
+        android:paddingLeft="0dp"
+        android:paddingRight="16dp"
+        android:paddingStart="0dp">