From 78d4f86cab8d338a44f7778b7a51c6d9ca550802 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 10 Jun 2016 13:30:33 -0400 Subject: [PATCH] Separate ActiveAndroid from models --- app/build.gradle | 13 +- .../org/isoron/uhabits/BaseAndroidTest.java | 26 +- .../org/isoron/uhabits/ui/HabitMatchers.java | 4 +- .../uhabits/ui/MainActivityActions.java | 8 +- .../java/org/isoron/uhabits/ui/MainTest.java | 10 +- .../isoron/uhabits/unit/HabitFixtures.java | 157 ++--- .../commands/ArchiveHabitsCommandTest.java | 2 +- .../commands/ChangeHabitColorCommandTest.java | 9 +- .../unit/commands/CreateHabitCommandTest.java | 19 +- .../commands/DeleteHabitsCommandTest.java | 22 +- .../unit/commands/EditHabitCommandTest.java | 72 +-- .../commands/ToggleRepetitionCommandTest.java | 13 +- .../commands/UnarchiveHabitsCommandTest.java | 8 +- .../unit/io/HabitsCSVExporterTest.java | 69 +-- .../isoron/uhabits/unit/io/ImportTest.java | 49 +- .../unit/models/CheckmarkListTest.java | 175 ------ .../isoron/uhabits/unit/models/HabitTest.java | 378 ------------ .../unit/models/RepetitionListTest.java | 191 ------ .../uhabits/unit/models/ScoreListTest.java | 160 ++--- .../isoron/uhabits/unit/models/ScoreTest.java | 51 +- .../uhabits/unit/tasks/ExportCSVTaskTest.java | 19 +- .../list/view/CheckmarkButtonViewTest.java | 38 +- .../list/view/CheckmarkPanelViewTest.java | 5 +- .../unit/views/CheckmarkWidgetViewTest.java | 32 +- .../unit/views/HabitFrequencyViewTest.java | 23 +- .../unit/views/HabitHistoryViewTest.java | 82 +-- .../unit/views/HabitScoreViewTest.java | 35 +- .../unit/views/HabitStreakViewTest.java | 22 +- .../uhabits/unit/views/NumberViewTest.java | 77 --- .../uhabits/unit/views/RingViewTest.java | 6 +- .../isoron/uhabits/unit/views/ViewTest.java | 2 +- .../org/isoron/uhabits/AndroidComponent.java | 3 + .../org/isoron/uhabits/AndroidModule.java | 35 +- .../org/isoron/uhabits/BaseComponent.java | 54 +- .../uhabits/HabitBroadcastReceiver.java | 265 +++++---- .../org/isoron/uhabits/HabitsApplication.java | 68 ++- .../org/isoron/uhabits/HabitsBackupAgent.java | 3 + .../java/org/isoron/uhabits/MainActivity.java | 3 + .../commands/ArchiveHabitsCommand.java | 18 +- .../commands/ChangeHabitColorCommand.java | 47 +- .../org/isoron/uhabits/commands/Command.java | 13 +- .../uhabits/commands/CommandRunner.java | 10 + .../uhabits/commands/CreateHabitCommand.java | 31 +- .../uhabits/commands/DeleteHabitsCommand.java | 17 +- .../uhabits/commands/EditHabitCommand.java | 63 +- .../commands/ToggleRepetitionCommand.java | 5 +- .../commands/UnarchiveHabitsCommand.java | 18 +- .../isoron/uhabits/commands/package-info.java | 24 + .../isoron/uhabits/io/AbstractImporter.java | 17 + .../isoron/uhabits/io/GenericImporter.java | 13 +- .../uhabits/io/HabitBullCSVImporter.java | 19 +- .../isoron/uhabits/io/HabitsCSVExporter.java | 22 +- .../org/isoron/uhabits/io/LoopDBImporter.java | 12 +- .../isoron/uhabits/io/RewireDBImporter.java | 38 +- .../isoron/uhabits/io/TickmateDBImporter.java | 82 +-- .../org/isoron/uhabits/io/package-info.java | 23 + .../org/isoron/uhabits/models/Checkmark.java | 77 ++- .../isoron/uhabits/models/CheckmarkList.java | 293 ++++------ .../java/org/isoron/uhabits/models/Habit.java | 547 +++++++----------- .../org/isoron/uhabits/models/HabitList.java | 235 ++++++++ .../isoron/uhabits/models/ModelFactory.java | 37 ++ .../uhabits/models/ModelObservable.java | 41 +- .../org/isoron/uhabits/models/Repetition.java | 55 +- .../isoron/uhabits/models/RepetitionList.java | 230 ++++---- .../java/org/isoron/uhabits/models/Score.java | 117 ++-- .../org/isoron/uhabits/models/ScoreList.java | 380 +++++------- .../org/isoron/uhabits/models/Streak.java | 65 ++- .../org/isoron/uhabits/models/StreakList.java | 179 +++--- .../models/memory/MemoryCheckmarkList.java | 102 ++++ .../models/memory/MemoryHabitList.java | 111 ++++ .../models/memory/MemoryModelFactory.java | 61 ++ .../models/memory/MemoryRepetitionList.java | 106 ++++ .../models/memory/MemoryStreakList.java | 86 +++ .../uhabits/models/memory/package-info.java | 23 + .../isoron/uhabits/models/package-info.java | 24 + .../models/sqlite/CheckmarkRecord.java | 62 ++ .../uhabits/models/sqlite/HabitRecord.java | 177 ++++++ .../models/sqlite/RepetitionRecord.java | 58 ++ .../models/sqlite/SQLModelFactory.java | 64 ++ .../models/sqlite/SQLiteCheckmarkList.java | 139 +++++ .../models/sqlite/SQLiteHabitList.java | 232 ++++++++ .../models/sqlite/SQLiteRepetitionList.java | 143 +++++ .../models/sqlite/SQLiteScoreList.java | 173 ++++++ .../models/sqlite/SQLiteStreakList.java | 115 ++++ .../uhabits/models/sqlite/ScoreRecord.java | 65 +++ .../uhabits/models/sqlite/StreakRecord.java | 66 +++ .../uhabits/models/sqlite/package-info.java | 23 + .../java/org/isoron/uhabits/package-info.java | 23 + .../isoron/uhabits/tasks/package-info.java | 24 + .../org/isoron/uhabits/ui/BaseSystem.java | 10 +- .../isoron/uhabits/ui/about/package-info.java | 2 +- .../ui/habits/edit/BaseDialogFragment.java | 43 +- .../ui/habits/edit/BaseDialogHelper.java | 40 +- .../edit/CreateHabitDialogFragment.java | 8 +- .../habits/edit/EditHabitDialogFragment.java | 15 +- .../ui/habits/edit/HistoryEditorDialog.java | 12 +- .../uhabits/ui/habits/edit/package-info.java | 23 + .../ui/habits/list/ListHabitsActivity.java | 12 +- .../ui/habits/list/ListHabitsController.java | 18 +- .../ui/habits/list/ListHabitsScreen.java | 2 +- .../habits/list/controllers/package-info.java | 2 +- .../list/model/HabitCardListAdapter.java | 19 +- .../habits/list/model/HabitCardListCache.java | 18 +- .../ui/habits/list/model/package-info.java | 2 +- .../uhabits/ui/habits/list/package-info.java | 2 +- .../ui/habits/list/views/HabitCardView.java | 8 +- .../ui/habits/show/ShowHabitActivity.java | 12 +- .../ui/habits/show/ShowHabitFragment.java | 20 +- .../ui/habits/show/ShowHabitHelper.java | 21 +- .../uhabits/ui/habits/show/package-info.java | 24 + .../habits/show}/views/HabitDataView.java | 2 +- .../show}/views/HabitFrequencyView.java | 17 +- .../habits/show}/views/HabitHistoryView.java | 15 +- .../habits/show}/views/HabitScoreView.java | 455 ++++++++------- .../habits/show}/views/HabitStreakView.java | 257 ++++---- .../habits/show}/views/HabitWidgetView.java | 4 +- .../{ => ui/habits/show}/views/RingView.java | 4 +- .../show}/views/ScrollableDataView.java | 91 +-- .../ui/habits/show/views/package-info.java | 24 + .../isoron/uhabits/ui/intro/package-info.java | 2 +- .../org/isoron/uhabits/ui/package-info.java | 23 + .../uhabits/ui/settings/package-info.java | 2 +- .../org/isoron/uhabits/utils/ColorUtils.java | 118 ++-- .../isoron/uhabits/utils/DatabaseUtils.java | 78 +-- .../isoron/uhabits/utils/ReminderUtils.java | 146 +++-- .../isoron/uhabits/utils/package-info.java | 23 + .../org/isoron/uhabits/views/NumberView.java | 166 ------ .../uhabits/views/RepetitionCountView.java | 85 --- .../uhabits/widgets/BaseWidgetProvider.java | 14 +- .../widgets/CheckmarkWidgetProvider.java | 4 +- .../widgets/FrequencyWidgetProvider.java | 6 +- .../uhabits/widgets/HabitPickerDialog.java | 25 +- .../widgets/HistoryWidgetProvider.java | 6 +- .../uhabits/widgets/ScoreWidgetProvider.java | 8 +- .../uhabits/widgets/StreakWidgetProvider.java | 6 +- .../isoron/uhabits/widgets/package-info.java | 23 + .../views/CheckmarkWidgetView.java | 131 +++-- .../{ => widgets}/views/GraphWidgetView.java | 6 +- .../uhabits/widgets/views/package-info.java | 23 + app/src/main/res/layout/list_habits_card.xml | 2 +- app/src/main/res/layout/show_habit.xml | 10 +- app/src/main/res/layout/widget_checkmark.xml | 2 +- .../java/org/isoron/uhabits/BaseUnitTest.java | 31 + .../java/org/isoron/uhabits/TestModule.java | 25 + .../uhabits/models/CheckmarkListTest.java | 204 +++++++ .../isoron/uhabits/models/HabitFixtures.java | 92 +++ .../isoron/uhabits/models/HabitListTest.java | 200 +++++++ .../org/isoron/uhabits/models/HabitTest.java | 114 ++++ .../uhabits/models/RepetitionListTest.java | 179 ++++++ .../isoron/uhabits/models/StreakListTest.java | 149 +++++ build.gradle | 14 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 152 files changed, 6175 insertions(+), 3741 deletions(-) delete mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java delete mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java delete mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java delete mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/views/NumberViewTest.java create mode 100644 app/src/main/java/org/isoron/uhabits/commands/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/io/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/HabitList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/ModelFactory.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/memory/MemoryCheckmarkList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/memory/MemoryStreakList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/memory/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/CheckmarkRecord.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/HabitRecord.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/RepetitionRecord.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/ScoreRecord.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/StreakRecord.java create mode 100644 app/src/main/java/org/isoron/uhabits/models/sqlite/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/tasks/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/ui/habits/edit/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/ui/habits/show/package-info.java rename app/src/main/java/org/isoron/uhabits/{ => ui/habits/show}/views/HabitDataView.java (95%) rename app/src/main/java/org/isoron/uhabits/{ => ui/habits/show}/views/HabitFrequencyView.java (96%) rename app/src/main/java/org/isoron/uhabits/{ => ui/habits/show}/views/HabitHistoryView.java (97%) rename app/src/main/java/org/isoron/uhabits/{ => ui/habits/show}/views/HabitScoreView.java (80%) rename app/src/main/java/org/isoron/uhabits/{ => ui/habits/show}/views/HabitStreakView.java (73%) rename app/src/main/java/org/isoron/uhabits/{ => ui/habits/show}/views/HabitWidgetView.java (98%) rename app/src/main/java/org/isoron/uhabits/{ => ui/habits/show}/views/RingView.java (98%) rename app/src/main/java/org/isoron/uhabits/{ => ui/habits/show}/views/ScrollableDataView.java (83%) create mode 100644 app/src/main/java/org/isoron/uhabits/ui/habits/show/views/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/ui/package-info.java create mode 100644 app/src/main/java/org/isoron/uhabits/utils/package-info.java delete mode 100644 app/src/main/java/org/isoron/uhabits/views/NumberView.java delete mode 100644 app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java create mode 100644 app/src/main/java/org/isoron/uhabits/widgets/package-info.java rename app/src/main/java/org/isoron/uhabits/{ => widgets}/views/CheckmarkWidgetView.java (74%) rename app/src/main/java/org/isoron/uhabits/{ => widgets}/views/GraphWidgetView.java (91%) create mode 100644 app/src/main/java/org/isoron/uhabits/widgets/views/package-info.java create mode 100644 app/src/test/java/org/isoron/uhabits/models/CheckmarkListTest.java create mode 100644 app/src/test/java/org/isoron/uhabits/models/HabitFixtures.java create mode 100644 app/src/test/java/org/isoron/uhabits/models/HabitListTest.java create mode 100644 app/src/test/java/org/isoron/uhabits/models/HabitTest.java create mode 100644 app/src/test/java/org/isoron/uhabits/models/RepetitionListTest.java create mode 100644 app/src/test/java/org/isoron/uhabits/models/StreakListTest.java diff --git a/app/build.gradle b/app/build.gradle index 08067b99c..318bc5d6c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' -apply plugin: 'com.getkeepsafe.dexcount' apply plugin: 'me.tatarka.retrolambda' android { @@ -31,6 +30,7 @@ android { lintOptions { checkReleaseBuilds false } + compileOptions { targetCompatibility 1.8 sourceCompatibility 1.8 @@ -77,14 +77,3 @@ dependencies { exclude group: 'com.android.support' } } - - -task grantAnimationPermission(type: Exec, dependsOn: 'installDebug') { - commandLine "adb shell pm grant org.isoron.uhabits android.permission.SET_ANIMATION_SCALE".split(' ') -} - -tasks.whenTaskAdded { task -> - if (task.name.startsWith('connected')) { - task.dependsOn grantAnimationPermission - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java b/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java index 8a5d6255a..8be833bc0 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java @@ -24,9 +24,11 @@ import android.os.Build; import android.os.Looper; import android.support.test.InstrumentationRegistry; +import org.isoron.uhabits.models.HabitList; +import org.isoron.uhabits.tasks.BaseTask; +import org.isoron.uhabits.unit.HabitFixtures; import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.InterfaceUtils; -import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.utils.Preferences; import org.junit.Before; @@ -36,20 +38,29 @@ import javax.inject.Inject; public class BaseAndroidTest { - protected Context testContext; - protected Context targetContext; + // 8:00am, January 25th, 2015 (UTC) + public static final long FIXED_LOCAL_TIME = 1422172800000L; + private static boolean isLooperPrepared; - public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC) + protected Context testContext; + + protected Context targetContext; @Inject protected Preferences prefs; + + @Inject + protected HabitList habitList; + protected AndroidTestComponent androidTestComponent; + protected HabitFixtures habitFixtures; + @Before public void setUp() { - if(!isLooperPrepared) + if (!isLooperPrepared) { Looper.prepare(); isLooperPrepared = true; @@ -64,9 +75,12 @@ public class BaseAndroidTest androidTestComponent = DaggerAndroidTestComponent.builder().build(); HabitsApplication.setComponent(androidTestComponent); androidTestComponent.inject(this); + + habitFixtures = new HabitFixtures(habitList); } - protected void waitForAsyncTasks() throws InterruptedException, TimeoutException + protected void waitForAsyncTasks() + throws InterruptedException, TimeoutException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java b/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java index ee8b810b8..3090d3dd0 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java @@ -39,7 +39,7 @@ public class HabitMatchers @Override public boolean matchesSafely(Habit habit) { - return habit.name.equals(name); + return habit.getName().equals(name); } @Override @@ -51,7 +51,7 @@ public class HabitMatchers @Override public void describeMismatchSafely(Habit habit, Description description) { - description.appendText("was ").appendText(habit.name); + description.appendText("was ").appendText(habit.getName()); } }; } 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 7da8081ef..5b63b0b5b 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainActivityActions.java @@ -23,7 +23,7 @@ import android.support.test.espresso.NoMatchingViewException; import android.support.test.espresso.contrib.RecyclerViewActions; import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.sqlite.HabitRecord; import java.util.Collections; import java.util.LinkedList; @@ -93,7 +93,7 @@ public class MainActivityActions onView(withId(R.id.buttonSave)) .perform(click()); - onData(allOf(is(instanceOf(Habit.class)), withName(name))) + onData(allOf(is(instanceOf(HabitRecord.class)), withName(name))) .onChildView(withId(R.id.label)); return name; @@ -135,7 +135,7 @@ public class MainActivityActions boolean first = true; for(String name : names) { - onData(allOf(is(instanceOf(Habit.class)), withName(name))) + onData(allOf(is(instanceOf(HabitRecord.class)), withName(name))) .onChildView(withId(R.id.label)) .perform(first ? longClick() : click()); @@ -160,7 +160,7 @@ public class MainActivityActions public static void assertHabitsExist(List names) { for(String name : names) - onData(allOf(is(instanceOf(Habit.class)), withName(name))) + onData(allOf(is(instanceOf(HabitRecord.class)), withName(name))) .check(matches(isDisplayed())); } 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 82732735d..9fba71854 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java @@ -30,8 +30,8 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.LargeTest; import org.isoron.uhabits.R; +import org.isoron.uhabits.models.sqlite.HabitRecord; import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.MainActivity; import org.junit.After; import org.junit.Before; @@ -190,13 +190,13 @@ public class MainTest { String name = addHabit(true); - onData(allOf(is(instanceOf(Habit.class)), withName(name))) + onData(allOf(is(instanceOf(HabitRecord.class)), withName(name))) .onChildView(withId(R.id.checkmarkPanel)) .perform(toggleAllCheckmarks()); Thread.sleep(1200); - onData(allOf(is(instanceOf(Habit.class)), withName(name))) + onData(allOf(is(instanceOf(HabitRecord.class)), withName(name))) .onChildView(withId(R.id.label)) .perform(click()); @@ -217,7 +217,7 @@ public class MainTest { String name = addHabit(); - onData(allOf(is(instanceOf(Habit.class)), withName(name))) + onData(allOf(is(instanceOf(HabitRecord.class)), withName(name))) .onChildView(withId(R.id.label)) .perform(longClick()); @@ -247,7 +247,7 @@ public class MainTest { String name = addHabit(); - onData(allOf(is(instanceOf(Habit.class)), withName(name))) + onData(allOf(is(instanceOf(HabitRecord.class)), withName(name))) .onChildView(withId(R.id.label)) .perform(click()); diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/HabitFixtures.java b/app/src/androidTest/java/org/isoron/uhabits/unit/HabitFixtures.java index 16c16cccf..26f792ad5 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/HabitFixtures.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/HabitFixtures.java @@ -19,151 +19,76 @@ package org.isoron.uhabits.unit; -import android.content.Context; -import android.support.annotation.Nullable; -import android.util.Log; - -import org.isoron.uhabits.utils.DatabaseUtils; -import org.isoron.uhabits.utils.FileUtils; -import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.tasks.ExportDBTask; -import org.isoron.uhabits.tasks.ImportDataTask; - -import java.io.File; -import java.io.InputStream; -import java.util.Random; - -import static org.junit.Assert.fail; +import org.isoron.uhabits.models.HabitList; +import org.isoron.uhabits.utils.DateUtils; public class HabitFixtures { - public static boolean NON_DAILY_HABIT_CHECKS[] = { true, false, false, true, true, true, false, - false, true, true }; + public boolean NON_DAILY_HABIT_CHECKS[] = { + true, false, false, true, true, true, false, false, true, true + }; - public static Habit createShortHabit() - { - Habit habit = new Habit(); - habit.name = "Wake up early"; - habit.description = "Did you wake up before 6am?"; - habit.freqNum = 2; - habit.freqDen = 3; - habit.save(); + private final HabitList habitList; - long timestamp = DateUtils.getStartOfToday(); - for(boolean c : NON_DAILY_HABIT_CHECKS) - { - if(c) habit.repetitions.toggle(timestamp); - timestamp -= DateUtils.millisecondsInOneDay; - } - - return habit; + public HabitFixtures(HabitList habitList) + { + this.habitList = habitList; } - public static Habit createEmptyHabit() + public Habit createEmptyHabit() { Habit habit = new Habit(); - habit.name = "Meditate"; - habit.description = "Did you meditate this morning?"; - habit.color = 3; - habit.freqNum = 1; - habit.freqDen = 1; - habit.save(); + habit.setName("Meditate"); + habit.setDescription("Did you meditate this morning?"); + habit.setColor(3); + habit.setFreqNum(1); + habit.setFreqDen(1); + habitList.add(habit); return habit; } - public static Habit createLongHabit() + public Habit createLongHabit() { Habit habit = createEmptyHabit(); - habit.freqNum = 3; - habit.freqDen = 7; - habit.color = 4; - habit.save(); + habit.setFreqNum(3); + habit.setFreqDen(7); + habit.setColor(4); long day = DateUtils.millisecondsInOneDay; long today = DateUtils.getStartOfToday(); - int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, 28, 50, 51, 52, - 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, 81, 83, 89, 90, 91, 95, - 102, 103, 108, 109, 120}; + int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, + 28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, + 81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120}; - for(int mark : marks) - habit.repetitions.toggle(today - mark * day); + for (int mark : marks) + habit.getRepetitions().toggleTimestamp(today - mark * day); return habit; } - public static void generateHugeDataSet() throws Throwable + public Habit createShortHabit() { - final int nHabits = 30; - final int nYears = 5; - - DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command() - { - @Override - public void execute() - { - Random rand = new Random(); - - for(int i = 0; i < nHabits; i++) - { - Log.i("HabitFixture", String.format("Creating habit %d / %d", i, nHabits)); - - Habit habit = new Habit(); - habit.name = String.format("Habit %d", i); - habit.save(); - - long today = DateUtils.getStartOfToday(); - long day = DateUtils.millisecondsInOneDay; - - - for(int j = 0; j < 365 * nYears; j++) - { - if(rand.nextBoolean()) - habit.repetitions.toggle(today - j * day); - } - - habit.scores.getTodayValue(); - habit.streaks.getAll(1); - } - } - }); + Habit habit = new Habit(); + habit.setName("Wake up early"); + habit.setDescription("Did you wake up before 6am?"); + habit.setFreqNum(2); + habit.setFreqDen(3); + habitList.add(habit); - ExportDBTask task = new ExportDBTask(null); - task.setListener(new ExportDBTask.Listener() + long timestamp = DateUtils.getStartOfToday(); + for (boolean c : NON_DAILY_HABIT_CHECKS) { - @Override - public void onExportDBFinished(@Nullable String filename) - { - if(filename != null) - Log.i("HabitFixture", String.format("Huge data set exported to %s", filename)); - else - Log.i("HabitFixture", "Failed to save database"); - } - }); - task.execute(); - - BaseTask.waitForTasks(30000); - } - - public static void loadHugeDataSet(Context testContext) throws Throwable - { - File baseDir = FileUtils.getFilesDir("Backups"); - if(baseDir == null) fail("baseDir should not be null"); - - File dst = new File(String.format("%s/%s", baseDir.getPath(), "loopHuge.db")); - InputStream in = testContext.getAssets().open("fixtures/loopHuge.db"); - FileUtils.copy(in, dst); - - ImportDataTask task = new ImportDataTask(dst, null); - task.execute(); + if (c) habit.getRepetitions().toggleTimestamp(timestamp); + timestamp -= DateUtils.millisecondsInOneDay; + } - BaseTask.waitForTasks(30000); + return habit; } - public static void purgeHabits() + public void purgeHabits(HabitList habitList) { - for(Habit h : Habit.getAll(true)) - h.cascadeDelete(); + for (Habit h : habitList.getAll(true)) + habitList.remove(h); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ArchiveHabitsCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ArchiveHabitsCommandTest.java index d00881506..57a43817a 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ArchiveHabitsCommandTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ArchiveHabitsCommandTest.java @@ -48,7 +48,7 @@ public class ArchiveHabitsCommandTest extends BaseAndroidTest { super.setUp(); - habit = HabitFixtures.createShortHabit(); + habit = habitFixtures.createShortHabit(); command = new ArchiveHabitsCommand(Collections.singletonList(habit)); } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ChangeHabitColorCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ChangeHabitColorCommandTest.java index dd9f334f4..bfff4f81b 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ChangeHabitColorCommandTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ChangeHabitColorCommandTest.java @@ -51,9 +51,8 @@ public class ChangeHabitColorCommandTest extends BaseAndroidTest for(int i = 0; i < 3; i ++) { - Habit habit = HabitFixtures.createShortHabit(); - habit.color = i+1; - habit.save(); + Habit habit = habitFixtures.createShortHabit(); + habit.setColor(i + 1); habits.add(habit); } @@ -79,12 +78,12 @@ public class ChangeHabitColorCommandTest extends BaseAndroidTest { int k = 0; for(Habit h : habits) - assertThat(h.color, equalTo(++k)); + assertThat(h.getColor(), equalTo(++k)); } private void checkNewColors() { for(Habit h : habits) - assertThat(h.color, equalTo(0)); + assertThat(h.getColor(), equalTo(0)); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/CreateHabitCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/CreateHabitCommandTest.java index 60ee83b9a..c5a9f39f2 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/CreateHabitCommandTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/CreateHabitCommandTest.java @@ -23,9 +23,9 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseAndroidTest; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.commands.CreateHabitCommand; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,6 +42,7 @@ public class CreateHabitCommandTest extends BaseAndroidTest { private CreateHabitCommand command; + private Habit model; @Before @@ -50,36 +51,36 @@ public class CreateHabitCommandTest extends BaseAndroidTest super.setUp(); model = new Habit(); - model.name = "New habit"; + model.setName("New habit"); command = new CreateHabitCommand(model); - HabitFixtures.purgeHabits(); + habitFixtures.purgeHabits(habitList); } @Test public void testExecuteUndoRedo() { - assertTrue(Habit.getAll(true).isEmpty()); + assertTrue(habitList.getAll(true).isEmpty()); command.execute(); - List allHabits = Habit.getAll(true); + List allHabits = habitList.getAll(true); assertThat(allHabits.size(), equalTo(1)); Habit habit = allHabits.get(0); Long id = habit.getId(); - assertThat(habit.name, equalTo(model.name)); + assertThat(habit.getName(), equalTo(model.getName())); command.undo(); - assertTrue(Habit.getAll(true).isEmpty()); + assertTrue(habitList.getAll(true).isEmpty()); command.execute(); - allHabits = Habit.getAll(true); + allHabits = habitList.getAll(true); assertThat(allHabits.size(), equalTo(1)); habit = allHabits.get(0); Long newId = habit.getId(); assertThat(id, equalTo(newId)); - assertThat(habit.name, equalTo(model.name)); + assertThat(habit.getName(), equalTo(model.getName())); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/DeleteHabitsCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/DeleteHabitsCommandTest.java index 045529c04..983448f41 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/DeleteHabitsCommandTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/DeleteHabitsCommandTest.java @@ -25,7 +25,6 @@ import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.commands.DeleteHabitsCommand; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -42,30 +41,31 @@ import static org.hamcrest.Matchers.equalTo; public class DeleteHabitsCommandTest extends BaseAndroidTest { private DeleteHabitsCommand command; + private LinkedList habits; @Rule public ExpectedException thrown = ExpectedException.none(); + @Override @Before public void setUp() { super.setUp(); - HabitFixtures.purgeHabits(); + habitFixtures.purgeHabits(habitList); habits = new LinkedList<>(); - // Habits that shuold be deleted - for(int i = 0; i < 3; i ++) + // Habits that should be deleted + for (int i = 0; i < 3; i++) { - Habit habit = HabitFixtures.createShortHabit(); + Habit habit = habitFixtures.createShortHabit(); habits.add(habit); } // Extra habit that should not be deleted - Habit extraHabit = HabitFixtures.createShortHabit(); - extraHabit.name = "extra"; - extraHabit.save(); + Habit extraHabit = habitFixtures.createShortHabit(); + extraHabit.setName("extra"); command = new DeleteHabitsCommand(habits); } @@ -73,11 +73,11 @@ public class DeleteHabitsCommandTest extends BaseAndroidTest @Test public void testExecuteUndoRedo() { - assertThat(Habit.getAll(true).size(), equalTo(4)); + assertThat(habitList.getAll(true).size(), equalTo(4)); command.execute(); - assertThat(Habit.getAll(true).size(), equalTo(1)); - assertThat(Habit.getAll(true).get(0).name, equalTo("extra")); + assertThat(habitList.getAll(true).size(), equalTo(1)); + assertThat(habitList.getAll(true).get(0).getName(), equalTo("extra")); thrown.expect(UnsupportedOperationException.class); command.undo(); diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/EditHabitCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/EditHabitCommandTest.java index a3c04fd82..46f077481 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/EditHabitCommandTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/EditHabitCommandTest.java @@ -25,12 +25,10 @@ import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.commands.EditHabitCommand; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import static junit.framework.Assert.assertTrue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -41,25 +39,25 @@ public class EditHabitCommandTest extends BaseAndroidTest { private EditHabitCommand command; + private Habit habit; + private Habit modified; - private Long id; + @Override @Before public void setUp() { super.setUp(); - habit = HabitFixtures.createShortHabit(); - habit.name = "original"; - habit.freqDen = 1; - habit.freqNum = 1; - habit.save(); - - id = habit.getId(); + habit = habitFixtures.createShortHabit(); + habit.setName("original"); + habit.setFreqDen(1); + habit.setFreqNum(1); - modified = new Habit(habit); - modified.name = "modified"; + modified = new Habit(); + modified.copyFrom(habit); + modified.setName("modified"); } @Test @@ -67,54 +65,44 @@ public class EditHabitCommandTest extends BaseAndroidTest { command = new EditHabitCommand(habit, modified); - int originalScore = habit.scores.getTodayValue(); - assertThat(habit.name, equalTo("original")); + int originalScore = habit.getScores().getTodayValue(); + assertThat(habit.getName(), equalTo("original")); command.execute(); - refreshHabit(); - assertThat(habit.name, equalTo("modified")); - assertThat(habit.scores.getTodayValue(), equalTo(originalScore)); + assertThat(habit.getName(), equalTo("modified")); + assertThat(habit.getScores().getTodayValue(), equalTo(originalScore)); command.undo(); - refreshHabit(); - assertThat(habit.name, equalTo("original")); - assertThat(habit.scores.getTodayValue(), equalTo(originalScore)); + assertThat(habit.getName(), equalTo("original")); + assertThat(habit.getScores().getTodayValue(), equalTo(originalScore)); command.execute(); - refreshHabit(); - assertThat(habit.name, equalTo("modified")); - assertThat(habit.scores.getTodayValue(), equalTo(originalScore)); + assertThat(habit.getName(), equalTo("modified")); + assertThat(habit.getScores().getTodayValue(), equalTo(originalScore)); } @Test public void testExecuteUndoRedo_withModifiedInterval() { - modified.freqNum = 1; - modified.freqDen = 7; + modified.setFreqNum(1); + modified.setFreqDen(7); command = new EditHabitCommand(habit, modified); - int originalScore = habit.scores.getTodayValue(); - assertThat(habit.name, equalTo("original")); + int originalScore = habit.getScores().getTodayValue(); + assertThat(habit.getName(), equalTo("original")); command.execute(); - refreshHabit(); - assertThat(habit.name, equalTo("modified")); - assertThat(habit.scores.getTodayValue(), greaterThan(originalScore)); + assertThat(habit.getName(), equalTo("modified")); + assertThat(habit.getScores().getTodayValue(), + greaterThan(originalScore)); command.undo(); - refreshHabit(); - assertThat(habit.name, equalTo("original")); - assertThat(habit.scores.getTodayValue(), equalTo(originalScore)); + assertThat(habit.getName(), equalTo("original")); + assertThat(habit.getScores().getTodayValue(), equalTo(originalScore)); command.execute(); - refreshHabit(); - assertThat(habit.name, equalTo("modified")); - assertThat(habit.scores.getTodayValue(), greaterThan(originalScore)); - } - - private void refreshHabit() - { - habit = Habit.get(id); - assertTrue(habit != null); + assertThat(habit.getName(), equalTo("modified")); + assertThat(habit.getScores().getTodayValue(), + greaterThan(originalScore)); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ToggleRepetitionCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ToggleRepetitionCommandTest.java index fb22aea6b..e14c35ad9 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ToggleRepetitionCommandTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/ToggleRepetitionCommandTest.java @@ -24,9 +24,8 @@ import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.commands.ToggleRepetitionCommand; -import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; +import org.isoron.uhabits.utils.DateUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -48,7 +47,7 @@ public class ToggleRepetitionCommandTest extends BaseAndroidTest { super.setUp(); - habit = HabitFixtures.createShortHabit(); + habit = habitFixtures.createShortHabit(); today = DateUtils.getStartOfToday(); command = new ToggleRepetitionCommand(habit, today); @@ -57,15 +56,15 @@ public class ToggleRepetitionCommandTest extends BaseAndroidTest @Test public void testExecuteUndoRedo() { - assertTrue(habit.repetitions.contains(today)); + assertTrue(habit.getRepetitions().containsTimestamp(today)); command.execute(); - assertFalse(habit.repetitions.contains(today)); + assertFalse(habit.getRepetitions().containsTimestamp(today)); command.undo(); - assertTrue(habit.repetitions.contains(today)); + assertTrue(habit.getRepetitions().containsTimestamp(today)); command.execute(); - assertFalse(habit.repetitions.contains(today)); + assertFalse(habit.getRepetitions().containsTimestamp(today)); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/UnarchiveHabitsCommandTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/UnarchiveHabitsCommandTest.java index 336ee5752..07cc11104 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/commands/UnarchiveHabitsCommandTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/commands/UnarchiveHabitsCommandTest.java @@ -25,7 +25,6 @@ import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.commands.UnarchiveHabitsCommand; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,17 +38,18 @@ import static junit.framework.Assert.assertTrue; @SmallTest public class UnarchiveHabitsCommandTest extends BaseAndroidTest { - private UnarchiveHabitsCommand command; private Habit habit; + @Override @Before public void setUp() { super.setUp(); - habit = HabitFixtures.createShortHabit(); - Habit.archive(Collections.singletonList(habit)); + habit = habitFixtures.createShortHabit(); + habit.setArchived(1); + habitList.update(habit); command = new UnarchiveHabitsCommand(Collections.singletonList(habit)); } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java index b68ec0a71..a965a7eb9 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java @@ -25,10 +25,9 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseAndroidTest; -import org.isoron.uhabits.utils.FileUtils; import org.isoron.uhabits.io.HabitsCSVExporter; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; +import org.isoron.uhabits.utils.FileUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -54,41 +53,18 @@ public class HabitsCSVExporterTest extends BaseAndroidTest { super.setUp(); - HabitFixtures.purgeHabits(); - HabitFixtures.createShortHabit(); - HabitFixtures.createEmptyHabit(); + habitFixtures.purgeHabits(habitList); + habitFixtures.createShortHabit(); + habitFixtures.createEmptyHabit(); Context targetContext = InstrumentationRegistry.getTargetContext(); baseDir = targetContext.getCacheDir(); } - private void unzip(File file) throws IOException - { - ZipFile zip = new ZipFile(file); - Enumeration e = zip.entries(); - - while(e.hasMoreElements()) - { - ZipEntry entry = e.nextElement(); - InputStream stream = zip.getInputStream(entry); - - String outputFilename = String.format("%s/%s", baseDir.getAbsolutePath(), - entry.getName()); - File outputFile = new File(outputFilename); - - File parent = outputFile.getParentFile(); - if(parent != null) parent.mkdirs(); - - FileUtils.copy(stream, outputFile); - } - - zip.close(); - } - @Test public void testExportCSV() throws IOException { - List habits = Habit.getAll(true); + List habits = habitList.getAll(true); HabitsCSVExporter exporter = new HabitsCSVExporter(habits, baseDir); String filename = exporter.writeArchive(); @@ -105,14 +81,41 @@ public class HabitsCSVExporterTest extends BaseAndroidTest assertPathExists("002 Meditate/Scores.csv"); } + private void assertAbsolutePathExists(String s) + { + File file = new File(s); + assertTrue( + String.format("File %s should exist", file.getAbsolutePath()), + file.exists()); + } + private void assertPathExists(String s) { - assertAbsolutePathExists(String.format("%s/%s", baseDir.getAbsolutePath(), s)); + assertAbsolutePathExists( + String.format("%s/%s", baseDir.getAbsolutePath(), s)); } - private void assertAbsolutePathExists(String s) + private void unzip(File file) throws IOException { - File file = new File(s); - assertTrue(String.format("File %s should exist", file.getAbsolutePath()), file.exists()); + ZipFile zip = new ZipFile(file); + Enumeration e = zip.entries(); + + while (e.hasMoreElements()) + { + ZipEntry entry = e.nextElement(); + InputStream stream = zip.getInputStream(entry); + + String outputFilename = + String.format("%s/%s", baseDir.getAbsolutePath(), + entry.getName()); + File outputFile = new File(outputFilename); + + File parent = outputFile.getParentFile(); + if (parent != null) parent.mkdirs(); + + FileUtils.copy(stream, outputFile); + } + + zip.close(); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java index 6ca6ba60a..4f73471b0 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java @@ -25,11 +25,10 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseAndroidTest; +import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.utils.FileUtils; import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.io.GenericImporter; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -60,7 +59,7 @@ public class ImportTest extends BaseAndroidTest super.setUp(); DateUtils.setFixedLocalTime(null); - HabitFixtures.purgeHabits(); + habitFixtures.purgeHabits(habitList); context = InstrumentationRegistry.getInstrumentation().getContext(); baseDir = FileUtils.getFilesDir("Backups"); if(baseDir == null) fail("baseDir should not be null"); @@ -89,7 +88,7 @@ public class ImportTest extends BaseAndroidTest { GregorianCalendar date = DateUtils.getStartOfTodayCalendar(); date.set(year, month - 1, day); - return h.repetitions.contains(date.getTimeInMillis()); + return h.getRepetitions().containsTimestamp(date.getTimeInMillis()); } @Test @@ -97,11 +96,11 @@ public class ImportTest extends BaseAndroidTest { importFromFile("tickmate.db"); - List habits = Habit.getAll(true); + List habits = habitList.getAll(true); assertThat(habits.size(), equalTo(3)); Habit h = habits.get(0); - assertThat(h.name, equalTo("Vegan")); + assertThat(h.getName(), equalTo("Vegan")); assertTrue(containsRepetition(h, 2016, 1, 24)); assertTrue(containsRepetition(h, 2016, 2, 5)); assertTrue(containsRepetition(h, 2016, 3, 18)); @@ -113,13 +112,13 @@ public class ImportTest extends BaseAndroidTest { importFromFile("rewire.db"); - List habits = Habit.getAll(true); + List habits = habitList.getAll(true); assertThat(habits.size(), equalTo(3)); Habit habit = habits.get(0); - assertThat(habit.name, equalTo("Wake up early")); - assertThat(habit.freqNum, equalTo(3)); - assertThat(habit.freqDen, equalTo(7)); + assertThat(habit.getName(), equalTo("Wake up early")); + assertThat(habit.getFreqNum(), equalTo(3)); + assertThat(habit.getFreqDen(), equalTo(7)); assertFalse(habit.hasReminder()); assertFalse(containsRepetition(habit, 2015, 12, 31)); assertTrue(containsRepetition(habit, 2016, 1, 18)); @@ -127,13 +126,13 @@ public class ImportTest extends BaseAndroidTest assertFalse(containsRepetition(habit, 2016, 3, 10)); habit = habits.get(1); - assertThat(habit.name, equalTo("brush teeth")); - assertThat(habit.freqNum, equalTo(3)); - assertThat(habit.freqDen, equalTo(7)); - assertThat(habit.reminderHour, equalTo(8)); - assertThat(habit.reminderMin, equalTo(0)); + assertThat(habit.getName(), equalTo("brush teeth")); + assertThat(habit.getFreqNum(), equalTo(3)); + assertThat(habit.getFreqDen(), equalTo(7)); + assertThat(habit.getReminderHour(), equalTo(8)); + assertThat(habit.getReminderMin(), equalTo(0)); boolean[] reminderDays = {false, true, true, true, true, true, false}; - assertThat(habit.reminderDays, equalTo(DateUtils.packWeekdayList(reminderDays))); + assertThat(habit.getReminderDays(), equalTo(DateUtils.packWeekdayList(reminderDays))); } @Test @@ -141,14 +140,14 @@ public class ImportTest extends BaseAndroidTest { importFromFile("habitbull.csv"); - List habits = Habit.getAll(true); + List habits = habitList.getAll(true); assertThat(habits.size(), equalTo(4)); Habit habit = habits.get(0); - assertThat(habit.name, equalTo("Breed dragons")); - assertThat(habit.description, equalTo("with love and fire")); - assertThat(habit.freqNum, equalTo(1)); - assertThat(habit.freqDen, equalTo(1)); + assertThat(habit.getName(), equalTo("Breed dragons")); + assertThat(habit.getDescription(), equalTo("with love and fire")); + assertThat(habit.getFreqNum(), equalTo(1)); + assertThat(habit.getFreqDen(), equalTo(1)); assertTrue(containsRepetition(habit, 2016, 3, 18)); assertTrue(containsRepetition(habit, 2016, 3, 19)); assertFalse(containsRepetition(habit, 2016, 3, 20)); @@ -159,13 +158,13 @@ public class ImportTest extends BaseAndroidTest { importFromFile("loop.db"); - List habits = Habit.getAll(true); + List habits = habitList.getAll(true); assertThat(habits.size(), equalTo(9)); Habit habit = habits.get(0); - assertThat(habit.name, equalTo("Wake up early")); - assertThat(habit.freqNum, equalTo(3)); - assertThat(habit.freqDen, equalTo(7)); + assertThat(habit.getName(), equalTo("Wake up early")); + assertThat(habit.getFreqNum(), equalTo(3)); + assertThat(habit.getFreqDen(), equalTo(7)); assertTrue(containsRepetition(habit, 2016, 3, 14)); assertTrue(containsRepetition(habit, 2016, 3, 16)); assertFalse(containsRepetition(habit, 2016, 3, 17)); 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 deleted file mode 100644 index 9af522aef..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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.BaseAndroidTest; -import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.IOException; -import java.io.StringWriter; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY; -import static org.isoron.uhabits.models.Checkmark.CHECKED_IMPLICITLY; -import static org.isoron.uhabits.models.Checkmark.UNCHECKED; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class CheckmarkListTest extends BaseAndroidTest -{ - Habit nonDailyHabit; - private Habit emptyHabit; - - @Before - public void setUp() - { - super.setUp(); - - HabitFixtures.purgeHabits(); - nonDailyHabit = HabitFixtures.createShortHabit(); - emptyHabit = HabitFixtures.createEmptyHabit(); - } - - @After - public void tearDown() - { - DateUtils.setFixedLocalTime(null); - } - - @Test - public void test_getAllValues_withNonDailyHabit() - { - int[] expectedValues = { CHECKED_EXPLICITLY, UNCHECKED, CHECKED_IMPLICITLY, - CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, UNCHECKED, - CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY }; - - int[] actualValues = nonDailyHabit.checkmarks.getAllValues(); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getAllValues_withEmptyHabit() - { - int[] expectedValues = new int[0]; - int[] actualValues = emptyHabit.checkmarks.getAllValues(); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getAllValues_moveForwardInTime() - { - travelInTime(3); - - int[] expectedValues = { UNCHECKED, UNCHECKED, UNCHECKED, CHECKED_EXPLICITLY, UNCHECKED, - CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, - UNCHECKED, CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY }; - - int[] actualValues = nonDailyHabit.checkmarks.getAllValues(); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getAllValues_moveBackwardsInTime() - { - travelInTime(-3); - - int[] expectedValues = { CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, - UNCHECKED, CHECKED_IMPLICITLY, CHECKED_EXPLICITLY, CHECKED_EXPLICITLY }; - - int[] actualValues = nonDailyHabit.checkmarks.getAllValues(); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getValues_withInvalidInterval() - { - int values[] = nonDailyHabit.checkmarks.getValues(100L, -100L); - assertThat(values, equalTo(new int[0])); - } - - @Test - public void test_getValues_withValidInterval() - { - long from = DateUtils.getStartOfToday() - 15 * DateUtils.millisecondsInOneDay; - long to = DateUtils.getStartOfToday() - 5 * DateUtils.millisecondsInOneDay; - - int[] expectedValues = { CHECKED_EXPLICITLY, UNCHECKED, CHECKED_IMPLICITLY, - CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, UNCHECKED, UNCHECKED, UNCHECKED, UNCHECKED, - UNCHECKED, UNCHECKED }; - - int[] actualValues = nonDailyHabit.checkmarks.getValues(from, to); - - assertThat(actualValues, equalTo(expectedValues)); - } - - @Test - public void test_getTodayValue() - { - travelInTime(-1); - assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(UNCHECKED)); - - travelInTime(0); - assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(CHECKED_EXPLICITLY)); - - travelInTime(1); - assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(UNCHECKED)); - } - - @Test - public void test_writeCSV() throws IOException - { - String expectedCSV = - "2015-01-16,2\n" + - "2015-01-17,2\n" + - "2015-01-18,1\n" + - "2015-01-19,0\n" + - "2015-01-20,2\n" + - "2015-01-21,2\n" + - "2015-01-22,2\n" + - "2015-01-23,1\n" + - "2015-01-24,0\n" + - "2015-01-25,2\n"; - - StringWriter writer = new StringWriter(); - nonDailyHabit.checkmarks.writeCSV(writer); - - assertThat(writer.toString(), equalTo(expectedCSV)); - } - - private void travelInTime(int days) - { - DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME + - days * DateUtils.millisecondsInOneDay); - } -} 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 deleted file mode 100644 index 265cd80f1..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * 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.hamcrest.MatcherAssert; -import org.isoron.uhabits.BaseAndroidTest; -import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.IOException; -import java.io.StringWriter; -import java.util.LinkedList; -import java.util.List; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.core.IsNot.not; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class HabitTest extends BaseAndroidTest -{ - @Before - public void setUp() - { - super.setUp(); - HabitFixtures.purgeHabits(); - } - - @Test - public void testConstructor_default() - { - Habit habit = new Habit(); - assertThat(habit.archived, is(0)); - assertThat(habit.highlight, is(0)); - - assertThat(habit.reminderHour, is(nullValue())); - assertThat(habit.reminderMin, is(nullValue())); - - assertThat(habit.reminderDays, is(not(nullValue()))); - assertThat(habit.streaks, is(not(nullValue()))); - assertThat(habit.scores, is(not(nullValue()))); - assertThat(habit.repetitions, is(not(nullValue()))); - assertThat(habit.checkmarks, is(not(nullValue()))); - } - - @Test - public void testConstructor_habit() - { - Habit model = new Habit(); - model.archived = 1; - model.highlight = 1; - model.color = 0; - model.freqNum = 10; - model.freqDen = 20; - model.reminderDays = 1; - model.reminderHour = 8; - model.reminderMin = 30; - model.position = 0; - - Habit habit = new Habit(model); - assertThat(habit.archived, is(model.archived)); - assertThat(habit.highlight, is(model.highlight)); - assertThat(habit.color, is(model.color)); - assertThat(habit.freqNum, is(model.freqNum)); - assertThat(habit.freqDen, is(model.freqDen)); - assertThat(habit.reminderDays, is(model.reminderDays)); - assertThat(habit.reminderHour, is(model.reminderHour)); - assertThat(habit.reminderMin, is(model.reminderMin)); - assertThat(habit.position, is(model.position)); - } - - @Test - public void test_get_withValidId() - { - Habit habit = new Habit(); - habit.save(); - - Habit habit2 = Habit.get(habit.getId()); - assertThat(habit, equalTo(habit2)); - } - - @Test - public void test_get_withInvalidId() - { - Habit habit = Habit.get(123456L); - assertThat(habit, is(nullValue())); - } - - @Test - public void test_getAll_withoutArchived() - { - List 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 test_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 test_count() - { - for(int i = 0; i < 10; i++) - { - Habit h = new Habit(); - if(i % 2 == 0) h.archived = 1; - h.save(); - } - - assertThat(Habit.count(), equalTo(5)); - } - - - @Test - public void test_countWithArchived() - { - for(int i = 0; i < 10; i++) - { - Habit h = new Habit(); - if(i % 2 == 0) h.archived = 1; - h.save(); - } - - assertThat(Habit.countWithArchived(), equalTo(10)); - } - - @Test - public void test_updateId() - { - Habit habit = new Habit(); - habit.name = "Hello World"; - habit.save(); - - Long oldId = habit.getId(); - Long newId = 123456L; - Habit.updateId(oldId, newId); - - Habit newHabit = Habit.get(newId); - if(newHabit == null) fail(); - assertThat(newHabit, is(not(nullValue()))); - assertThat(newHabit.name, equalTo(habit.name)); - } - - @Test - public void test_reorder() - { - List ids = new LinkedList<>(); - - int n = 10; - for (int i = 0; i < n; i++) - { - Habit h = new Habit(); - h.save(); - ids.add(h.getId()); - assertThat(h.position, is(i)); - } - - int operations[][] = { - {5, 2}, - {3, 7}, - {4, 4}, - {3, 2} - }; - - int expectedPosition[][] = { - {0, 1, 3, 4, 5, 2, 6, 7, 8, 9}, - {0, 1, 7, 3, 4, 2, 5, 6, 8, 9}, - {0, 1, 7, 3, 4, 2, 5, 6, 8, 9}, - {0, 1, 7, 2, 4, 3, 5, 6, 8, 9}, - }; - - for(int i = 0; i < operations.length; i++) - { - int from = operations[i][0]; - int to = operations[i][1]; - - Habit fromHabit = Habit.getByPosition(from); - Habit toHabit = Habit.getByPosition(to); - Habit.reorder(fromHabit, toHabit); - - int actualPositions[] = new int[n]; - - for (int j = 0; j < n; j++) - { - Habit h = Habit.get(ids.get(j)); - if (h == null) fail(); - actualPositions[j] = h.position; - } - - assertThat(actualPositions, equalTo(expectedPosition[i])); - } - } - - @Test - public void test_rebuildOrder() - { - List ids = new LinkedList<>(); - int originalPositions[] = { 0, 1, 1, 4, 6, 8, 10, 10, 13}; - - for (int p : originalPositions) - { - Habit h = new Habit(); - h.position = p; - h.save(); - ids.add(h.getId()); - } - - Habit.rebuildOrder(); - - for (int i = 0; i < originalPositions.length; i++) - { - Habit h = Habit.get(ids.get(i)); - if(h == null) fail(); - assertThat(h.position, is(i)); - } - } - - @Test - public void test_getHabitsWithReminder() - { - List habitsWithReminder = new LinkedList<>(); - - for(int i = 0; i < 10; i++) - { - Habit habit = new Habit(); - if(i % 2 == 0) - { - habit.reminderDays = DateUtils.ALL_WEEK_DAYS; - habit.reminderHour = 8; - habit.reminderMin = 30; - habitsWithReminder.add(habit); - } - habit.save(); - } - - assertThat(habitsWithReminder, equalTo(Habit.getHabitsWithReminder())); - } - - @Test - public void test_archive_unarchive() - { - List 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 test_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 test_hasReminder_clearReminder() - { - Habit h = new Habit(); - assertThat(h.hasReminder(), is(false)); - - h.reminderDays = DateUtils.ALL_WEEK_DAYS; - h.reminderHour = 8; - h.reminderMin = 30; - assertThat(h.hasReminder(), is(true)); - - h.clearReminder(); - assertThat(h.hasReminder(), is(false)); - } - - @Test - public void test_writeCSV() throws IOException - { - HabitFixtures.createEmptyHabit(); - HabitFixtures.createShortHabit(); - - String expectedCSV = - "Position,Name,Description,NumRepetitions,Interval,Color\n" + - "001,Meditate,Did you meditate this morning?,1,1,#AFB42B\n" + - "002,Wake up early,Did you wake up before 6am?,2,3,#00897B\n"; - - StringWriter writer = new StringWriter(); - Habit.writeCSV(Habit.getAll(true), writer); - - MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV)); - } -} 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 deleted file mode 100644 index 5cfbf95e5..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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.BaseAndroidTest; -import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Repetition; -import org.isoron.uhabits.unit.HabitFixtures; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.Arrays; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.Random; - -import static junit.framework.Assert.assertFalse; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class RepetitionListTest extends BaseAndroidTest -{ - private Habit habit; - private Habit emptyHabit; - - @Before - public void setUp() - { - super.setUp(); - - HabitFixtures.purgeHabits(); - habit = HabitFixtures.createShortHabit(); - emptyHabit = HabitFixtures.createEmptyHabit(); - } - - @After - public void tearDown() - { - DateUtils.setFixedLocalTime(null); - } - - @Test - public void test_contains() - { - long current = DateUtils.getStartOfToday(); - - for(boolean b : HabitFixtures.NON_DAILY_HABIT_CHECKS) - { - assertThat(habit.repetitions.contains(current), equalTo(b)); - current -= DateUtils.millisecondsInOneDay; - } - - for(int i = 0; i < 3; i++) - { - assertThat(habit.repetitions.contains(current), equalTo(false)); - current -= DateUtils.millisecondsInOneDay; - } - } - - @Test - public void test_delete() - { - long timestamp = DateUtils.getStartOfToday(); - assertThat(habit.repetitions.contains(timestamp), equalTo(true)); - - habit.repetitions.delete(timestamp); - assertThat(habit.repetitions.contains(timestamp), equalTo(false)); - } - - @Test - public void test_toggle() - { - long timestamp = DateUtils.getStartOfToday(); - assertThat(habit.repetitions.contains(timestamp), equalTo(true)); - - habit.repetitions.toggle(timestamp); - assertThat(habit.repetitions.contains(timestamp), equalTo(false)); - - habit.repetitions.toggle(timestamp); - assertThat(habit.repetitions.contains(timestamp), equalTo(true)); - } - - @Test - public void test_getWeekDayFrequency() - { - Random random = new Random(); - Integer weekdayCount[][] = new Integer[12][7]; - Integer monthCount[] = new Integer[12]; - - Arrays.fill(monthCount, 0); - for(Integer row[] : weekdayCount) - Arrays.fill(row, 0); - - GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); - - // Sets the current date to the end of November - day.set(2015, 10, 30); - DateUtils.setFixedLocalTime(day.getTimeInMillis()); - - // Add repetitions randomly from January to December - // Leaves the month of March empty, to check that it returns null - day.set(2015, 0, 1); - for(int i = 0; i < 365; i ++) - { - if(random.nextBoolean()) - { - int month = day.get(Calendar.MONTH); - int week = day.get(Calendar.DAY_OF_WEEK) % 7; - - if(month != 2) - { - if (month <= 10) - { - weekdayCount[month][week]++; - monthCount[month]++; - } - emptyHabit.repetitions.toggle(day.getTimeInMillis()); - } - } - - day.add(Calendar.DAY_OF_YEAR, 1); - } - - HashMap freq = emptyHabit.repetitions.getWeekdayFrequency(); - - // Repetitions until November should be counted correctly - for(int month = 0; month < 11; month++) - { - day.set(2015, month, 1); - Integer actualCount[] = freq.get(day.getTimeInMillis()); - if(monthCount[month] == 0) - assertThat(actualCount, equalTo(null)); - else - assertThat(actualCount, equalTo(weekdayCount[month])); - } - - // Repetitions in December should be discarded - day.set(2015, 11, 1); - assertThat(freq.get(day.getTimeInMillis()), equalTo(null)); - } - - @Test - public void test_count() - { - long to = DateUtils.getStartOfToday(); - long from = to - 9 * DateUtils.millisecondsInOneDay; - assertThat(habit.repetitions.count(from, to), equalTo(6)); - - to = DateUtils.getStartOfToday() - DateUtils.millisecondsInOneDay; - from = to - 5 * DateUtils.millisecondsInOneDay; - assertThat(habit.repetitions.count(from, to), equalTo(3)); - } - - @Test - public void test_getOldest() - { - long expectedOldestTimestamp = DateUtils.getStartOfToday() - 9 * DateUtils.millisecondsInOneDay; - - assertThat(habit.repetitions.getOldestTimestamp(), equalTo(expectedOldestTimestamp)); - - Repetition oldest = habit.repetitions.getOldest(); - assertFalse(oldest == null); - assertThat(oldest.timestamp, equalTo(expectedOldestTimestamp)); - } -} 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 index 7ab145a37..fdda41504 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java @@ -23,11 +23,9 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.BaseAndroidTest; -import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.utils.DatabaseUtils; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Score; -import org.isoron.uhabits.unit.HabitFixtures; +import org.isoron.uhabits.utils.DatabaseUtils; +import org.isoron.uhabits.utils.DateUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -50,8 +48,8 @@ public class ScoreListTest extends BaseAndroidTest { super.setUp(); - HabitFixtures.purgeHabits(); - habit = HabitFixtures.createEmptyHabit(); + habitFixtures.purgeHabits(habitList); + habit = habitFixtures.createEmptyHabit(); } @After @@ -61,38 +59,53 @@ public class ScoreListTest extends BaseAndroidTest } @Test - public void test_invalidateNewerThan() + public void test_getAllValues_withGroups() { - assertThat(habit.scores.getTodayValue(), equalTo(0)); - - toggleRepetitions(0, 2); - assertThat(habit.scores.getTodayValue(), equalTo(1948077)); + toggleRepetitions(0, 20); - habit.freqNum = 1; - habit.freqDen = 2; - habit.scores.invalidateNewerThan(0); + int expectedValues[] = {11434978, 7894999, 3212362}; - assertThat(habit.scores.getTodayValue(), equalTo(1974654)); + int actualValues[] = habit.getScores().getAllValues(7); + assertThat(actualValues, equalTo(expectedValues)); } @Test - public void test_getTodayStarValue() + public void test_getAllValues_withoutGroups() { - 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)); + toggleRepetitions(0, 20); - while(habit.scores.getTodayValue() < Score.FULL_STAR_CUTOFF) toggleRepetitions(k, ++k); - assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.FULL_STAR)); + 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.getScores().getAllValues(1); + assertThat(actualValues, equalTo(expectedValues)); } @Test public void test_getTodayValue() { toggleRepetitions(0, 20); - assertThat(habit.scores.getTodayValue(), equalTo(12629351)); + assertThat(habit.getScores().getTodayValue(), equalTo(12629351)); } @Test @@ -100,77 +113,84 @@ public class ScoreListTest extends BaseAndroidTest { toggleRepetitions(0, 20); - int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773, - 10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023, - 4507040, 3699107, 2846927, 1948077, 1000000 }; + int expectedValues[] = { + 12629351, + 12266245, + 11883254, + 11479288, + 11053198, + 10603773, + 10129735, + 9629735, + 9102352, + 8546087, + 7959357, + 7340494, + 6687738, + 5999234, + 5273023, + 4507040, + 3699107, + 2846927, + 1948077, + 1000000 + }; long current = DateUtils.getStartOfToday(); - for(int expectedValue : expectedValues) + for (int expectedValue : expectedValues) { - assertThat(habit.scores.getValue(current), equalTo(expectedValue)); + assertThat(habit.getScores().getValue(current), + equalTo(expectedValue)); current -= DateUtils.millisecondsInOneDay; } } @Test - public void test_getAllValues_withoutGroups() + public void test_invalidateNewerThan() { - 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)); - } + assertThat(habit.getScores().getTodayValue(), equalTo(0)); - @Test - public void test_getAllValues_withGroups() - { - toggleRepetitions(0, 20); + toggleRepetitions(0, 2); + assertThat(habit.getScores().getTodayValue(), equalTo(1948077)); - int expectedValues[] = { 11434978, 7894999, 3212362 }; + habit.setFreqNum(1); + habit.setFreqDen(2); + habit.getScores().invalidateNewerThan(0); - int actualValues[] = habit.scores.getAllValues(7); - assertThat(actualValues, equalTo(expectedValues)); + assertThat(habit.getScores().getTodayValue(), equalTo(1974654)); } @Test public void test_writeCSV() throws IOException { - HabitFixtures.purgeHabits(); - Habit habit = HabitFixtures.createShortHabit(); - - String expectedCSV = - "2015-01-16,0.0519\n" + - "2015-01-17,0.1021\n" + - "2015-01-18,0.0986\n" + - "2015-01-19,0.0952\n" + - "2015-01-20,0.1439\n" + - "2015-01-21,0.1909\n" + - "2015-01-22,0.2364\n" + - "2015-01-23,0.2283\n" + - "2015-01-24,0.2205\n" + - "2015-01-25,0.2649\n"; + habitFixtures.purgeHabits(habitList); + Habit habit = habitFixtures.createShortHabit(); + + String expectedCSV = "2015-01-16,0.0519\n" + + "2015-01-17,0.1021\n" + + "2015-01-18,0.0986\n" + + "2015-01-19,0.0952\n" + + "2015-01-20,0.1439\n" + + "2015-01-21,0.1909\n" + + "2015-01-22,0.2364\n" + + "2015-01-23,0.2283\n" + + "2015-01-24,0.2205\n" + + "2015-01-25,0.2649\n"; StringWriter writer = new StringWriter(); - habit.scores.writeCSV(writer); + habit.getScores().writeCSV(writer); assertThat(writer.toString(), equalTo(expectedCSV)); } private void toggleRepetitions(final int from, final int to) { - DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command() - { - @Override - public void execute() - { - long today = DateUtils.getStartOfToday(); - for (int i = from; i < to; i++) - habit.repetitions.toggle(today - i * DateUtils.millisecondsInOneDay); - } + DatabaseUtils.executeAsTransaction(() -> { + long today = DateUtils.getStartOfToday(); + for (int i = from; i < to; i++) + habit + .getRepetitions() + .toggleTimestamp(today - i * DateUtils.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 index 16f6a5297..bc0a31c31 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java @@ -36,6 +36,7 @@ import static org.junit.Assert.assertThat; @SmallTest public class ScoreTest extends BaseAndroidTest { + @Override @Before public void setUp() { @@ -61,48 +62,24 @@ public class ScoreTest extends BaseAndroidTest assertThat(Score.compute(1, 0, checkmark), equalTo(1000000)); assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387)); assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775)); - assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE)); + assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo( + Score.MAX_VALUE)); } @Test public void test_compute_withNonDailyHabit() { int checkmark = Checkmark.CHECKED_EXPLICITLY; - assertThat(Score.compute(1/3.0, 0, checkmark), equalTo(1000000)); - assertThat(Score.compute(1/3.0, 5000000, checkmark), equalTo(5916180)); - assertThat(Score.compute(1/3.0, 10000000, checkmark), equalTo(10832360)); - assertThat(Score.compute(1/3.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE)); - - assertThat(Score.compute(1/7.0, 0, checkmark), equalTo(1000000)); - assertThat(Score.compute(1/7.0, 5000000, checkmark), equalTo(5964398)); - assertThat(Score.compute(1/7.0, 10000000, checkmark), equalTo(10928796)); - assertThat(Score.compute(1/7.0, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE)); - } - - @Test - public void test_getStarStatus() - { - Score s = new Score(); - - s.score = Score.FULL_STAR_CUTOFF + 1; - assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR)); - - s.score = Score.FULL_STAR_CUTOFF; - assertThat(s.getStarStatus(), equalTo(Score.FULL_STAR)); - - s.score = Score.FULL_STAR_CUTOFF - 1; - assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR)); - - s.score = Score.HALF_STAR_CUTOFF + 1; - assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR)); - - s.score = Score.HALF_STAR_CUTOFF; - assertThat(s.getStarStatus(), equalTo(Score.HALF_STAR)); - - s.score = Score.HALF_STAR_CUTOFF - 1; - assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR)); - - s.score = 0; - assertThat(s.getStarStatus(), equalTo(Score.EMPTY_STAR)); + assertThat(Score.compute(1 / 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)); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java index 189dfdc96..51cb53dae 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java @@ -52,21 +52,16 @@ public class ExportCSVTaskTest extends BaseAndroidTest @Test public void testExportCSV() throws Throwable { - HabitFixtures.createShortHabit(); - List habits = Habit.getAll(true); + habitFixtures.createShortHabit(); + List habits = habitList.getAll(true); ExportCSVTask task = new ExportCSVTask(habits, null); - task.setListener(new ExportCSVTask.Listener() - { - @Override - public void onExportCSVFinished(String archiveFilename) - { - assertThat(archiveFilename, is(not(nullValue()))); + task.setListener(archiveFilename -> { + assertThat(archiveFilename, is(not(nullValue()))); - File f = new File(archiveFilename); - assertTrue(f.exists()); - assertTrue(f.canRead()); - } + File f = new File(archiveFilename); + assertTrue(f.exists()); + assertTrue(f.canRead()); }); task.execute(); diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/ui/habits/list/view/CheckmarkButtonViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/ui/habits/list/view/CheckmarkButtonViewTest.java index 599a8e5f2..1a99e87df 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/ui/habits/list/view/CheckmarkButtonViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/ui/habits/list/view/CheckmarkButtonViewTest.java @@ -40,8 +40,10 @@ public class CheckmarkButtonViewTest extends ViewTest public static final String PATH = "ui/habits/list/CheckmarkButtonView/"; private CountDownLatch latch; + private CheckmarkButtonView view; + @Override @Before public void setUp() { @@ -51,24 +53,23 @@ public class CheckmarkButtonViewTest extends ViewTest latch = new CountDownLatch(1); view = new CheckmarkButtonView(targetContext); view.setValue(Checkmark.UNCHECKED); - view.setColor(ColorUtils.CSV_PALETTE[7]); + view.setColor(ColorUtils.getAndroidTestColor(7)); measureView(dpToPixels(40), dpToPixels(40), view); } - protected void assertRendersCheckedExplicitly() throws IOException - { - assertRenders(view, PATH + "render_explicit_check.png"); - } - - protected void assertRendersUnchecked() throws IOException + @Test + public void testRender_explicitCheck() throws Exception { - assertRenders(view, PATH + "render_unchecked.png"); + view.setValue(Checkmark.CHECKED_EXPLICITLY); + assertRendersCheckedExplicitly(); } - protected void assertRendersCheckedImplicitly() throws IOException + @Test + public void testRender_implicitCheck() throws Exception { - assertRenders(view, PATH + "render_implicit_check.png"); + view.setValue(Checkmark.CHECKED_IMPLICITLY); + assertRendersCheckedImplicitly(); } @Test @@ -78,18 +79,19 @@ public class CheckmarkButtonViewTest extends ViewTest assertRendersUnchecked(); } - @Test - public void testRender_explicitCheck() throws Exception + protected void assertRendersCheckedExplicitly() throws IOException { - view.setValue(Checkmark.CHECKED_EXPLICITLY); - assertRendersCheckedExplicitly(); + assertRenders(view, PATH + "render_explicit_check.png"); } - @Test - public void testRender_implicitCheck() throws Exception + protected void assertRendersCheckedImplicitly() throws IOException { - view.setValue(Checkmark.CHECKED_IMPLICITLY); - assertRendersCheckedImplicitly(); + assertRenders(view, PATH + "render_implicit_check.png"); + } + + protected void assertRendersUnchecked() throws IOException + { + assertRenders(view, PATH + "render_unchecked.png"); } // @Test diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/ui/habits/list/view/CheckmarkPanelViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/ui/habits/list/view/CheckmarkPanelViewTest.java index 1f6edd1c4..bd57a3818 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/ui/habits/list/view/CheckmarkPanelViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/ui/habits/list/view/CheckmarkPanelViewTest.java @@ -54,13 +54,14 @@ public class CheckmarkPanelViewTest extends ViewTest Habit habit = new Habit(); latch = new CountDownLatch(1); - checkmarks = new int[]{Checkmark.CHECKED_EXPLICITLY, Checkmark.UNCHECKED, + checkmarks = new int[]{ + Checkmark.CHECKED_EXPLICITLY, Checkmark.UNCHECKED, Checkmark.CHECKED_IMPLICITLY, Checkmark.CHECKED_EXPLICITLY}; view = new CheckmarkPanelView(targetContext); view.setHabit(habit); view.setCheckmarkValues(checkmarks); - view.setColor(ColorUtils.CSV_PALETTE[7]); + view.setColor(ColorUtils.getAndroidTestColor(7)); measureView(dpToPixels(200), dpToPixels(200), view); } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/CheckmarkWidgetViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/CheckmarkWidgetViewTest.java index 5cde3d739..a5eb79627 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/CheckmarkWidgetViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/views/CheckmarkWidgetViewTest.java @@ -23,11 +23,10 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.R; +import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.InterfaceUtils; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.CheckmarkWidgetView; +import org.isoron.uhabits.widgets.views.CheckmarkWidgetView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,6 +38,7 @@ import java.io.IOException; public class CheckmarkWidgetViewTest extends ViewTest { private CheckmarkWidgetView view; + private Habit habit; @Before @@ -47,7 +47,7 @@ public class CheckmarkWidgetViewTest extends ViewTest super.setUp(); InterfaceUtils.setFixedTheme(R.style.TransparentWidgetTheme); - habit = HabitFixtures.createShortHabit(); + habit = habitFixtures.createShortHabit(); view = new CheckmarkWidgetView(targetContext); view.setHabit(habit); refreshData(view); @@ -60,23 +60,14 @@ public class CheckmarkWidgetViewTest extends ViewTest assertRenders(view, "CheckmarkView/checked.png"); } - @Test - public void testRender_unchecked() throws IOException - { - habit.repetitions.toggle(DateUtils.getStartOfToday()); - view.refreshData(); - - assertRenders(view, "CheckmarkView/unchecked.png"); - } - @Test public void testRender_implicitlyChecked() throws IOException { long today = DateUtils.getStartOfToday(); long day = DateUtils.millisecondsInOneDay; - habit.repetitions.toggle(today); - habit.repetitions.toggle(today - day); - habit.repetitions.toggle(today - 2 * day); + habit.getRepetitions().toggleTimestamp(today); + habit.getRepetitions().toggleTimestamp(today - day); + habit.getRepetitions().toggleTimestamp(today - 2 * day); view.refreshData(); assertRenders(view, "CheckmarkView/implicitly_checked.png"); @@ -88,4 +79,13 @@ public class CheckmarkWidgetViewTest extends ViewTest measureView(dpToPixels(300), dpToPixels(300), view); assertRenders(view, "CheckmarkView/large_size.png"); } + + @Test + public void testRender_unchecked() throws IOException + { + habit.getRepetitions().toggleTimestamp(DateUtils.getStartOfToday()); + view.refreshData(); + + assertRenders(view, "CheckmarkView/unchecked.png"); + } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitFrequencyViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitFrequencyViewTest.java index b99cb4c89..f43578699 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitFrequencyViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitFrequencyViewTest.java @@ -23,8 +23,7 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.HabitFrequencyView; +import org.isoron.uhabits.ui.habits.show.views.HabitFrequencyView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,8 +39,8 @@ public class HabitFrequencyViewTest extends ViewTest { super.setUp(); - HabitFixtures.purgeHabits(); - Habit habit = HabitFixtures.createLongHabit(); + habitFixtures.purgeHabits(habitList); + Habit habit = habitFixtures.createLongHabit(); view = new HabitFrequencyView(targetContext); view.setHabit(habit); @@ -56,10 +55,12 @@ public class HabitFrequencyViewTest extends ViewTest } @Test - public void testRender_withTransparentBackground() throws Throwable + public void testRender_withDataOffset() throws Throwable { - view.setIsBackgroundTransparent(true); - assertRenders(view, "HabitFrequencyView/renderTransparent.png"); + view.onScroll(null, null, -dpToPixels(150), 0); + view.invalidate(); + + assertRenders(view, "HabitFrequencyView/renderDataOffset.png"); } @Test @@ -70,11 +71,9 @@ public class HabitFrequencyViewTest extends ViewTest } @Test - public void testRender_withDataOffset() throws Throwable + public void testRender_withTransparentBackground() throws Throwable { - view.onScroll(null, null, -dpToPixels(150), 0); - view.invalidate(); - - assertRenders(view, "HabitFrequencyView/renderDataOffset.png"); + view.setIsBackgroundTransparent(true); + assertRenders(view, "HabitFrequencyView/renderTransparent.png"); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitHistoryViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitHistoryViewTest.java index 7bf667491..f2dcb0721 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitHistoryViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitHistoryViewTest.java @@ -22,10 +22,9 @@ package org.isoron.uhabits.unit.views; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.HabitHistoryView; +import org.isoron.uhabits.utils.DateUtils; +import org.isoron.uhabits.ui.habits.show.views.HabitHistoryView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -40,6 +39,7 @@ import static org.hamcrest.Matchers.equalTo; public class HabitHistoryViewTest extends ViewTest { private Habit habit; + private HabitHistoryView view; @Before @@ -47,8 +47,8 @@ public class HabitHistoryViewTest extends ViewTest { super.setUp(); - HabitFixtures.purgeHabits(); - habit = HabitFixtures.createLongHabit(); + habitFixtures.purgeHabits(habitList); + habit = habitFixtures.createLongHabit(); view = new HabitHistoryView(targetContext); view.setHabit(habit); @@ -57,69 +57,69 @@ public class HabitHistoryViewTest extends ViewTest } @Test - public void testRender() throws Throwable + public void tapDate_atInvalidLocations() throws Throwable { - assertRenders(view, "HabitHistoryView/render.png"); - } + int expectedCheckmarkValues[] = habit.getCheckmarks().getAllValues(); - @Test - public void testRender_withTransparentBackground() throws Throwable - { - view.setIsBackgroundTransparent(true); - assertRenders(view, "HabitHistoryView/renderTransparent.png"); - } + view.setIsEditable(true); + tap(view, 118, 13); // header + tap(view, 336, 60); // tomorrow's square + tap(view, 370, 60); // right axis + waitForAsyncTasks(); - @Test - public void testRender_withDifferentSize() throws Throwable - { - measureView(dpToPixels(200), dpToPixels(200), view); - assertRenders(view, "HabitHistoryView/renderDifferentSize.png"); + int actualCheckmarkValues[] = habit.getCheckmarks().getAllValues(); + assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues)); } @Test - public void testRender_withDataOffset() throws Throwable + public void tapDate_withEditableView() throws Throwable { - view.onScroll(null, null, -dpToPixels(150), 0); - view.invalidate(); + view.setIsEditable(true); + tap(view, 340, 40); // today's square + waitForAsyncTasks(); - assertRenders(view, "HabitHistoryView/renderDataOffset.png"); + long today = DateUtils.getStartOfToday(); + assertFalse(habit.getRepetitions().containsTimestamp(today)); } @Test - public void tapDate_withEditableView() throws Throwable + public void tapDate_withReadOnlyView() throws Throwable { - view.setIsEditable(true); + view.setIsEditable(false); tap(view, 340, 40); // today's square waitForAsyncTasks(); long today = DateUtils.getStartOfToday(); - assertFalse(habit.repetitions.contains(today)); + assertTrue(habit.getRepetitions().containsTimestamp(today)); } @Test - public void tapDate_atInvalidLocations() throws Throwable + public void testRender() throws Throwable { - int expectedCheckmarkValues[] = habit.checkmarks.getAllValues(); + assertRenders(view, "HabitHistoryView/render.png"); + } - view.setIsEditable(true); - tap(view, 118, 13); // header - tap(view, 336, 60); // tomorrow's square - tap(view, 370, 60); // right axis - waitForAsyncTasks(); + @Test + public void testRender_withDataOffset() throws Throwable + { + view.onScroll(null, null, -dpToPixels(150), 0); + view.invalidate(); - int actualCheckmarkValues[] = habit.checkmarks.getAllValues(); - assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues)); + assertRenders(view, "HabitHistoryView/renderDataOffset.png"); } @Test - public void tapDate_withReadOnlyView() throws Throwable + public void testRender_withDifferentSize() throws Throwable { - view.setIsEditable(false); - tap(view, 340, 40); // today's square - waitForAsyncTasks(); + measureView(dpToPixels(200), dpToPixels(200), view); + assertRenders(view, "HabitHistoryView/renderDifferentSize.png"); + } - long today = DateUtils.getStartOfToday(); - assertTrue(habit.repetitions.contains(today)); + @Test + public void testRender_withTransparentBackground() throws Throwable + { + view.setIsBackgroundTransparent(true); + assertRenders(view, "HabitHistoryView/renderTransparent.png"); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitScoreViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitScoreViewTest.java index 1f5b13a2f..31c0d84a0 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitScoreViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitScoreViewTest.java @@ -24,8 +24,7 @@ import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.HabitScoreView; +import org.isoron.uhabits.ui.habits.show.views.HabitScoreView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,6 +34,7 @@ import org.junit.runner.RunWith; public class HabitScoreViewTest extends ViewTest { private Habit habit; + private HabitScoreView view; @Before @@ -42,8 +42,8 @@ public class HabitScoreViewTest extends ViewTest { super.setUp(); - HabitFixtures.purgeHabits(); - habit = HabitFixtures.createLongHabit(); + habitFixtures.purgeHabits(habitList); + habit = habitFixtures.createLongHabit(); view = new HabitScoreView(targetContext); view.setHabit(habit); @@ -55,15 +55,18 @@ public class HabitScoreViewTest extends ViewTest @Test public void testRender() throws Throwable { - Log.d("HabitScoreViewTest", String.format("height=%d", dpToPixels(100))); + Log.d("HabitScoreViewTest", + String.format("height=%d", dpToPixels(100))); assertRenders(view, "HabitScoreView/render.png"); } @Test - public void testRender_withTransparentBackground() throws Throwable + public void testRender_withDataOffset() throws Throwable { - view.setIsTransparencyEnabled(true); - assertRenders(view, "HabitScoreView/renderTransparent.png"); + view.onScroll(null, null, -dpToPixels(150), 0); + view.invalidate(); + + assertRenders(view, "HabitScoreView/renderDataOffset.png"); } @Test @@ -73,15 +76,6 @@ public class HabitScoreViewTest extends ViewTest assertRenders(view, "HabitScoreView/renderDifferentSize.png"); } - @Test - public void testRender_withDataOffset() throws Throwable - { - view.onScroll(null, null, -dpToPixels(150), 0); - view.invalidate(); - - assertRenders(view, "HabitScoreView/renderDataOffset.png"); - } - @Test public void testRender_withMonthlyBucket() throws Throwable { @@ -92,6 +86,13 @@ public class HabitScoreViewTest extends ViewTest assertRenders(view, "HabitScoreView/renderMonthly.png"); } + @Test + public void testRender_withTransparentBackground() throws Throwable + { + view.setIsTransparencyEnabled(true); + assertRenders(view, "HabitScoreView/renderTransparent.png"); + } + @Test public void testRender_withYearlyBucket() throws Throwable { diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitStreakViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitStreakViewTest.java index 3b65c5a5d..9cb86086f 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitStreakViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/views/HabitStreakViewTest.java @@ -23,8 +23,7 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.unit.HabitFixtures; -import org.isoron.uhabits.views.HabitStreakView; +import org.isoron.uhabits.ui.habits.show.views.HabitStreakView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,13 +34,14 @@ public class HabitStreakViewTest extends ViewTest { private HabitStreakView view; + @Override @Before public void setUp() { super.setUp(); - HabitFixtures.purgeHabits(); - Habit habit = HabitFixtures.createLongHabit(); + habitFixtures.purgeHabits(habitList); + Habit habit = habitFixtures.createLongHabit(); view = new HabitStreakView(targetContext); measureView(dpToPixels(300), dpToPixels(100), view); @@ -56,13 +56,6 @@ public class HabitStreakViewTest extends ViewTest assertRenders(view, "HabitStreakView/render.png"); } - @Test - public void testRender_withTransparentBackground() throws Throwable - { - view.setIsBackgroundTransparent(true); - assertRenders(view, "HabitStreakView/renderTransparent.png"); - } - @Test public void testRender_withSmallSize() throws Throwable { @@ -71,4 +64,11 @@ public class HabitStreakViewTest extends ViewTest assertRenders(view, "HabitStreakView/renderSmallSize.png"); } + + @Test + public void testRender_withTransparentBackground() throws Throwable + { + view.setIsBackgroundTransparent(true); + assertRenders(view, "HabitStreakView/renderTransparent.png"); + } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/NumberViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/NumberViewTest.java deleted file mode 100644 index ca61aac04..000000000 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/NumberViewTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.views; - -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.utils.ColorUtils; -import org.isoron.uhabits.views.NumberView; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.IOException; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class NumberViewTest extends ViewTest -{ - private NumberView view; - - @Before - public void setUp() - { - super.setUp(); - - view = new NumberView(targetContext); - view.setLabel("Hello world"); - view.setNumber(31); - view.setColor(ColorUtils.CSV_PALETTE[0]); - measureView(dpToPixels(100), dpToPixels(100), view); - } - - @Test - public void testRender_base() throws IOException - { - assertRenders(view, "NumberView/render.png"); - } - - @Test - public void testRender_withLongLabel() throws IOException - { - view.setLabel("The quick brown fox jumps over the lazy fox"); - - measureView(dpToPixels(100), dpToPixels(100), view); - assertRenders(view, "NumberView/renderLongLabel.png"); - } - - @Test - public void testRender_withDifferentParams() throws IOException - { - view.setNumber(500); - view.setColor(ColorUtils.CSV_PALETTE[5]); - view.setTextSize(targetContext.getResources().getDimension(R.dimen.tinyTextSize)); - - measureView(dpToPixels(200), dpToPixels(200), view); - assertRenders(view, "NumberView/renderDifferentParams.png"); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/RingViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/RingViewTest.java index 972fae3c8..0b6ba8c47 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/RingViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/views/RingViewTest.java @@ -24,7 +24,7 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.isoron.uhabits.utils.ColorUtils; -import org.isoron.uhabits.views.RingView; +import org.isoron.uhabits.ui.habits.show.views.RingView; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,7 +45,7 @@ public class RingViewTest extends ViewTest view = new RingView(targetContext); view.setPercentage(0.6f); view.setText("60%"); - view.setColor(ColorUtils.CSV_PALETTE[0]); + view.setColor(ColorUtils.getAndroidTestColor(0)); view.setBackgroundColor(Color.WHITE); view.setThickness(dpToPixels(3)); } @@ -61,7 +61,7 @@ public class RingViewTest extends ViewTest public void testRender_withDifferentParams() throws IOException { view.setPercentage(0.25f); - view.setColor(ColorUtils.CSV_PALETTE[5]); + view.setColor(ColorUtils.getAndroidTestColor(5)); measureView(dpToPixels(200), dpToPixels(200), view); assertRenders(view, "RingView/renderDifferentParams.png"); diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/views/ViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/views/ViewTest.java index 691e9d50e..1f71cea9a 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/views/ViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/views/ViewTest.java @@ -30,7 +30,7 @@ import org.isoron.uhabits.BaseAndroidTest; import org.isoron.uhabits.utils.FileUtils; import org.isoron.uhabits.utils.InterfaceUtils; import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.views.HabitDataView; +import org.isoron.uhabits.ui.habits.show.views.HabitDataView; import java.io.File; import java.io.FileOutputStream; diff --git a/app/src/main/java/org/isoron/uhabits/AndroidComponent.java b/app/src/main/java/org/isoron/uhabits/AndroidComponent.java index 45eb998d9..4a53960e0 100644 --- a/app/src/main/java/org/isoron/uhabits/AndroidComponent.java +++ b/app/src/main/java/org/isoron/uhabits/AndroidComponent.java @@ -23,6 +23,9 @@ import javax.inject.Singleton; import dagger.Component; +/** + * Dependency injection component for classes that are specific to Android. + */ @Singleton @Component(modules = {AndroidModule.class}) public interface AndroidComponent extends BaseComponent diff --git a/app/src/main/java/org/isoron/uhabits/AndroidModule.java b/app/src/main/java/org/isoron/uhabits/AndroidModule.java index 3bbc1d4d1..bd658b9f8 100644 --- a/app/src/main/java/org/isoron/uhabits/AndroidModule.java +++ b/app/src/main/java/org/isoron/uhabits/AndroidModule.java @@ -20,6 +20,10 @@ package org.isoron.uhabits; import org.isoron.uhabits.commands.CommandRunner; +import org.isoron.uhabits.models.HabitList; +import org.isoron.uhabits.models.ModelFactory; +import org.isoron.uhabits.models.sqlite.SQLModelFactory; +import org.isoron.uhabits.models.sqlite.SQLiteHabitList; import org.isoron.uhabits.ui.habits.list.model.HabitCardListCache; import org.isoron.uhabits.utils.Preferences; @@ -28,27 +32,46 @@ import javax.inject.Singleton; import dagger.Module; import dagger.Provides; +/** + * Module that provides dependencies when the application is running on + * Android. + *

+ * This module is also used for instrumented tests. + */ @Module public class AndroidModule { @Provides @Singleton - Preferences providePreferences() + CommandRunner provideCommandRunner() { - return new Preferences(); + return new CommandRunner(); } @Provides @Singleton - CommandRunner provideCommandRunner() + HabitCardListCache provideHabitCardListCache() { - return new CommandRunner(); + return new HabitCardListCache(); } @Provides @Singleton - HabitCardListCache provideHabitCardListCache() + HabitList provideHabitList() { - return new HabitCardListCache(); + return SQLiteHabitList.getInstance(); + } + + @Provides + ModelFactory provideModelFactory() + { + return new SQLModelFactory(); + } + + @Provides + @Singleton + Preferences providePreferences() + { + return new Preferences(); } } diff --git a/app/src/main/java/org/isoron/uhabits/BaseComponent.java b/app/src/main/java/org/isoron/uhabits/BaseComponent.java index ea1b6cecb..2c6c550ea 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseComponent.java +++ b/app/src/main/java/org/isoron/uhabits/BaseComponent.java @@ -19,16 +19,34 @@ package org.isoron.uhabits; +import org.isoron.uhabits.commands.ArchiveHabitsCommand; +import org.isoron.uhabits.commands.ChangeHabitColorCommand; +import org.isoron.uhabits.commands.CreateHabitCommand; +import org.isoron.uhabits.commands.DeleteHabitsCommand; +import org.isoron.uhabits.commands.EditHabitCommand; +import org.isoron.uhabits.commands.UnarchiveHabitsCommand; +import org.isoron.uhabits.io.AbstractImporter; +import org.isoron.uhabits.io.HabitsCSVExporter; +import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.tasks.ToggleRepetitionTask; +import org.isoron.uhabits.ui.BaseSystem; import org.isoron.uhabits.ui.habits.edit.BaseDialogFragment; +import org.isoron.uhabits.ui.habits.edit.HistoryEditorDialog; +import org.isoron.uhabits.ui.habits.list.ListHabitsActivity; +import org.isoron.uhabits.ui.habits.list.ListHabitsController; import org.isoron.uhabits.ui.habits.list.ListHabitsSelectionMenu; +import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController; import org.isoron.uhabits.ui.habits.list.model.HabitCardListAdapter; import org.isoron.uhabits.ui.habits.list.model.HabitCardListCache; -import org.isoron.uhabits.ui.habits.list.ListHabitsController; -import org.isoron.uhabits.ui.habits.list.controllers.CheckmarkButtonController; import org.isoron.uhabits.ui.habits.list.model.HintList; import org.isoron.uhabits.ui.habits.list.views.CheckmarkPanelView; +import org.isoron.uhabits.ui.habits.show.ShowHabitActivity; +import org.isoron.uhabits.widgets.BaseWidgetProvider; +import org.isoron.uhabits.widgets.HabitPickerDialog; +/** + * Base component for dependency injection. + */ public interface BaseComponent { void inject(CheckmarkButtonController checkmarkButtonController); @@ -50,4 +68,36 @@ public interface BaseComponent void inject(HintList hintList); void inject(HabitCardListAdapter habitCardListAdapter); + + void inject(ArchiveHabitsCommand archiveHabitsCommand); + + void inject(ChangeHabitColorCommand changeHabitColorCommand); + + void inject(UnarchiveHabitsCommand unarchiveHabitsCommand); + + void inject(EditHabitCommand editHabitCommand); + + void inject(CreateHabitCommand createHabitCommand); + + void inject(HabitPickerDialog habitPickerDialog); + + void inject(BaseWidgetProvider baseWidgetProvider); + + void inject(ShowHabitActivity showHabitActivity); + + void inject(DeleteHabitsCommand deleteHabitsCommand); + + void inject(ListHabitsActivity listHabitsActivity); + + void inject(BaseSystem baseSystem); + + void inject(HistoryEditorDialog historyEditorDialog); + + void inject(HabitsApplication application); + + void inject(Habit habit); + + void inject(AbstractImporter abstractImporter); + + void inject(HabitsCSVExporter habitsCSVExporter); } diff --git a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java index d992bda62..561290636 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java +++ b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java @@ -40,6 +40,7 @@ import org.isoron.uhabits.commands.CommandRunner; import org.isoron.uhabits.commands.ToggleRepetitionCommand; import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.ui.habits.show.ShowHabitActivity; import org.isoron.uhabits.utils.DateUtils; @@ -50,12 +51,26 @@ import java.util.Date; import javax.inject.Inject; +/** + * The Android BroadacastReceiver for Loop Habit Tracker. + *

+ * Currently, all broadcast messages are received and processed by this class. + */ public class HabitBroadcastReceiver extends BroadcastReceiver { public static final String ACTION_CHECK = "org.isoron.uhabits.ACTION_CHECK"; - public static final String ACTION_DISMISS = "org.isoron.uhabits.ACTION_DISMISS"; - public static final String ACTION_SHOW_REMINDER = "org.isoron.uhabits.ACTION_SHOW_REMINDER"; - public static final String ACTION_SNOOZE = "org.isoron.uhabits.ACTION_SNOOZE"; + + public static final String ACTION_DISMISS = + "org.isoron.uhabits.ACTION_DISMISS"; + + public static final String ACTION_SHOW_REMINDER = + "org.isoron.uhabits.ACTION_SHOW_REMINDER"; + + public static final String ACTION_SNOOZE = + "org.isoron.uhabits.ACTION_SNOOZE"; + + @Inject + HabitList habitList; @Inject CommandRunner commandRunner; @@ -66,6 +81,68 @@ public class HabitBroadcastReceiver extends BroadcastReceiver HabitsApplication.getComponent().inject(this); } + public static PendingIntent buildCheckIntent(Context context, + Habit habit, + Long timestamp) + { + Uri data = habit.getUri(); + Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class); + checkIntent.setData(data); + checkIntent.setAction(ACTION_CHECK); + if (timestamp != null) checkIntent.putExtra("timestamp", timestamp); + return PendingIntent.getBroadcast(context, 0, checkIntent, + PendingIntent.FLAG_ONE_SHOT); + } + + public static PendingIntent buildDismissIntent(Context context) + { + Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class); + deleteIntent.setAction(ACTION_DISMISS); + return PendingIntent.getBroadcast(context, 0, deleteIntent, 0); + } + + public static PendingIntent buildSnoozeIntent(Context context, Habit habit) + { + Uri data = habit.getUri(); + Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class); + snoozeIntent.setData(data); + snoozeIntent.setAction(ACTION_SNOOZE); + return PendingIntent.getBroadcast(context, 0, snoozeIntent, 0); + } + + public static PendingIntent buildViewHabitIntent(Context context, + Habit habit) + { + Intent intent = new Intent(context, ShowHabitActivity.class); + intent.setData( + Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId())); + + return TaskStackBuilder + .create(context.getApplicationContext()) + .addNextIntentWithParentStack(intent) + .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + } + + public static void dismissNotification(Context context, Habit habit) + { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService( + Activity.NOTIFICATION_SERVICE); + + int notificationId = (int) (habit.getId() % Integer.MAX_VALUE); + notificationManager.cancel(notificationId); + } + + public static void sendRefreshBroadcast(Context context) + { + LocalBroadcastManager manager = + LocalBroadcastManager.getInstance(context); + Intent refreshIntent = new Intent(HabitsApplication.ACTION_REFRESH); + manager.sendBroadcast(refreshIntent); + + WidgetManager.updateWidgets(context); + } + @Override public void onReceive(final Context context, Intent intent) { @@ -89,40 +166,23 @@ public class HabitBroadcastReceiver extends BroadcastReceiver break; case Intent.ACTION_BOOT_COMPLETED: - ReminderUtils.createReminderAlarms(context); + ReminderUtils.createReminderAlarms(context, habitList); break; } } - private void createReminderAlarmsDelayed(final Context context) - { - new Handler().postDelayed(() -> ReminderUtils.createReminderAlarms(context), 5000); - } - - private void snoozeHabit(Context context, Intent intent) - { - Uri data = intent.getData(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - long delayMinutes = Long.parseLong(prefs.getString("pref_snooze_interval", "15")); - - long habitId = ContentUris.parseId(data); - Habit habit = Habit.get(habitId); - if(habit != null) - ReminderUtils.createReminderAlarm(context, habit, - new Date().getTime() + delayMinutes * 60 * 1000); - dismissNotification(context, habitId); - } - private void checkHabit(Context context, Intent intent) { Uri data = intent.getData(); - Long timestamp = intent.getLongExtra("timestamp", DateUtils.getStartOfToday()); + Long timestamp = + intent.getLongExtra("timestamp", DateUtils.getStartOfToday()); long habitId = ContentUris.parseId(data); - Habit habit = Habit.get(habitId); - if(habit != null) + Habit habit = habitList.getById(habitId); + if (habit != null) { - ToggleRepetitionCommand command = new ToggleRepetitionCommand(habit, timestamp); + ToggleRepetitionCommand command = + new ToggleRepetitionCommand(habit, timestamp); commandRunner.execute(command, habitId); } @@ -130,36 +190,26 @@ public class HabitBroadcastReceiver extends BroadcastReceiver sendRefreshBroadcast(context); } - public static void sendRefreshBroadcast(Context context) - { - LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context); - Intent refreshIntent = new Intent(HabitsApplication.ACTION_REFRESH); - manager.sendBroadcast(refreshIntent); - - WidgetManager.updateWidgets(context); - } - - private void dismissAllHabits() + private boolean checkWeekday(Intent intent, Habit habit) { + Long timestamp = + intent.getLongExtra("timestamp", DateUtils.getStartOfToday()); - } - - private void dismissNotification(Context context, Long habitId) - { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE); + boolean reminderDays[] = + DateUtils.unpackWeekdayList(habit.getReminderDays()); + int weekday = DateUtils.getWeekday(timestamp); - int notificationId = (int) (habitId % Integer.MAX_VALUE); - notificationManager.cancel(notificationId); + return reminderDays[weekday]; } - private void createNotification(final Context context, final Intent intent) { final Uri data = intent.getData(); - final Habit habit = Habit.get(ContentUris.parseId(data)); - final Long timestamp = intent.getLongExtra("timestamp", DateUtils.getStartOfToday()); - final Long reminderTime = intent.getLongExtra("reminderTime", DateUtils.getStartOfToday()); + final Habit habit = habitList.getById(ContentUris.parseId(data)); + final Long timestamp = + intent.getLongExtra("timestamp", DateUtils.getStartOfToday()); + final Long reminderTime = + intent.getLongExtra("reminderTime", DateUtils.getStartOfToday()); if (habit == null) return; @@ -170,7 +220,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver @Override protected void doInBackground() { - todayValue = habit.checkmarks.getTodayValue(); + todayValue = habit.getCheckmarks().getTodayValue(); } @Override @@ -183,41 +233,46 @@ public class HabitBroadcastReceiver extends BroadcastReceiver Intent contentIntent = new Intent(context, MainActivity.class); contentIntent.setData(data); PendingIntent contentPendingIntent = - PendingIntent.getActivity(context, 0, contentIntent, 0); + PendingIntent.getActivity(context, 0, contentIntent, 0); - PendingIntent dismissPendingIntent = buildDismissIntent(context); - PendingIntent checkIntentPending = buildCheckIntent(context, habit, timestamp); - PendingIntent snoozeIntentPending = buildSnoozeIntent(context, habit); + PendingIntent dismissPendingIntent = + buildDismissIntent(context); + PendingIntent checkIntentPending = + buildCheckIntent(context, habit, timestamp); + PendingIntent snoozeIntentPending = + buildSnoozeIntent(context, habit); Uri ringtoneUri = ReminderUtils.getRingtoneUri(context); NotificationCompat.WearableExtender wearableExtender = - new NotificationCompat.WearableExtender().setBackground( - BitmapFactory.decodeResource(context.getResources(), - R.drawable.stripe)); + new NotificationCompat.WearableExtender().setBackground( + BitmapFactory.decodeResource(context.getResources(), + R.drawable.stripe)); Notification notification = - new NotificationCompat.Builder(context) - .setSmallIcon(R.drawable.ic_notification) - .setContentTitle(habit.name) - .setContentText(habit.description) - .setContentIntent(contentPendingIntent) - .setDeleteIntent(dismissPendingIntent) - .addAction(R.drawable.ic_action_check, - context.getString(R.string.check), checkIntentPending) - .addAction(R.drawable.ic_action_snooze, - context.getString(R.string.snooze), snoozeIntentPending) - .setSound(ringtoneUri) - .extend(wearableExtender) - .setWhen(reminderTime) - .setShowWhen(true) - .build(); + new NotificationCompat.Builder(context) + .setSmallIcon(R.drawable.ic_notification) + .setContentTitle(habit.getName()) + .setContentText(habit.getDescription()) + .setContentIntent(contentPendingIntent) + .setDeleteIntent(dismissPendingIntent) + .addAction(R.drawable.ic_action_check, + context.getString(R.string.check), + checkIntentPending) + .addAction(R.drawable.ic_action_snooze, + context.getString(R.string.snooze), + snoozeIntentPending) + .setSound(ringtoneUri) + .extend(wearableExtender) + .setWhen(reminderTime) + .setShowWhen(true) + .build(); notification.flags |= Notification.FLAG_AUTO_CANCEL; NotificationManager notificationManager = - (NotificationManager) context.getSystemService( - Activity.NOTIFICATION_SERVICE); + (NotificationManager) context.getSystemService( + Activity.NOTIFICATION_SERVICE); int notificationId = (int) (habit.getId() % Integer.MAX_VALUE); notificationManager.notify(notificationId, notification); @@ -227,59 +282,39 @@ public class HabitBroadcastReceiver extends BroadcastReceiver }.execute(); } - public static PendingIntent buildSnoozeIntent(Context context, Habit habit) - { - Uri data = habit.getUri(); - Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class); - snoozeIntent.setData(data); - snoozeIntent.setAction(ACTION_SNOOZE); - return PendingIntent.getBroadcast(context, 0, snoozeIntent, 0); - } - - public static PendingIntent buildCheckIntent(Context context, Habit habit, Long timestamp) - { - Uri data = habit.getUri(); - Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class); - checkIntent.setData(data); - checkIntent.setAction(ACTION_CHECK); - if(timestamp != null) checkIntent.putExtra("timestamp", timestamp); - return PendingIntent.getBroadcast(context, 0, checkIntent, PendingIntent.FLAG_ONE_SHOT); - } - - public static PendingIntent buildDismissIntent(Context context) + private void createReminderAlarmsDelayed(final Context context) { - Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class); - deleteIntent.setAction(ACTION_DISMISS); - return PendingIntent.getBroadcast(context, 0, deleteIntent, 0); + new Handler().postDelayed( + () -> ReminderUtils.createReminderAlarms(context, habitList), 5000); } - public static PendingIntent buildViewHabitIntent(Context context, Habit habit) + private void dismissAllHabits() { - Intent intent = new Intent(context, ShowHabitActivity.class); - intent.setData(Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId())); - return TaskStackBuilder.create(context.getApplicationContext()) - .addNextIntentWithParentStack(intent) - .getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); } - private boolean checkWeekday(Intent intent, Habit habit) + private void dismissNotification(Context context, Long habitId) { - Long timestamp = intent.getLongExtra("timestamp", DateUtils.getStartOfToday()); - - boolean reminderDays[] = DateUtils.unpackWeekdayList(habit.reminderDays); - int weekday = DateUtils.getWeekday(timestamp); + NotificationManager notificationManager = + (NotificationManager) context.getSystemService( + Activity.NOTIFICATION_SERVICE); - return reminderDays[weekday]; + int notificationId = (int) (habitId % Integer.MAX_VALUE); + notificationManager.cancel(notificationId); } - public static void dismissNotification(Context context, Habit habit) + private void snoozeHabit(Context context, Intent intent) { - NotificationManager notificationManager = - (NotificationManager) context.getSystemService( - Activity.NOTIFICATION_SERVICE); + Uri data = intent.getData(); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context); + long delayMinutes = + Long.parseLong(prefs.getString("pref_snooze_interval", "15")); - int notificationId = (int) (habit.getId() % Integer.MAX_VALUE); - notificationManager.cancel(notificationId); + long habitId = ContentUris.parseId(data); + Habit habit = habitList.getById(habitId); + if (habit != null) ReminderUtils.createReminderAlarm(context, habit, + new Date().getTime() + delayMinutes * 60 * 1000); + dismissNotification(context, habitId); } } diff --git a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java index 19706124c..34f72d2fe 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -25,37 +25,53 @@ import android.support.annotation.Nullable; import com.activeandroid.ActiveAndroid; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.utils.DatabaseUtils; import java.io.File; +import javax.inject.Inject; + +/** + * The Android application for Loop Habit Tracker. + */ public class HabitsApplication extends Application { - public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH"; - public static final int RESULT_IMPORT_DATA = 1; + public static final String ACTION_REFRESH = + "org.isoron.uhabits.ACTION_REFRESH"; + + public static final int RESULT_BUG_REPORT = 4; + public static final int RESULT_EXPORT_CSV = 2; + public static final int RESULT_EXPORT_DB = 3; - public static final int RESULT_BUG_REPORT = 4; + + public static final int RESULT_IMPORT_DATA = 1; @Nullable private static HabitsApplication application; + private static BaseComponent component; + @Nullable private static Context context; - private static BaseComponent component; - public static boolean isTestMode() + @Inject + HabitList habitList; + + public static BaseComponent getComponent() { - try - { - if(context != null) - context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest"); - return true; - } - catch (final Exception e) - { - return false; - } + return component; + } + + public HabitList getHabitList() + { + return habitList; + } + + public static void setComponent(BaseComponent component) + { + HabitsApplication.component = component; } @Nullable @@ -70,14 +86,19 @@ public class HabitsApplication extends Application return application; } - public static BaseComponent getComponent() - { - return component; - } - - public static void setComponent(BaseComponent component) + public static boolean isTestMode() { - HabitsApplication.component = component; + try + { + if (context != null) context + .getClassLoader() + .loadClass("org.isoron.uhabits.unit.models.HabitTest"); + return true; + } + catch (final Exception e) + { + return false; + } } @Override @@ -91,9 +112,10 @@ public class HabitsApplication extends Application if (isTestMode()) { File db = DatabaseUtils.getDatabaseFile(); - if(db.exists()) db.delete(); + if (db.exists()) db.delete(); } + component.inject(this); DatabaseUtils.initializeActiveAndroid(); } diff --git a/app/src/main/java/org/isoron/uhabits/HabitsBackupAgent.java b/app/src/main/java/org/isoron/uhabits/HabitsBackupAgent.java index 6e619ef7a..f337949ea 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsBackupAgent.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsBackupAgent.java @@ -23,6 +23,9 @@ import android.app.backup.BackupAgentHelper; import android.app.backup.FileBackupHelper; import android.app.backup.SharedPreferencesBackupHelper; +/** + * An Android BackupAgentHelper customized for this application. + */ public class HabitsBackupAgent extends BackupAgentHelper { @Override diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index 0674a8035..ba7b6da06 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -21,6 +21,9 @@ package org.isoron.uhabits; import org.isoron.uhabits.ui.habits.list.ListHabitsActivity; +/** + * Application that starts upon clicking the launcher icon. + */ public class MainActivity extends ListHabitsActivity { /* 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 25e998b7b..9e3238010 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ArchiveHabitsCommand.java @@ -19,38 +19,52 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.util.List; +import javax.inject.Inject; + +/** + * Command to archive a list of habits. + */ public class ArchiveHabitsCommand extends Command { + @Inject + HabitList habitList; private List habits; public ArchiveHabitsCommand(List habits) { + HabitsApplication.getComponent().inject(this); this.habits = habits; } @Override public void execute() { - Habit.archive(habits); + for(Habit h : habits) h.setArchived(1); + habitList.update(habits); } @Override public void undo() { - Habit.unarchive(habits); + for(Habit h : habits) h.setArchived(0); + habitList.update(habits); } + @Override public Integer getExecuteStringId() { return R.string.toast_habit_archived; } + @Override public Integer getUndoStringId() { return R.string.toast_habit_unarchived; 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 ffcd6dfee..e09ec470b 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ChangeHabitColorCommand.java @@ -19,60 +19,65 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; -import org.isoron.uhabits.utils.DatabaseUtils; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + +/** + * Command to change the color of a list of habits. + */ public class ChangeHabitColorCommand extends Command { + @Inject + HabitList habitList; + List habits; + List originalColors; + Integer newColor; public ChangeHabitColorCommand(List habits, Integer newColor) { + HabitsApplication.getComponent().inject(this); + this.habits = habits; this.newColor = newColor; this.originalColors = new ArrayList<>(habits.size()); - for(Habit h : habits) - originalColors.add(h.color); + for (Habit h : habits) originalColors.add(h.getColor()); } @Override public void execute() { - Habit.setColor(habits, newColor); + for(Habit h : habits) h.setColor(newColor); + habitList.update(habits); } @Override - public void undo() - { - DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command() - { - @Override - public void execute() - { - int k = 0; - for(Habit h : habits) - { - h.color = originalColors.get(k++); - h.save(); - } - } - }); - } - public Integer getExecuteStringId() { return R.string.toast_habit_changed; } + @Override public Integer getUndoStringId() { return R.string.toast_habit_changed; } + + @Override + public void undo() + { + int k = 0; + for (Habit h : habits) h.setColor(originalColors.get(k++)); + habitList.update(habits); + } } diff --git a/app/src/main/java/org/isoron/uhabits/commands/Command.java b/app/src/main/java/org/isoron/uhabits/commands/Command.java index b9427e38a..e319a5095 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/Command.java +++ b/app/src/main/java/org/isoron/uhabits/commands/Command.java @@ -19,12 +19,19 @@ package org.isoron.uhabits.commands; +/** + * A Command represents a desired set of changes that should be performed on the + * models. + *

+ * A command can be executed and undone. Each of these operations also provide + * an string that should be displayed to the user upon their completion. + *

+ * In general, commands should always be executed by a {@link CommandRunner}. + */ public abstract class Command { public abstract void execute(); - public abstract void undo(); - public Integer getExecuteStringId() { return null; @@ -34,4 +41,6 @@ public abstract class Command { return null; } + + public abstract void undo(); } diff --git a/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java b/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java index 035e55087..d1051e744 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CommandRunner.java @@ -26,6 +26,12 @@ import org.isoron.uhabits.tasks.BaseTask; import java.util.LinkedList; +/** + * A CommandRunner executes and undoes commands. + *

+ * CommandRunners also allows objects to subscribe to it, and receive events + * whenever a command is performed. + */ public class CommandRunner { private LinkedList listeners; @@ -71,6 +77,10 @@ public class CommandRunner listeners.remove(l); } + /** + * Interface implemented by objects that want to receive an event whenever a + * command is executed. + */ public interface Listener { void onCommandExecuted(@NonNull Command command, 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 7cc9ad51c..7668e5d2f 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/CreateHabitCommand.java @@ -19,41 +19,48 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; +import javax.inject.Inject; + +/** + * Command to create a habit. + */ public class CreateHabitCommand extends Command { + @Inject + HabitList habitList; + private Habit model; private Long savedId; public CreateHabitCommand(Habit model) { this.model = model; + HabitsApplication.getComponent().inject(this); } @Override public void execute() { - Habit savedHabit = new Habit(model); - if (savedId == null) - { - savedHabit.save(); - savedId = savedHabit.getId(); - } - else - { - savedHabit.save(savedId); - } + Habit savedHabit = new Habit(); + savedHabit.copyFrom(model); + savedHabit.setId(savedId); + + habitList.add(savedHabit); + savedId = savedHabit.getId(); } @Override public void undo() { - Habit habit = Habit.get(savedId); + Habit habit = habitList.getById(savedId); if(habit == null) throw new RuntimeException("Habit not found"); - habit.cascadeDelete(); + habitList.remove(habit); } @Override 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 34e26c50c..0d800cd17 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/DeleteHabitsCommand.java @@ -19,27 +19,36 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.util.List; +import javax.inject.Inject; + +/** + * Command to delete a list of habits. + */ public class DeleteHabitsCommand extends Command { + @Inject + HabitList habitList; + private List habits; public DeleteHabitsCommand(List habits) { this.habits = habits; + HabitsApplication.getComponent().inject(this); } @Override public void execute() { for(Habit h : habits) - h.cascadeDelete(); - - Habit.rebuildOrder(); + habitList.remove(h); } @Override @@ -48,11 +57,13 @@ public class DeleteHabitsCommand extends Command throw new UnsupportedOperationException(); } + @Override public Integer getExecuteStringId() { return R.string.toast_habit_deleted; } + @Override public Integer getUndoStringId() { return R.string.toast_habit_restored; 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 7a7787d6a..90884e119 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/EditHabitCommand.java @@ -19,24 +19,43 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; +import javax.inject.Inject; + +/** + * Command to modify a habit. + */ public class EditHabitCommand extends Command { + @Inject + HabitList habitList; + private Habit original; + private Habit modified; + private long savedId; + private boolean hasIntervalChanged; public EditHabitCommand(Habit original, Habit modified) { + HabitsApplication.getComponent().inject(this); + this.savedId = original.getId(); - this.modified = new Habit(modified); - this.original = new Habit(original); + this.modified = new Habit(); + this.original = new Habit(); + + this.modified.copyFrom(modified); + this.original.copyFrom(original); - hasIntervalChanged = (!this.original.freqDen.equals(this.modified.freqDen) || - !this.original.freqNum.equals(this.modified.freqNum)); + hasIntervalChanged = + (!this.original.getFreqDen().equals(this.modified.getFreqDen()) || + !this.original.getFreqNum().equals(this.modified.getFreqNum())); } @Override @@ -45,6 +64,18 @@ public class EditHabitCommand extends Command copyAttributes(this.modified); } + @Override + public Integer getExecuteStringId() + { + return R.string.toast_habit_changed; + } + + @Override + public Integer getUndoStringId() + { + return R.string.toast_habit_changed_back; + } + @Override public void undo() { @@ -53,11 +84,11 @@ public class EditHabitCommand extends Command private void copyAttributes(Habit model) { - Habit habit = Habit.get(savedId); - if(habit == null) throw new RuntimeException("Habit not found"); + Habit habit = habitList.getById(savedId); + if (habit == null) throw new RuntimeException("Habit not found"); - habit.copyAttributes(model); - habit.save(); + habit.copyFrom(model); + habitList.update(habit); invalidateIfNeeded(habit); } @@ -66,19 +97,9 @@ public class EditHabitCommand extends Command { if (hasIntervalChanged) { - habit.checkmarks.deleteNewerThan(0); - habit.streaks.deleteNewerThan(0); - habit.scores.invalidateNewerThan(0); + habit.getCheckmarks().invalidateNewerThan(0); + habit.getStreaks().invalidateNewerThan(0); + habit.getScores().invalidateNewerThan(0); } } - - public Integer getExecuteStringId() - { - return R.string.toast_habit_changed; - } - - public Integer getUndoStringId() - { - return R.string.toast_habit_changed_back; - } } \ No newline at end of file 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 451908433..8a3e981f8 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/ToggleRepetitionCommand.java @@ -21,6 +21,9 @@ package org.isoron.uhabits.commands; import org.isoron.uhabits.models.Habit; +/** + * Command to toggle a repetition. + */ public class ToggleRepetitionCommand extends Command { private Long offset; @@ -35,7 +38,7 @@ public class ToggleRepetitionCommand extends Command @Override public void execute() { - habit.repetitions.toggle(offset); + habit.getRepetitions().toggleTimestamp(offset); } @Override 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 612481fa7..6f06e61eb 100644 --- a/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java +++ b/app/src/main/java/org/isoron/uhabits/commands/UnarchiveHabitsCommand.java @@ -19,38 +19,52 @@ package org.isoron.uhabits.commands; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.util.List; +import javax.inject.Inject; + +/** + * Command to unarchive a list of habits. + */ public class UnarchiveHabitsCommand extends Command { + @Inject + HabitList habitList; private List habits; public UnarchiveHabitsCommand(List habits) { this.habits = habits; + HabitsApplication.getComponent().inject(this); } @Override public void execute() { - Habit.unarchive(habits); + for(Habit h : habits) h.setArchived(0); + habitList.update(habits); } @Override public void undo() { - Habit.archive(habits); + for(Habit h : habits) h.setArchived(1); + habitList.update(habits); } + @Override public Integer getExecuteStringId() { return R.string.toast_habit_unarchived; } + @Override public Integer getUndoStringId() { return R.string.toast_habit_archived; diff --git a/app/src/main/java/org/isoron/uhabits/commands/package-info.java b/app/src/main/java/org/isoron/uhabits/commands/package-info.java new file mode 100644 index 000000000..8fce85ae1 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/commands/package-info.java @@ -0,0 +1,24 @@ +/* + * 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 . + */ + +/** + * Provides commands to modify the models, such as {@link + * org.isoron.uhabits.commands.CreateHabitCommand}. + */ +package org.isoron.uhabits.commands; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java b/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java index 83cfddcb8..3f6da5f8b 100644 --- a/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java @@ -21,13 +21,30 @@ package org.isoron.uhabits.io; import android.support.annotation.NonNull; +import org.isoron.uhabits.HabitsApplication; +import org.isoron.uhabits.models.HabitList; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; +import javax.inject.Inject; + +/** + * AbstractImporter is the base class for all classes that import data from + * files into the app. + */ public abstract class AbstractImporter { + @Inject + HabitList habitList; + + public AbstractImporter() + { + HabitsApplication.getComponent().inject(this); + } + public abstract boolean canHandle(@NonNull File file) throws IOException; public abstract void importHabitsFromFile(@NonNull File file) throws IOException; diff --git a/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java index c08a3a72f..6f8e2ef70 100644 --- a/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java @@ -26,6 +26,10 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; +/** + * A GenericImporter decides which implementation of AbstractImporter is able to + * handle a given file and delegates to it the task of importing the data. + */ public class GenericImporter extends AbstractImporter { List importers; @@ -42,8 +46,8 @@ public class GenericImporter extends AbstractImporter @Override public boolean canHandle(@NonNull File file) throws IOException { - for(AbstractImporter importer : importers) - if(importer.canHandle(file)) return true; + for (AbstractImporter importer : importers) + if (importer.canHandle(file)) return true; return false; } @@ -51,8 +55,7 @@ public class GenericImporter extends AbstractImporter @Override public void importHabitsFromFile(@NonNull File file) throws IOException { - for(AbstractImporter importer : importers) - if(importer.canHandle(file)) - importer.importHabitsFromFile(file); + for (AbstractImporter importer : importers) + if (importer.canHandle(file)) importer.importHabitsFromFile(file); } } diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java index 5a1b6c514..0645fd00d 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java @@ -24,8 +24,8 @@ import android.support.annotation.NonNull; import com.activeandroid.ActiveAndroid; import com.opencsv.CSVReader; -import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.utils.DateUtils; import java.io.BufferedReader; import java.io.File; @@ -34,6 +34,9 @@ import java.io.IOException; import java.util.Calendar; import java.util.HashMap; +/** + * Class that imports data from HabitBull CSV files. + */ public class HabitBullCSVImporter extends AbstractImporter { @Override @@ -89,16 +92,16 @@ public class HabitBullCSVImporter extends AbstractImporter if(h == null) { h = new Habit(); - h.name = name; - h.description = description; - h.freqNum = h.freqDen = 1; - h.save(); - + h.setName(name); + h.setDescription(description); + h.setFreqDen(1); + h.setFreqNum(1); + habitList.add(h); habits.put(name, h); } - if(!h.repetitions.contains(timestamp)) - h.repetitions.toggle(timestamp); + if(!h.getRepetitions().containsTimestamp(timestamp)) + h.getRepetitions().toggleTimestamp(timestamp); } } } diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java index 0a3a80e26..e53102f0a 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java @@ -21,8 +21,10 @@ package org.isoron.uhabits.io; import android.support.annotation.NonNull; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.models.CheckmarkList; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.models.ScoreList; import org.isoron.uhabits.utils.DateUtils; @@ -37,6 +39,11 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; +import javax.inject.Inject; + +/** + * Class that exports the application data to CSV files. + */ public class HabitsCSVExporter { private List habits; @@ -46,8 +53,13 @@ public class HabitsCSVExporter private String exportDirName; + @Inject + HabitList habitList; + public HabitsCSVExporter(List habits, File dir) { + HabitsApplication.getComponent().inject(this); + this.habits = habits; this.exportDirName = dir.getAbsolutePath() + "/"; @@ -61,20 +73,20 @@ public class HabitsCSVExporter new File(exportDirName).mkdirs(); FileWriter out = new FileWriter(exportDirName + filename); generateFilenames.add(filename); - Habit.writeCSV(habits, out); + habitList.writeCSV(out); out.close(); for(Habit h : habits) { - String sane = sanitizeFilename(h.name); - String habitDirName = String.format("%03d %s", h.position + 1, sane); + String sane = sanitizeFilename(h.getName()); + String habitDirName = String.format("%03d %s", habitList.indexOf(h) + 1, sane); habitDirName = habitDirName.trim() + "/"; new File(exportDirName + habitDirName).mkdirs(); generateDirs.add(habitDirName); - writeScores(habitDirName, h.scores); - writeCheckmarks(habitDirName, h.checkmarks); + writeScores(habitDirName, h.getScores()); + writeCheckmarks(habitDirName, h.getCheckmarks()); } } diff --git a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java index 6050a384e..d0037f9e3 100644 --- a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java @@ -31,18 +31,22 @@ import org.isoron.uhabits.utils.FileUtils; import java.io.File; import java.io.IOException; +/** + * Class that imports data from database files exported by Loop Habit Tracker. + */ public class LoopDBImporter extends AbstractImporter { @Override public boolean canHandle(@NonNull File file) throws IOException { - if(!isSQLite3File(file)) return false; + if (!isSQLite3File(file)) return false; SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, - SQLiteDatabase.OPEN_READONLY); + SQLiteDatabase.OPEN_READONLY); - Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?", - new String[]{"Checkmarks", "Repetitions"}); + Cursor c = db.rawQuery( + "select count(*) from SQLITE_MASTER where name=? or name=?", + new String[]{"Checkmarks", "Repetitions"}); boolean result = (c.moveToFirst() && c.getInt(0) == 2); diff --git a/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java index a68d6c392..82d36282d 100644 --- a/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java @@ -23,14 +23,17 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; +import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.utils.DatabaseUtils; import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.models.Habit; import java.io.File; import java.io.IOException; import java.util.GregorianCalendar; +/** + * Class that imports database files exported by Rewire. + */ public class RewireDBImporter extends AbstractImporter { @Override @@ -57,7 +60,7 @@ public class RewireDBImporter extends AbstractImporter final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, SQLiteDatabase.OPEN_READONLY); - DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command() + DatabaseUtils.executeAsTransaction(new DatabaseUtils.Callback() { @Override public void execute() @@ -91,30 +94,30 @@ public class RewireDBImporter extends AbstractImporter int periodIndex = c.getInt(7); Habit habit = new Habit(); - habit.name = name; - habit.description = description; + habit.setName(name); + habit.setDescription(description); int periods[] = { 7, 31, 365 }; switch (schedule) { case 0: - habit.freqNum = activeDays.split(",").length; - habit.freqDen = 7; + habit.setFreqNum(activeDays.split(",").length); + habit.setFreqDen(7); break; case 1: - habit.freqNum = days; - habit.freqDen = periods[periodIndex]; + habit.setFreqNum(days); + habit.setFreqDen(periods[periodIndex]); break; case 2: - habit.freqNum = 1; - habit.freqDen = repeatingCount; + habit.setFreqNum(1); + habit.setFreqDen(repeatingCount); break; } - habit.save(); + habitList.add(habit); createReminder(db, habit, id); createCheckmarks(db, habit, id); @@ -150,10 +153,10 @@ public class RewireDBImporter extends AbstractImporter reminderDays[idx] = true; } - habit.reminderDays = DateUtils.packWeekdayList(reminderDays); - habit.reminderHour = rewireReminder / 60; - habit.reminderMin = rewireReminder % 60; - habit.save(); + habit.setReminderDays(DateUtils.packWeekdayList(reminderDays)); + habit.setReminderHour(rewireReminder / 60); + habit.setReminderMin(rewireReminder % 60); + habitList.update(habit); } finally { @@ -161,7 +164,8 @@ public class RewireDBImporter extends AbstractImporter } } - private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int rewireHabitId) + private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull + Habit habit, int rewireHabitId) { Cursor c = null; @@ -181,7 +185,7 @@ public class RewireDBImporter extends AbstractImporter GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); cal.set(year, month - 1, day); - habit.repetitions.toggle(cal.getTimeInMillis()); + habit.getRepetitions().toggleTimestamp(cal.getTimeInMillis()); } while (c.moveToNext()); } diff --git a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java index 766af577a..c47c5580a 100644 --- a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java @@ -23,26 +23,30 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; +import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.utils.DatabaseUtils; import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.models.Habit; import java.io.File; import java.io.IOException; import java.util.GregorianCalendar; +/** + * Class that imports data from database files exported by Tickmate. + */ public class TickmateDBImporter extends AbstractImporter { @Override public boolean canHandle(@NonNull File file) throws IOException { - if(!isSQLite3File(file)) return false; + if (!isSQLite3File(file)) return false; SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, - SQLiteDatabase.OPEN_READONLY); + SQLiteDatabase.OPEN_READONLY); - Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?", - new String[]{"tracks", "track2groups"}); + Cursor c = db.rawQuery( + "select count(*) from SQLITE_MASTER where name=? or name=?", + new String[]{"tracks", "track2groups"}); boolean result = (c.moveToFirst() && c.getInt(0) == 2); @@ -54,47 +58,39 @@ public class TickmateDBImporter extends AbstractImporter @Override public void importHabitsFromFile(@NonNull File file) throws IOException { - final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, + final SQLiteDatabase db = + SQLiteDatabase.openDatabase(file.getPath(), null, SQLiteDatabase.OPEN_READONLY); - DatabaseUtils.executeAsTransaction(new DatabaseUtils.Command() - { - @Override - public void execute() - { - createHabits(db); - } - }); - + DatabaseUtils.executeAsTransaction(() -> createHabits(db)); db.close(); } - private void createHabits(SQLiteDatabase db) + private void createCheckmarks(@NonNull SQLiteDatabase db, + @NonNull Habit habit, + int tickmateTrackId) { Cursor c = null; try { - c = db.rawQuery("select _id, name, description from tracks", new String[0]); + String[] params = {Integer.toString(tickmateTrackId)}; + c = db.rawQuery( + "select distinct year, month, day from ticks where _track_id=?", + params); if (!c.moveToFirst()) return; do { - int id = c.getInt(0); - String name = c.getString(1); - String description = c.getString(2); - - Habit habit = new Habit(); - habit.name = name; - habit.description = description; - habit.freqNum = 1; - habit.freqDen = 1; - habit.save(); + int year = c.getInt(0); + int month = c.getInt(1); + int day = c.getInt(2); - createCheckmarks(db, habit, id); + GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); + cal.set(year, month, day); - } - while (c.moveToNext()); + habit.getRepetitions().toggleTimestamp(cal.getTimeInMillis()); + } while (c.moveToNext()); } finally { @@ -102,28 +98,32 @@ public class TickmateDBImporter extends AbstractImporter } } - private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int tickmateTrackId) + private void createHabits(SQLiteDatabase db) { Cursor c = null; try { - String[] params = { Integer.toString(tickmateTrackId) }; - c = db.rawQuery("select distinct year, month, day from ticks where _track_id=?", params); + c = db.rawQuery("select _id, name, description from tracks", + new String[0]); if (!c.moveToFirst()) return; do { - int year = c.getInt(0); - int month = c.getInt(1); - int day = c.getInt(2); + int id = c.getInt(0); + String name = c.getString(1); + String description = c.getString(2); - GregorianCalendar cal = DateUtils.getStartOfTodayCalendar(); - cal.set(year, month, day); + Habit habit = new Habit(); + habit.setName(name); + habit.setDescription(description); + habit.setFreqNum(1); + habit.setFreqDen(1); + habitList.add(habit); + + createCheckmarks(db, habit, id); - habit.repetitions.toggle(cal.getTimeInMillis()); - } - while (c.moveToNext()); + } while (c.moveToNext()); } finally { diff --git a/app/src/main/java/org/isoron/uhabits/io/package-info.java b/app/src/main/java/org/isoron/uhabits/io/package-info.java new file mode 100644 index 000000000..5cbd932fb --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +/** + * Provides classes that deal with importing from and exporting to files. + */ +package org.isoron.uhabits.io; \ No newline at end of file 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 a6c5ec06f..775b1b032 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Checkmark.java +++ b/app/src/main/java/org/isoron/uhabits/models/Checkmark.java @@ -19,48 +19,65 @@ package org.isoron.uhabits.models; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; -import com.activeandroid.annotation.Table; +import org.apache.commons.lang3.builder.ToStringBuilder; -@Table(name = "Checkmarks") -public class Checkmark extends Model +/** + * A Checkmark represents the completion status of the habit for a given day. + *

+ * While repetitions simply record that the habit was performed at a given date, + * a checkmark provides more information, such as whether a repetition was + * expected at that day or not. + *

+ * Checkmarks are computed automatically from the list of repetitions. + */ +public class Checkmark { /** - * Indicates that there was no repetition at the timestamp, even though a repetition was - * expected. + * Indicates that there was a repetition at the timestamp. */ - public static final int UNCHECKED = 0; + public static final int CHECKED_EXPLICITLY = 2; /** - * Indicates that there was no repetition at the timestamp, but one was not expected in any - * case, due to the frequency of the habit. + * 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. + * Indicates that there was no repetition at the timestamp, even though a + * repetition was expected. */ - public static final int CHECKED_EXPLICITLY = 2; + public static final int UNCHECKED = 0; - /** - * The habit to which this checkmark belongs. - */ - @Column(name = "habit") - public Habit habit; + final 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; + final long timestamp; - /** - * Indicates whether there is a repetition at the given timestamp or not, and whether the - * repetition was expected. Assumes one of the values UNCHECKED, CHECKED_EXPLICITLY or - * CHECKED_IMPLICITLY. - */ - @Column(name = "value") - public Integer value; + final int value; + + public Checkmark(Habit habit, long timestamp, int value) + { + this.habit = habit; + this.timestamp = timestamp; + this.value = value; + } + + public long getTimestamp() + { + return timestamp; + } + + public int getValue() + { + return value; + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("timestamp", timestamp) + .append("value", value) + .toString(); + } } 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 c0306cdb3..054aa35b3 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -19,18 +19,10 @@ package org.isoron.uhabits.models; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteStatement; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.activeandroid.Cache; -import com.activeandroid.query.Delete; -import com.activeandroid.query.Select; - import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.utils.InterfaceUtils; import java.io.IOException; import java.io.Writer; @@ -38,9 +30,13 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; -public class CheckmarkList +/** + * The collection of {@link Checkmark}s belonging to a habit. + */ +public abstract class CheckmarkList { - private Habit habit; + protected Habit habit; + public ModelObservable observable = new ModelObservable(); public CheckmarkList(Habit habit) @@ -49,125 +45,123 @@ public class CheckmarkList } /** - * Deletes every checkmark that has timestamp either equal or newer than a given timestamp. - * These checkmarks will be recomputed at the next time they are queried. + * 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. * - * @param timestamp the timestamp + * @return values for the checkmarks in the interval */ - public void deleteNewerThan(long timestamp) + @NonNull + public int[] getAllValues() { - new Delete().from(Checkmark.class) - .where("habit = ?", habit.getId()) - .and("timestamp >= ?", timestamp) - .execute(); + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep == null) return new int[0]; - observable.notifyListeners(); + Long fromTimestamp = oldestRep.getTimestamp(); + Long toTimestamp = DateUtils.getStartOfToday(); + + return getValues(fromTimestamp, toTimestamp); } /** - * 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. + * Returns the checkmark for today. * - * @param fromTimestamp timestamp for the oldest checkmark - * @param toTimestamp timestamp for the newest checkmark - * @return values for the checkmarks inside the given interval + * @return checkmark for today */ - @NonNull - public int[] getValues(long fromTimestamp, long toTimestamp) + @Nullable + public Checkmark getToday() { - compute(fromTimestamp, toTimestamp); - - if(fromTimestamp > toTimestamp) return new int[0]; - - String query = "select value, timestamp from Checkmarks where " + - "habit = ? and timestamp >= ? and timestamp <= ?"; - - SQLiteDatabase db = Cache.openDatabase(); - String args[] = { habit.getId().toString(), Long.toString(fromTimestamp), - Long.toString(toTimestamp) }; - Cursor cursor = db.rawQuery(query, args); - - long day = DateUtils.millisecondsInOneDay; - int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1; - int[] checks = new int[nDays]; - - if (cursor.moveToFirst()) - { - do - { - long timestamp = cursor.getLong(1); - int offset = (int) ((timestamp - fromTimestamp) / day); - checks[nDays - offset - 1] = cursor.getInt(0); - - } while (cursor.moveToNext()); - } - - cursor.close(); - return checks; + long today = DateUtils.getStartOfToday(); + compute(today, today); + return getNewest(); } /** - * Returns the values for all the checkmarks, since the oldest repetition of the habit until - * today. If there are no repetitions at all, returns an empty array. - * - * The values are returned in an array containing one integer value for each day since the - * first repetition of the habit until today. The first entry corresponds to today, the second - * entry corresponds to yesterday, and so on. + * Returns the value of today's checkmark. * - * @return values for the checkmarks in the interval + * @return value of today's checkmark */ - @NonNull - public int[] getAllValues() + public int getTodayValue() { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return new int[0]; + Checkmark today = getToday(); + if (today != null) return today.getValue(); + else return Checkmark.UNCHECKED; + } - Long fromTimestamp = oldestRep.timestamp; - Long toTimestamp = DateUtils.getStartOfToday(); + /** + * 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 from timestamp for the oldest checkmark + * @param to timestamp for the newest checkmark + * @return values for the checkmarks inside the given interval + */ + public abstract int[] getValues(long from, long to); - return getValues(fromTimestamp, toTimestamp); - } + /** + * Marks as invalid every checkmark that has timestamp either equal or newer + * than a given timestamp. These checkmarks will be recomputed at the next + * time they are queried. + * + * @param timestamp the timestamp + */ + public abstract void invalidateNewerThan(long timestamp); /** - * Computes and stores one checkmark for each day, since the first repetition until today. - * Days that already have a corresponding checkmark are skipped. + * Writes the entire list of checkmarks to the given writer, in CSV format. + * There is one line for each checkmark. Each line contains two fields: + * timestamp and value. + * + * @param out the writer where the CSV will be output + * @throws IOException in case write operations fail */ - protected void computeAll() + public void writeCSV(Writer out) throws IOException { - long fromTimestamp = habit.repetitions.getOldestTimestamp(); - if(fromTimestamp == 0) return; + computeAll(); - Long toTimestamp = DateUtils.getStartOfToday(); + int values[] = getAllValues(); + long timestamp = DateUtils.getStartOfToday(); + SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); - compute(fromTimestamp, toTimestamp); + for (int value : values) + { + String date = dateFormat.format(new Date(timestamp)); + out.write(String.format("%s,%d\n", date, value)); + timestamp -= DateUtils.millisecondsInOneDay; + } } /** - * Computes and stores one checkmark for each day that falls inside the specified interval of - * time. Days that already have a corresponding checkmark are skipped. + * 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 + * @param to timestamp for the end of the interval */ protected void compute(long from, final long to) { - InterfaceUtils.throwIfMainThread(); - final long day = DateUtils.millisecondsInOneDay; - Checkmark newestCheckmark = findNewest(); - if(newestCheckmark != null) from = newestCheckmark.timestamp + day; + Checkmark newestCheckmark = getNewest(); + if (newestCheckmark != null) + from = newestCheckmark.getTimestamp() + day; - if(from > to) return; + if (from > to) return; - long fromExtended = from - (long) (habit.freqDen) * day; - List reps = habit.repetitions - .selectFromTo(fromExtended, to) - .execute(); + long fromExtended = from - (long) (habit.getFreqDen()) * day; + List reps = + habit.getRepetitions().getByInterval(fromExtended, to); final int nDays = (int) ((to - from) / day) + 1; int nDaysExtended = (int) ((to - fromExtended) / day) + 1; @@ -175,7 +169,7 @@ public class CheckmarkList for (Repetition rep : reps) { - int offset = (int) ((rep.timestamp - fromExtended) / day); + int offset = (int) ((rep.getTimestamp() - fromExtended) / day); checks[nDaysExtended - offset - 1] = Checkmark.CHECKED_EXPLICITLY; } @@ -183,11 +177,11 @@ public class CheckmarkList { int counter = 0; - for (int j = 0; j < habit.freqDen; j++) + for (int j = 0; j < habit.getFreqDen(); j++) if (checks[i + j] == 2) counter++; - if (counter >= habit.freqNum) - if(checks[i] != Checkmark.CHECKED_EXPLICITLY) + if (counter >= habit.getFreqNum()) + if (checks[i] != Checkmark.CHECKED_EXPLICITLY) checks[i] = Checkmark.CHECKED_IMPLICITLY; } @@ -199,106 +193,29 @@ public class CheckmarkList insert(timestamps, checks); } - private void insert(long timestamps[], int values[]) - { - String query = "insert into Checkmarks(habit, timestamp, value) values (?,?,?)"; - - SQLiteDatabase db = Cache.openDatabase(); - db.beginTransaction(); - - try - { - SQLiteStatement statement = db.compileStatement(query); - - for (int i = 0; i < timestamps.length; i++) - { - statement.bindLong(1, habit.getId()); - statement.bindLong(2, timestamps[i]); - statement.bindLong(3, values[i]); - statement.execute(); - } - - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } - } - /** - * Returns newest checkmark that has already been computed. Ignores any checkmark that has - * timestamp in the future. This does not update the cache. - * - * @return newest checkmark already computed + * Computes and stores one checkmark for each day, since the first + * repetition until today. Days that already have a corresponding checkmark + * are skipped. */ - @Nullable - protected Checkmark findNewest() + protected void computeAll() { - return new Select().from(Checkmark.class) - .where("habit = ?", habit.getId()) - .and("timestamp <= ?", DateUtils.getStartOfToday()) - .orderBy("timestamp desc") - .limit(1) - .executeSingle(); - } + Repetition oldest = habit.getRepetitions().getOldest(); + if (oldest == null) return; - /** - * Returns the checkmark for today. - * - * @return checkmark for today - */ - @Nullable - public Checkmark getToday() - { - long today = DateUtils.getStartOfToday(); - compute(today, today); - return findNewest(); - } + Long today = DateUtils.getStartOfToday(); - /** - * Returns the value of today's checkmark. - * - * @return value of today's checkmark - */ - public int getTodayValue() - { - Checkmark today = getToday(); - if(today != null) return today.value; - else return Checkmark.UNCHECKED; + compute(oldest.getTimestamp(), today); } /** - * Writes the entire list of checkmarks to the given writer, in CSV format. There is one - * line for each checkmark. Each line contains two fields: timestamp and value. + * Returns newest checkmark that has already been computed. Ignores any + * checkmark that has timestamp in the future. This does not update the + * cache. * - * @param out the writer where the CSV will be output - * @throws IOException in case write operations fail + * @return newest checkmark already computed */ + protected abstract Checkmark getNewest(); - public void writeCSV(Writer out) throws IOException - { - computeAll(); - - SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); - - String query = "select timestamp, value from checkmarks where habit = ? order by timestamp"; - String params[] = { habit.getId().toString() }; - - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); - - if(!cursor.moveToFirst()) return; - - do - { - String timestamp = dateFormat.format(new Date(cursor.getLong(0))); - Integer value = cursor.getInt(1); - out.write(String.format("%s,%d\n", timestamp, value)); - - } while(cursor.moveToNext()); - - cursor.close(); - out.close(); - } + protected abstract void insert(long timestamps[], int values[]); } 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 3d4066823..aeaf152b1 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -19,135 +19,75 @@ package org.isoron.uhabits.models; -import android.annotation.SuppressLint; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.activeandroid.ActiveAndroid; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; -import com.activeandroid.annotation.Table; -import com.activeandroid.query.Delete; -import com.activeandroid.query.From; -import com.activeandroid.query.Select; -import com.activeandroid.query.Update; -import com.activeandroid.util.SQLiteUtils; -import com.opencsv.CSVWriter; - -import org.isoron.uhabits.utils.ColorUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.utils.DateUtils; -import java.io.IOException; -import java.io.Writer; -import java.util.List; import java.util.Locale; -@Table(name = "Habits") -public class Habit extends Model +import javax.inject.Inject; + +/** + * The thing that the user wants to track. + */ +public class Habit { - /** - * Name of the habit - */ - @Column(name = "name") - public String name; + public static final String HABIT_URI_FORMAT = + "content://org.isoron.uhabits/habit/%d"; - /** - * Description of the habit - */ - @Column(name = "description") - public String description; + @Nullable + private Long id; - /** - * Frequency numerator. If a habit is performed 3 times in 7 days, this field equals 3. - */ - @Column(name = "freq_num") - public Integer freqNum; + @NonNull + private String name; - /** - * Frequency denominator. If a habit is performed 3 times in 7 days, this field equals 7. - */ - @Column(name = "freq_den") - public Integer freqDen; + @NonNull + private String description; - /** - * Color of the habit. - * - * This number is not an android.graphics.Color, but an index to the activity color palette, - * which changes according to the theme. To convert this color into an android.graphics.Color, - * use ColorHelper.getColor(context, habit.color). - */ - @Column(name = "color") - public Integer color; + @NonNull + private Integer freqNum; - /** - * Position of the habit. Habits are usually sorted by this field. - */ - @Column(name = "position") - public Integer position; + @NonNull + private Integer freqDen; + + @NonNull + private Integer color; - /** - * Hour of the day the reminder should be shown. If there is no reminder, this equals to null. - */ @Nullable - @Column(name = "reminder_hour") - public Integer reminderHour; + private 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; + private Integer reminderMin; - /** - * Days of the week the reminder should be shown. This field can be converted to a list of - * booleans using the method DateHelper.unpackWeekdayList and converted back to an integer by - * using the method DateHelper.packWeekdayList. If the habit has no reminders, this value - * should be ignored. - */ @NonNull - @Column(name = "reminder_days") - public Integer reminderDays; + private Integer reminderDays; - /** - * Not currently used. - */ - @Column(name = "highlight") - public Integer highlight; + @NonNull + private 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; + @NonNull + private Integer archived; - /** - * List of streaks belonging to this habit. - */ @NonNull - public StreakList streaks; + private StreakList streaks; - /** - * List of scores belonging to this habit. - */ @NonNull - public ScoreList scores; + private ScoreList scores; - /** - * List of repetitions belonging to this habit. - */ @NonNull - public RepetitionList repetitions; + private RepetitionList repetitions; - /** - * List of checkmarks belonging to this habit. - */ @NonNull - public CheckmarkList checkmarks; + private CheckmarkList checkmarks; + + private ModelObservable observable = new ModelObservable(); - public ModelObservable observable = new ModelObservable(); + @Inject + ModelFactory factory; /** * Constructs a habit with the same attributes as the specified habit. @@ -156,320 +96,281 @@ public class Habit extends Model */ public Habit(Habit model) { + HabitsApplication.getComponent().inject(this); + reminderDays = DateUtils.ALL_WEEK_DAYS; - copyAttributes(model); + copyFrom(model); - checkmarks = new CheckmarkList(this); - streaks = new StreakList(this); - scores = new ScoreList(this); - repetitions = new RepetitionList(this); + checkmarks = factory.buildCheckmarkList(this); + streaks = factory.buildStreakList(this); + scores = factory.buildScoreList(this); + repetitions = factory.buidRepetitionList(this); } /** - * Constructs a habit with default attributes. The habit is not archived, not highlighted, has - * no reminders and is placed in the last position of the list of habits. + * Constructs a habit with default attributes. + *

+ * The habit is not archived, not highlighted, has no reminders and is + * placed in the last position of the list of habits. */ public Habit() { + HabitsApplication.getComponent().inject(this); + this.color = 5; - this.position = Habit.countWithArchived(); this.highlight = 0; this.archived = 0; this.freqDen = 7; this.freqNum = 3; this.reminderDays = DateUtils.ALL_WEEK_DAYS; - checkmarks = new CheckmarkList(this); - streaks = new StreakList(this); - scores = new ScoreList(this); - repetitions = new RepetitionList(this); + checkmarks = factory.buildCheckmarkList(this); + streaks = factory.buildStreakList(this); + scores = factory.buildScoreList(this); + repetitions = factory.buidRepetitionList(this); } /** - * Returns the habit with specified id. - * - * @param id the id of the habit - * @return the habit, or null if none exist + * Clears the reminder for a habit. This sets all the related fields to + * null. */ - @Nullable - public static Habit get(long id) + public void clearReminder() { - return Habit.load(Habit.class, id); + reminderHour = null; + reminderMin = null; + reminderDays = DateUtils.ALL_WEEK_DAYS; + observable.notifyListeners(); } /** - * Returns a list of all habits, optionally including archived habits. + * Copies all the attributes of the specified habit into this habit * - * @param includeArchive whether archived habits should be included the list - * @return list of all habits + * @param model the model whose attributes should be copied from */ - @NonNull - public static List getAll(boolean includeArchive) + public void copyFrom(@NonNull Habit model) { - if(includeArchive) return selectWithArchived().execute(); - else return select().execute(); + this.name = model.getName(); + this.description = model.getDescription(); + this.freqNum = model.getFreqNum(); + this.freqDen = model.getFreqDen(); + this.color = model.getColor(); + this.reminderHour = model.getReminderHour(); + this.reminderMin = model.getReminderMin(); + this.reminderDays = model.getReminderDays(); + this.highlight = model.getHighlight(); + this.archived = model.getArchived(); + observable.notifyListeners(); } /** - * 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 + * Flag that indicates whether the habit is archived. Archived habits are + * usually omitted from listings, unless explicitly included. */ - @Nullable - public static Habit getByPosition(int position) + public Integer getArchived() { - return selectWithArchived().where("position = ?", position).executeSingle(); + return archived; } /** - * Changes the id of a habit on the database. - * - * @param oldId the original id - * @param newId the new id + * List of checkmarks belonging to this habit. */ - @SuppressLint("DefaultLocale") - public static void updateId(long oldId, long newId) + @NonNull + public CheckmarkList getCheckmarks() { - SQLiteUtils.execSql(String.format("update Habits set Id = %d where Id = %d", newId, oldId)); + return checkmarks; } - @NonNull - protected static From select() + /** + * Color of the habit. + *

+ * This number is not an android.graphics.Color, but an index to the + * activity color palette, which changes according to the theme. To convert + * this color into an android.graphics.Color, use ColorHelper.getColor(context, + * habit.color). + */ + public Integer getColor() { - return new Select().from(Habit.class).where("archived = 0").orderBy("position"); + return color; } - @NonNull - protected static From selectWithArchived() + public void setColor(Integer color) { - return new Select().from(Habit.class).orderBy("position"); + this.color = color; } /** - * Returns the total number of unarchived habits. - * - * @return number of unarchived habits + * Description of the habit */ - public static int count() + public String getDescription() { - return select().count(); + return description; } - /** - * Returns the total number of habits, including archived habits. - * - * @return number of habits, including archived - */ - public static int countWithArchived() + public void setDescription(String description) { - return selectWithArchived().count(); + this.description = description; } /** - * Returns a list the habits that have a reminder. Does not include archived habits. - * - * @return list of habits with reminder + * Frequency denominator. If a habit is performed 3 times in 7 days, this + * field equals 7. */ - @NonNull - public static List getHabitsWithReminder() + public Integer getFreqDen() + { + return freqDen; + } + + public void setFreqDen(Integer freqDen) { - return select().where("reminder_hour is not null").execute(); + this.freqDen = freqDen; } /** - * 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 + * Frequency numerator. If a habit is performed 3 times in 7 days, this + * field equals 3. */ - public static void reorder(Habit from, Habit to) + public Integer getFreqNum() { - if(from == to) return; - - if (to.position < from.position) - { - new Update(Habit.class).set("position = position + 1") - .where("position >= ? and position < ?", to.position, from.position) - .execute(); - } - else - { - new Update(Habit.class).set("position = position - 1") - .where("position > ? and position <= ?", from.position, to.position) - .execute(); - } + return freqNum; + } - from.position = to.position; - from.save(); + public void setFreqNum(Integer freqNum) + { + this.freqNum = freqNum; } /** - * Recomputes the position for every habit in the database. It should never be necessary - * to call this method. + * Not currently used. */ - public static void rebuildOrder() + public Integer getHighlight() { - List habits = selectWithArchived().execute(); + return highlight; + } - ActiveAndroid.beginTransaction(); - try - { - int i = 0; - for (Habit h : habits) - { - h.position = i++; - h.save(); - } + public void setHighlight(Integer highlight) + { + this.highlight = highlight; + } - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - } + public Long getId() + { + return id; + } + public void setId(Long id) + { + this.id = id; } /** - * Copies all the attributes of the specified habit into this habit - * - * @param model the model whose attributes should be copied from + * Name of the habit */ - public void copyAttributes(@NonNull Habit model) - { - this.name = model.name; - this.description = model.description; - this.freqNum = model.freqNum; - this.freqDen = model.freqDen; - this.color = model.color; - this.position = model.position; - this.reminderHour = model.reminderHour; - this.reminderMin = model.reminderMin; - this.reminderDays = model.reminderDays; - this.highlight = model.highlight; - this.archived = model.archived; + public String getName() + { + return name; + } - observable.notifyListeners(); + public void setName(String name) + { + this.name = name; } - /** - * 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) + public ModelObservable getObservable() { - save(); - Habit.updateId(getId(), id); + return observable; } /** - * Deletes the habit and all data associated to it, including checkmarks, repetitions and - * scores. + * Days of the week the reminder should be shown. This field can be + * converted to a list of booleans using the method DateHelper.unpackWeekdayList + * and converted back to an integer by using the method + * DateHelper.packWeekdayList. If the habit has no reminders, this value + * should be ignored. */ - public void cascadeDelete() + @NonNull + public Integer getReminderDays() { - Long id = getId(); - - ActiveAndroid.beginTransaction(); - try - { - new Delete().from(Checkmark.class).where("habit = ?", id).execute(); - new Delete().from(Repetition.class).where("habit = ?", id).execute(); - new Delete().from(Score.class).where("habit = ?", id).execute(); - new Delete().from(Streak.class).where("habit = ?", id).execute(); - delete(); + return reminderDays; + } - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - } + public void setReminderDays(@NonNull Integer reminderDays) + { + this.reminderDays = reminderDays; } /** - * Returns the public URI that identifies this habit - * @return the uri + * Hour of the day the reminder should be shown. If there is no reminder, + * this equals to null. */ - public Uri getUri() + @Nullable + public Integer getReminderHour() { - String s = String.format(Locale.US, "content://org.isoron.uhabits/habit/%d", getId()); - return Uri.parse(s); + return reminderHour; + } + + public void setReminderHour(@Nullable Integer reminderHour) + { + this.reminderHour = reminderHour; } /** - * Returns whether the habit is archived or not. - * @return true if archived + * Minute the reminder should be shown. If there is no reminder, this equals + * to null. */ - public boolean isArchived() + @Nullable + public Integer getReminderMin() { - return archived != 0; + return reminderMin; } - private static void updateAttributes(@NonNull List habits, @Nullable Integer color, - @Nullable Integer archived) + public void setReminderMin(@Nullable Integer reminderMin) { - ActiveAndroid.beginTransaction(); - - try - { - for (Habit h : habits) - { - if(color != null) h.color = color; - if(archived != null) h.archived = archived; - h.save(); - } + this.reminderMin = reminderMin; + } - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - for(Habit h : habits) - h.observable.notifyListeners(); - } + /** + * List of repetitions belonging to this habit. + */ + @NonNull + public RepetitionList getRepetitions() + { + return repetitions; } /** - * Archives an entire list of habits - * - * @param habits the habits to be archived + * List of scores belonging to this habit. */ - public static void archive(@NonNull List habits) + @NonNull + public ScoreList getScores() { - updateAttributes(habits, null, 1); + return scores; } /** - * Unarchives an entire list of habits - * - * @param habits the habits to be unarchived + * List of streaks belonging to this habit. */ - public static void unarchive(@NonNull List habits) + @NonNull + public StreakList getStreaks() { - updateAttributes(habits, null, 0); + return streaks; } /** - * Sets the color for an entire list of habits. + * Returns the public URI that identifies this habit * - * @param habits the habits to be modified - * @param color the new color to be set + * @return the uri */ - public static void setColor(@NonNull List habits, int color) + public Uri getUri() { - updateAttributes(habits, color, null); - for(Habit h : habits) - h.observable.notifyListeners(); + String s = String.format(Locale.US, HABIT_URI_FORMAT, getId()); + return Uri.parse(s); } /** * Checks whether the habit has a reminder set. * - * @return true if habit has reminder + * @return true if habit has reminder, false otherwise */ public boolean hasReminder() { @@ -477,47 +378,35 @@ public class Habit extends Model } /** - * Clears the reminder for a habit. This sets all the related fields to null. + * Returns whether the habit is archived or not. + * + * @return true if archived */ - public void clearReminder() + public boolean isArchived() { - reminderHour = null; - reminderMin = null; - reminderDays = DateUtils.ALL_WEEK_DAYS; - observable.notifyListeners(); + return archived != 0; } - /** - * Writes the list of habits to the given writer, in CSV format. There is one line for each - * habit, containing the fields name, description, frequency numerator, frequency denominator - * and color. The color is written in HTML format (#000000). - * - * @param habits the list of habits to write - * @param out the writer that will receive the result - * @throws IOException if write operations fail - */ - public static void writeCSV(List habits, Writer out) throws IOException + public void setArchived(Integer archived) { - String header[] = { "Position", "Name", "Description", "NumRepetitions", "Interval", "Color" }; - - CSVWriter csv = new CSVWriter(out); - csv.writeNext(header, false); - - for(Habit habit : habits) - { - String[] cols = - { - String.format("%03d", habit.position + 1), - habit.name, - habit.description, - Integer.toString(habit.freqNum), - Integer.toString(habit.freqDen), - ColorUtils.toHTML(ColorUtils.CSV_PALETTE[habit.color]) - }; - - csv.writeNext(cols, false); - } + this.archived = archived; + } - csv.close(); + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("id", id) + .append("name", name) + .append("description", description) + .append("freqNum", freqNum) + .append("freqDen", freqDen) + .append("color", color) + .append("reminderHour", reminderHour) + .append("reminderMin", reminderMin) + .append("reminderDays", reminderDays) + .append("highlight", highlight) + .append("archived", archived) + .toString(); } } diff --git a/app/src/main/java/org/isoron/uhabits/models/HabitList.java b/app/src/main/java/org/isoron/uhabits/models/HabitList.java new file mode 100644 index 000000000..8c8fd8226 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/HabitList.java @@ -0,0 +1,235 @@ +/* + * 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.models; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.opencsv.CSVWriter; + +import org.isoron.uhabits.utils.ColorUtils; + +import java.io.IOException; +import java.io.Writer; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * An ordered collection of {@link Habit}s. + */ +public abstract class HabitList +{ + private ModelObservable observable; + + /** + * Creates a new HabitList. + *

+ * Depending on the implementation, this list can either be empty or be + * populated by some pre-existing habits. + */ + public HabitList() + { + observable = new ModelObservable(); + } + + /** + * Inserts a new habit in the list. + * + * @param habit the habit to be inserted + */ + public abstract void add(Habit habit); + + /** + * Returns the total number of unarchived habits. + * + * @return number of unarchived habits + */ + public abstract int count(); + + /** + * Returns the total number of habits, including archived habits. + * + * @return number of habits, including archived + */ + public abstract int countWithArchived(); + + /** + * Returns a list of all habits, optionally including archived habits. + * + * @param includeArchive whether archived habits should be included the + * list + * @return list of all habits + */ + @NonNull + public abstract List getAll(boolean includeArchive); + + /** + * Returns the habit with specified id. + * + * @param id the id of the habit + * @return the habit, or null if none exist + */ + public abstract Habit getById(long id); + + /** + * Returns the habit that occupies a certain position. + * + * @param position the position of the desired habit + * @return the habit at that position, or null if there is none + */ + @Nullable + public abstract Habit getByPosition(int position); + + /** + * Returns the list of habits that match a given condition. + * + * @param matcher the matcher that checks the condition + * @return the list of matching habits + */ + @NonNull + public List getFiltered(HabitMatcher matcher) + { + LinkedList habits = new LinkedList<>(); + for (Habit h : getAll(true)) if (matcher.matches(h)) habits.add(h); + return habits; + } + + public ModelObservable getObservable() + { + return observable; + } + + /** + * Returns a list the habits that have a reminder. Does not include archived + * habits. + * + * @return list of habits with reminder + */ + @NonNull + public List getWithReminder() + { + return getFiltered(habit -> habit.hasReminder()); + } + + /** + * Returns the index of the given habit in the list, or -1 if the list does + * not contain the habit. + * + * @param h the habit + * @return the index of the habit, or -1 if not in the list + */ + public abstract int indexOf(Habit h); + + /** + * Removes the given habit from the list. + *

+ * If the given habit is not in the list, does nothing. + * + * @param h the habit to be removed. + */ + public abstract void remove(@NonNull Habit h); + + /** + * Changes the position of a habit in the list. + * + * @param from the habit that should be moved + * @param to the habit that currently occupies the desired position + */ + public abstract void reorder(Habit from, Habit to); + + /** + * Notifies the list that a certain list of habits has been modified. + *

+ * Depending on the implementation, this operation might trigger a write to + * disk, or do nothing at all. To make sure that the habits get persisted, + * this operation must be called. + * + * @param habits the list of habits that have been modified. + */ + public abstract void update(List habits); + + /** + * Notifies the list that a certain habit has been modified. + *

+ * See {@link #update(List)} for more details. + * + * @param habit the habit that has been modified. + */ + public void update(Habit habit) + { + update(Collections.singletonList(habit)); + } + + /** + * Writes the list of habits to the given writer, in CSV format. There is + * one line for each habit, containing the fields name, description, + * frequency numerator, frequency denominator and color. The color is + * written in HTML format (#000000). + * + * @param out the writer that will receive the result + * @throws IOException if write operations fail + */ + public void writeCSV(Writer out) throws IOException + { + String header[] = { + "Position", + "Name", + "Description", + "NumRepetitions", + "Interval", + "Color" + }; + + CSVWriter csv = new CSVWriter(out); + csv.writeNext(header, false); + + for (Habit habit : getAll(true)) + { + String[] cols = { + String.format("%03d", indexOf(habit) + 1), + habit.getName(), + habit.getDescription(), + Integer.toString(habit.getFreqNum()), + Integer.toString(habit.getFreqDen()), + ColorUtils.CSV_PALETTE[habit.getColor()] + }; + + csv.writeNext(cols, false); + } + + csv.close(); + } + + /** + * A HabitMatcher decides whether habits match or not a certain condition. + * They can be used to produce filtered lists of habits. + */ + public interface HabitMatcher + { + /** + * Returns true if the given habit matches. + * + * @param habit the habit to be checked. + * @return true if matches, false otherwise. + */ + boolean matches(Habit habit); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/ModelFactory.java b/app/src/main/java/org/isoron/uhabits/models/ModelFactory.java new file mode 100644 index 000000000..c1300ac71 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/ModelFactory.java @@ -0,0 +1,37 @@ +/* + * 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.models; + +/** + * Interface implemented by factories that provide concrete implementations + * of the core model classes. + */ +public interface ModelFactory +{ + RepetitionList buidRepetitionList(Habit habit); + + HabitList buildHabitList(); + + CheckmarkList buildCheckmarkList(Habit habit); + + ScoreList buildScoreList(Habit habit); + + StreakList buildStreakList(Habit habit); +} diff --git a/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java b/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java index 2ff7650f3..ffd8a8499 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java +++ b/app/src/main/java/org/isoron/uhabits/models/ModelObservable.java @@ -22,33 +22,62 @@ package org.isoron.uhabits.models; import java.util.LinkedList; import java.util.List; +/** + * A ModelObservable allows objects to subscribe themselves to it and receive + * notifications whenever the model is changed. + */ public class ModelObservable { List listeners; + /** + * Creates a new ModelObservable with no listeners. + */ public ModelObservable() { super(); listeners = new LinkedList<>(); } - public interface Listener + /** + * Adds the given listener to the observable. + * + * @param l the listener to be added. + */ + public void addListener(Listener l) { - void onModelChange(); + listeners.add(l); } - public void addListener(Listener l) + /** + * Notifies every listener that the model has changed. + *

+ * Only models should call this method. + */ + public void notifyListeners() { - listeners.add(l); + for (Listener l : listeners) l.onModelChange(); } + /** + * Removes the given listener. + *

+ * The listener will no longer be notified when the model changes. If the + * given listener is not subscrined to this observable, does nothing. + * + * @param l the listener to be removed + */ public void removeListener(Listener l) { listeners.remove(l); } - public void notifyListeners() + /** + * Interface implemented by objects that want to be notified when the model + * changes. + */ + public interface Listener { - for(Listener l : listeners) l.onModelChange(); + void onModelChange(); } } 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 f43243698..b3e8b2771 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Repetition.java +++ b/app/src/main/java/org/isoron/uhabits/models/Repetition.java @@ -19,22 +19,51 @@ package org.isoron.uhabits.models; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; -import com.activeandroid.annotation.Table; +import android.support.annotation.NonNull; -@Table(name = "Repetitions") -public class Repetition extends Model +import org.apache.commons.lang3.builder.ToStringBuilder; + +/** + * Represents a record that the user has performed a certain habit at a certain + * date. + */ +public class Repetition { - /** - * Habit to which this repetition belong. - */ - @Column(name = "habit") - public Habit habit; + @NonNull + private final Habit habit; + + private final long timestamp; /** - * Timestamp of the day this repetition occurred. Time of day should be midnight (UTC). + * Creates a new repetition with given parameters. + *

+ * The timestamp corresponds to the days this repetition occurred. Time of + * day must be midnight (UTC). + * + * @param habit the habit to which this repetition belongs. + * @param timestamp the time this repetition occurred. */ - @Column(name = "timestamp") - public Long timestamp; + public Repetition(Habit habit, long timestamp) + { + this.habit = habit; + this.timestamp = timestamp; + } + + public Habit getHabit() + { + return habit; + } + + public long getTimestamp() + { + return timestamp; + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("timestamp", timestamp) + .toString(); + } } 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 f9766645d..051825805 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -19,199 +19,179 @@ package org.isoron.uhabits.models; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.activeandroid.Cache; -import com.activeandroid.query.Delete; -import com.activeandroid.query.From; -import com.activeandroid.query.Select; -import com.activeandroid.util.SQLiteUtils; - -import org.isoron.uhabits.utils.DatabaseUtils; import org.isoron.uhabits.utils.DateUtils; import java.util.Arrays; -import java.util.GregorianCalendar; +import java.util.Calendar; import java.util.HashMap; +import java.util.List; -public class RepetitionList +/** + * The collection of {@link Repetition}s belonging to a habit. + */ +public abstract class RepetitionList { @NonNull - private Habit habit; - public ModelObservable observable = new ModelObservable(); - - public RepetitionList(@NonNull Habit habit) - { - this.habit = habit; - } + protected final Habit habit; @NonNull - protected From select() - { - return new Select().from(Repetition.class) - .where("habit = ?", habit.getId()) - .and("timestamp <= ?", DateUtils.getStartOfToday()) - .orderBy("timestamp"); - } + protected final ModelObservable observable; - @NonNull - protected From selectFromTo(long timeFrom, long timeTo) + public RepetitionList(@NonNull Habit habit) { - return select().and("timestamp >= ?", timeFrom).and("timestamp <= ?", timeTo); + this.habit = habit; + this.observable = new ModelObservable(); } /** - * Checks whether there is a repetition at a given timestamp. + * Adds a repetition to the list. + *

+ * Any implementation of this method must call observable.notifyListeners() + * after the repetition has been added. * - * @param timestamp the timestamp to check - * @return true if there is a repetition + * @param repetition the repetition to be added. */ - public boolean contains(long timestamp) - { - int count = select().where("timestamp = ?", timestamp).count(); - return (count > 0); - } + public abstract void add(Repetition repetition); /** - * Deletes the repetition at a given timestamp, if it exists. + * Returns true if the list contains a repetition that has the given + * timestamp. * - * @param timestamp the timestamp of the repetition to delete + * @param timestamp the timestamp to find. + * @return true if list contains repetition with given timestamp, false + * otherwise. */ - public void delete(long timestamp) + public boolean containsTimestamp(long timestamp) { - new Delete().from(Repetition.class) - .where("habit = ?", habit.getId()) - .and("timestamp = ?", timestamp) - .execute(); + return (getByTimestamp(timestamp) != null); } /** - * Toggles the repetition at a certain timestamp. That is, deletes the repetition if it exists - * or creates one if it does not. + * Returns the list of repetitions that happened within the given time + * interval. + * + * The list is sorted by timestamp in decreasing order. That is, the first + * element corresponds to the most recent timestamp. The endpoints of the + * interval are included. * - * @param timestamp the timestamp of the repetition to toggle + * @param fromTimestamp timestamp of the beginning of the interval + * @param toTimestamp timestamp of the end of the interval + * @return list of repetitions within given time interval */ - public void toggle(long timestamp) - { - timestamp = DateUtils.getStartOfDay(timestamp); - - if (contains(timestamp)) - delete(timestamp); - else - insert(timestamp); - - habit.scores.invalidateNewerThan(timestamp); - habit.checkmarks.deleteNewerThan(timestamp); - habit.streaks.deleteNewerThan(timestamp); - observable.notifyListeners(); - } - - private void insert(long timestamp) - { - String[] args = { habit.getId().toString(), Long.toString(timestamp) }; - SQLiteUtils.execSql("insert into Repetitions(habit, timestamp) values (?,?)", args); - } + public abstract List getByInterval(long fromTimestamp, + long toTimestamp); /** - * Returns the oldest repetition for the habit. If there is no repetition, returns null. - * Repetitions in the future are discarded. + * Returns the repetition that has the given timestamp, or null if none + * exists. * - * @return oldest repetition for the habit + * @param timestamp the repetition timestamp. + * @return the repetition that has the given timestamp. */ @Nullable - public Repetition getOldest() + public abstract Repetition getByTimestamp(long timestamp); + + @NonNull + public ModelObservable getObservable() { - return (Repetition) select().limit(1).executeSingle(); + return observable; } /** - * Returns the timestamp of the oldest repetition. If there are no repetitions, returns zero. - * Repetitions in the future are discarded. + * Returns the oldest repetition in the list. + *

+ * If the list is empty, returns null. Repetitions in the future are + * discarded. * - * @return timestamp of the oldest repetition + * @return oldest repetition in the list, or null if list is empty. */ - public long getOldestTimestamp() - { - String[] args = { habit.getId().toString(), Long.toString(DateUtils.getStartOfToday()) }; - String query = "select timestamp from Repetitions where habit = ? and timestamp <= ? " + - "order by timestamp limit 1"; - - return DatabaseUtils.longQuery(query, args); - } + @Nullable + public abstract Repetition getOldest(); /** - * Returns the total number of repetitions for each month, from the first repetition until - * today, grouped by day of week. The repetitions are returned in a HashMap. The key is the - * timestamp for the first day of the month, at midnight (00:00). The value is an integer - * array with 7 entries. The first entry contains the total number of repetitions during - * the specified month that occurred on a Saturday. The second entry corresponds to Sunday, - * and so on. If there are no repetitions during a certain month, the value is null. + * Returns the total number of repetitions for each month, from the first + * repetition until today, grouped by day of week. + *

+ * The repetitions are returned in a HashMap. The key is the timestamp for + * the first day of the month, at midnight (00:00). The value is an integer + * array with 7 entries. The first entry contains the total number of + * repetitions during the specified month that occurred on a Saturday. The + * second entry corresponds to Sunday, and so on. If there are no + * repetitions during a certain month, the value is null. * * @return total number of repetitions by month versus day of week */ @NonNull public HashMap getWeekdayFrequency() { - Repetition oldestRep = getOldest(); - if(oldestRep == null) return new HashMap<>(); + List reps = getByInterval(0, DateUtils.getStartOfToday()); + HashMap map = new HashMap<>(); - String query = "select strftime('%Y', timestamp / 1000, 'unixepoch') as year," + - "strftime('%m', timestamp / 1000, 'unixepoch') as month," + - "strftime('%w', timestamp / 1000, 'unixepoch') as weekday, " + - "count(*) from repetitions " + - "where habit = ? and timestamp <= ? " + - "group by year, month, weekday"; - - String[] params = { habit.getId().toString(), - Long.toString(DateUtils.getStartOfToday()) }; - - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); - - if(!cursor.moveToFirst()) return new HashMap<>(); - - HashMap map = new HashMap<>(); - GregorianCalendar date = DateUtils.getStartOfTodayCalendar(); - - do + for (Repetition r : reps) { - int year = Integer.parseInt(cursor.getString(0)); - int month = Integer.parseInt(cursor.getString(1)); - int weekday = (Integer.parseInt(cursor.getString(2)) + 1) % 7; - int count = cursor.getInt(3); + Calendar date = DateUtils.getCalendar(r.getTimestamp()); + int weekday = date.get(Calendar.DAY_OF_WEEK) % 7; + date.set(Calendar.DAY_OF_MONTH, 1); - date.set(year, month - 1, 1); long timestamp = date.getTimeInMillis(); - Integer[] list = map.get(timestamp); - if(list == null) + if (list == null) { list = new Integer[7]; Arrays.fill(list, 0); map.put(timestamp, list); } - list[weekday] = count; + list[weekday]++; } - while (cursor.moveToNext()); - cursor.close(); return map; } /** - * Returns the total number of repetitions that happened within the specified interval of time. + * Removes a given repetition from the list. + *

+ * If the list does not contain the repetition, it is unchanged. + *

+ * Any implementation of this method must call observable.notifyListeners() + * after the repetition has been added. * - * @param from beginning of the interval - * @param to end of the interval - * @return number of repetition in the given interval + * @param repetition the repetition to be removed */ - public int count(long from, long to) + public abstract void remove(@NonNull Repetition repetition); + + /** + * Adds or remove a repetition at a certain timestamp. + *

+ * If there exists a repetition on the list with the given timestamp, the + * method removes this repetition from the list and returns it. If there are + * no repetitions with the given timestamp, creates and adds one to the + * list, then returns it. + * + * @param timestamp the timestamp for the timestamp that should be added or + * removed. + * @return the repetition that has been added or removed. + */ + @NonNull + public Repetition toggleTimestamp(long timestamp) { - return selectFromTo(from, to).count(); + timestamp = DateUtils.getStartOfDay(timestamp); + Repetition rep = getByTimestamp(timestamp); + + if (rep != null) remove(rep); + else + { + rep = new Repetition(habit, timestamp); + add(rep); + } + +// habit.getScores().invalidateNewerThan(timestamp); +// habit.getCheckmarks().invalidateNewerThan(timestamp); +// habit.getStreaks().invalidateNewerThan(timestamp); + return rep; } } 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 5eba480e9..5943950fe 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Score.java +++ b/app/src/main/java/org/isoron/uhabits/models/Score.java @@ -19,78 +19,60 @@ package org.isoron.uhabits.models; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; -import com.activeandroid.annotation.Table; +import org.apache.commons.lang3.builder.ToStringBuilder; -@Table(name = "Score") -public class Score extends Model +/** + * Represents how strong a habit is at a certain date. + */ +public class Score { /** - * Minimum score value required to earn half a star. - */ - 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; - - /** - * 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. + * Habit to which this score belongs to. */ - public static final int HALF_STAR = 1; + private Habit habit; /** - * Status indicating that the habit has earned a full star. + * Timestamp of the day to which this score applies. Time of day should be + * midnight (UTC). */ - public static final int FULL_STAR = 2; + private Long timestamp; /** - * Habit to which this score belongs to. + * Value of the score. */ - @Column(name = "habit") - public Habit habit; + private Integer value; /** - * Timestamp of the day to which this score applies. Time of day should be midnight (UTC). + * Maximum score value attainable by any habit. */ - @Column(name = "timestamp") - public Long timestamp; + public static final int MAX_VALUE = 19259478; - /** - * Value of the score. - */ - @Column(name = "score") - public Integer score; + public Score(Habit habit, Long timestamp, Integer value) + { + this.habit = habit; + this.timestamp = timestamp; + this.value = value; + } /** - * Given the frequency of the habit, the previous score, and the value of the current checkmark, - * computes the current score for the habit. + * 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. * - * 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 frequency the frequency of the habit + * @param previousScore the previous score of the habit * @param checkmarkValue the value of the current checkmark - * * @return the current score */ - public static int compute(double frequency, int previousScore, int checkmarkValue) + public static int compute(double frequency, + int previousScore, + int checkmarkValue) { double multiplier = Math.pow(0.5, 1.0 / (14.0 / frequency - 1)); int score = (int) (previousScore * multiplier); @@ -104,16 +86,27 @@ public class Score extends Model return score; } - /** - * Return the current star status for the habit, which can one of EMPTY_STAR, HALF_STAR or - * FULL_STAR. - * - * @return current star status - */ - public int getStarStatus() + public Habit getHabit() + { + return habit; + } + + public Long getTimestamp() + { + return timestamp; + } + + public Integer getValue() + { + return value; + } + + @Override + public String toString() { - if(score >= Score.FULL_STAR_CUTOFF) return Score.FULL_STAR; - if(score >= Score.HALF_STAR_CUTOFF) return Score.HALF_STAR; - return Score.EMPTY_STAR; + return new ToStringBuilder(this) + .append("timestamp", timestamp) + .append("value", value) + .toString(); } } 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 e49ec56ca..7b0749a54 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -21,328 +21,222 @@ package org.isoron.uhabits.models; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteStatement; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.activeandroid.Cache; -import com.activeandroid.query.Delete; -import com.activeandroid.query.From; -import com.activeandroid.query.Select; -import com.activeandroid.util.SQLiteUtils; -import org.isoron.uhabits.utils.DatabaseUtils; import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.utils.InterfaceUtils; import java.io.IOException; import java.io.Writer; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.LinkedList; +import java.util.List; -public class ScoreList +public abstract class ScoreList { - @NonNull - private Habit habit; - public ModelObservable observable = new ModelObservable(); + protected final Habit habit; + + protected ModelObservable observable; /** - * Constructs a new ScoreList associated with the given habit. + * Creates a new ScoreList for the given habit. + *

+ * The list is populated automatically according to the repetitions that the + * habit has. * - * @param habit the habit this list should be associated with + * @param habit the habit to which the scores belong. */ - public ScoreList(@NonNull Habit habit) + public ScoreList(Habit habit) { this.habit = habit; - } - - protected From select() - { - return new Select() - .from(Score.class) - .where("habit = ?", habit.getId()) - .orderBy("timestamp desc"); + observable = new ModelObservable(); } /** - * Marks all scores that have timestamp equal to or newer than the given timestamp as invalid. - * Any following getValue calls will trigger the scores to be recomputed. + * 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 timestamp the oldest timestamp that should be invalidated + * @param divisor the size of the groups + * @return array of values, with one entry for each group of days */ - public void invalidateNewerThan(long timestamp) + @NonNull + public int[] getAllValues(long divisor) { - new Delete().from(Score.class) - .where("habit = ?", habit.getId()) - .and("timestamp >= ?", timestamp) - .execute(); + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep == null) return new int[0]; - observable.notifyListeners(); + long fromTimestamp = oldestRep.getTimestamp(); + long toTimestamp = DateUtils.getStartOfToday(); + return getValues(fromTimestamp, toTimestamp, divisor); } - /** - * Computes and saves the scores that are missing since the first repetition of the habit. - */ - private void computeAll() + public ModelObservable getObservable() { - long fromTimestamp = habit.repetitions.getOldestTimestamp(); - if(fromTimestamp == 0) return; - - long toTimestamp = DateUtils.getStartOfToday(); - compute(fromTimestamp, toTimestamp); + return observable; } /** - * 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. + * Returns the value of the score for today. * - * @param from timestamp of the beginning of the interval - * @param to timestamp of the end of the time interval + * @return value of today's score */ - protected void compute(long from, long to) + public int getTodayValue() { - InterfaceUtils.throwIfMainThread(); - - final long day = DateUtils.millisecondsInOneDay; - final double freq = ((double) habit.freqNum) / habit.freqDen; - - int newestScoreValue = findNewestValue(); - long newestTimestamp = findNewestTimestamp(); - - if(newestTimestamp > 0) - from = newestTimestamp + day; - - final int checkmarkValues[] = habit.checkmarks.getValues(from, to); - final long beginning = from; - - int lastScore = newestScoreValue; - int size = checkmarkValues.length; - - long timestamps[] = new long[size]; - long values[] = new long[size]; - - for (int i = 0; i < checkmarkValues.length; i++) - { - int checkmarkValue = checkmarkValues[checkmarkValues.length - i - 1]; - lastScore = Score.compute(freq, lastScore, checkmarkValue); - timestamps[i] = beginning + day * i; - values[i] = lastScore; - } - - insert(timestamps, values); + return getValue(DateUtils.getStartOfToday()); } /** - * Returns the value of the most recent score that was already computed. If no score has been - * computed yet, returns zero. + * Returns the value of the score for a given day. + * + * @param timestamp the timestamp of a day + * @return score for that day + */ + public abstract int getValue(long timestamp); + + /** + * Marks all scores that have timestamp equal to or newer than the given + * timestamp as invalid. Any following getValue calls will trigger the + * scores to be recomputed. * - * @return value of newest score, or zero if none exist + * @param timestamp the oldest timestamp that should be invalidated */ - protected int findNewestValue() - { - String args[] = { habit.getId().toString() }; - String query = "select score from Score where habit = ? order by timestamp desc limit 1"; - return SQLiteUtils.intQuery(query, args); - } + public abstract void invalidateNewerThan(long timestamp); - private long findNewestTimestamp() + public void writeCSV(Writer out) throws IOException { - String args[] = { habit.getId().toString() }; - String query = "select timestamp from Score where habit = ? order by timestamp desc limit 1"; - return DatabaseUtils.longQuery(query, args); - } + computeAll(); - private void insert(long timestamps[], long values[]) - { - String query = "insert into Score(habit, timestamp, score) values (?,?,?)"; + SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); - SQLiteDatabase db = Cache.openDatabase(); - db.beginTransaction(); + String query = + "select timestamp, score from score where habit = ? order by timestamp"; + String params[] = {habit.getId().toString()}; - try - { - SQLiteStatement statement = db.compileStatement(query); + SQLiteDatabase db = Cache.openDatabase(); + Cursor cursor = db.rawQuery(query, params); - for (int i = 0; i < timestamps.length; i++) - { - statement.bindLong(1, habit.getId()); - statement.bindLong(2, timestamps[i]); - statement.bindLong(3, values[i]); - statement.execute(); - } + if (!cursor.moveToFirst()) return; - db.setTransactionSuccessful(); - } - finally + do { - db.endTransaction(); - } - } - - /** - * Returns the score for a certain day. - * - * @param timestamp the timestamp for the day - * @return the score for the day - */ - @Nullable - protected Score get(long timestamp) - { - Repetition oldestRep = habit.repetitions.getOldest(); - if(oldestRep == null) return null; - - compute(oldestRep.timestamp, timestamp); + String timestamp = dateFormat.format(new Date(cursor.getLong(0))); + String score = String.format("%.4f", + ((float) cursor.getInt(1)) / Score.MAX_VALUE); + out.write(String.format("%s,%s\n", timestamp, score)); - return select().where("timestamp = ?", timestamp).executeSingle(); - } + } while (cursor.moveToNext()); - /** - * Returns the value of the score for a given day. - * - * @param timestamp the timestamp of a day - * @return score for that day - */ - public int getValue(long timestamp) - { - computeAll(); - String[] args = { habit.getId().toString(), Long.toString(timestamp) }; - return SQLiteUtils.intQuery("select score from Score where habit = ? and timestamp = ?", args); + cursor.close(); + out.close(); } - /** - * 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 = DateUtils.getStartOfToday(); - return getValues(fromTimestamp, toTimestamp, divisor); - } + protected abstract void add(List scores); /** - * Same as getAllValues(long), but using a specified interval. + * 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 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 + * @param from timestamp of the beginning of the interval + * @param to timestamp of the end of the time interval */ - @NonNull - protected int[] getValues(long from, long to, long divisor) + protected void compute(long from, long to) { - compute(from, to); + final long day = DateUtils.millisecondsInOneDay; + final double freq = ((double) habit.getFreqNum()) / habit.getFreqDen(); - divisor *= DateUtils.millisecondsInOneDay; - Long offset = to + divisor; + int newestValue = 0; + long newestTimestamp = 0; - String query = "select ((timestamp - ?) / ?) as time, avg(score) from Score " + - "where habit = ? and timestamp >= ? and timestamp <= ? " + - "group by time order by time desc"; + Score newest = getNewestComputed(); + if(newest != null) + { + newestValue = newest.getValue(); + newestTimestamp = newest.getTimestamp(); + } - String params[] = { offset.toString(), Long.toString(divisor), habit.getId().toString(), - Long.toString(from), Long.toString(to) }; + if (newestTimestamp > 0) from = newestTimestamp + day; - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); - - if(!cursor.moveToFirst()) return new int[0]; + final int checkmarkValues[] = habit.getCheckmarks().getValues(from, to); + final long beginning = from; - int k = 0; - int[] scores = new int[cursor.getCount()]; + int lastScore = newestValue; + List scores = new LinkedList<>(); - do + for (int i = 0; i < checkmarkValues.length; i++) { - scores[k++] = (int) cursor.getFloat(1); + int value = checkmarkValues[checkmarkValues.length - i - 1]; + lastScore = Score.compute(freq, lastScore, value); + scores.add(new Score(habit, beginning + day * i, lastScore)); } - while (cursor.moveToNext()); - cursor.close(); - return scores; + add(scores); } /** - * Returns the score for today. - * - * @return score for today + * Computes and saves the scores that are missing since the first repetition + * of the habit. */ - @Nullable - protected Score getToday() + protected void computeAll() { - return get(DateUtils.getStartOfToday()); + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep == null) return; + + long toTimestamp = DateUtils.getStartOfToday(); + compute(oldestRep.getTimestamp(), toTimestamp); } /** - * Returns the value of the score for today. + * Returns the score for a certain day. * - * @return value of today's score + * @param timestamp the timestamp for the day + * @return the score for the day */ - public int getTodayValue() - { - return getValue(DateUtils.getStartOfToday()); - } + protected abstract Score get(long timestamp); /** - * Returns the star status for today. The returned value is either Score.EMPTY_STAR, - * Score.HALF_STAR or Score.FULL_STAR. + * Returns the most recent score that was already computed. + *

+ * If no score has been computed yet, returns null. * - * @return star status for today + * @return the newest score computed, or null if none exist */ - public int getTodayStarStatus() - { - Score score = getToday(); - if(score != null) return score.getStarStatus(); - else return Score.EMPTY_STAR; - } - - public void writeCSV(Writer out) throws IOException - { - computeAll(); - - SimpleDateFormat dateFormat = DateUtils.getCSVDateFormat(); - - String query = "select timestamp, score from score where habit = ? order by timestamp"; - String params[] = { habit.getId().toString() }; - - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); - - if(!cursor.moveToFirst()) return; - - do - { - String timestamp = dateFormat.format(new Date(cursor.getLong(0))); - String score = String.format("%.4f", ((float) cursor.getInt(1)) / Score.MAX_VALUE); - out.write(String.format("%s,%s\n", timestamp, score)); - - } while(cursor.moveToNext()); + @Nullable + protected abstract Score getNewestComputed(); - cursor.close(); - out.close(); - } + /** + * Same as getAllValues(long), but using a specified interval. + * + * @param from beginning of the interval (included) + * @param to end of the interval (included) + * @param divisor size of the groups + * @return array of values, with one entry for each group of days + */ + protected abstract int[] getValues(long from, long to, long divisor); } diff --git a/app/src/main/java/org/isoron/uhabits/models/Streak.java b/app/src/main/java/org/isoron/uhabits/models/Streak.java index 35a20f444..53854a798 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Streak.java +++ b/app/src/main/java/org/isoron/uhabits/models/Streak.java @@ -19,20 +19,63 @@ package org.isoron.uhabits.models; -import com.activeandroid.Model; -import com.activeandroid.annotation.Column; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.isoron.uhabits.utils.DateUtils; -public class Streak extends Model +public class Streak { - @Column(name = "habit") - public Habit habit; + private Habit habit; - @Column(name = "start") - public Long start; + private long start; - @Column(name = "end") - public Long end; + private long end; - @Column(name = "length") - public Long length; + public Streak(Habit habit, long start, long end) + { + this.habit = habit; + this.start = start; + this.end = end; + } + + public int compareLonger(Streak other) + { + if (this.getLength() != other.getLength()) + return Long.signum(this.getLength() - other.getLength()); + + return Long.signum(this.getEnd() - other.getEnd()); + } + + public int compareNewer(Streak other) + { + return Long.signum(this.getEnd() - other.getEnd()); + } + + public long getEnd() + { + return end; + } + + public Habit getHabit() + { + return habit; + } + + public long getLength() + { + return (end - start) / DateUtils.millisecondsInOneDay + 1; + } + + public long getStart() + { + return start; + } + + @Override + public String toString() + { + return new ToStringBuilder(this) + .append("start", start) + .append("end", end) + .toString(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/StreakList.java b/app/src/main/java/org/isoron/uhabits/models/StreakList.java index 523b9e339..19fc4718c 100644 --- a/app/src/main/java/org/isoron/uhabits/models/StreakList.java +++ b/app/src/main/java/org/isoron/uhabits/models/StreakList.java @@ -19,100 +19,123 @@ package org.isoron.uhabits.models; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; - -import com.activeandroid.ActiveAndroid; -import com.activeandroid.Cache; -import com.activeandroid.query.Delete; -import com.activeandroid.query.Select; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.utils.InterfaceUtils; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; -public class StreakList +/** + * The collection of {@link Streak}s that belong to a habit. + *

+ * This list is populated automatically from the list of repetitions. + */ +public abstract class StreakList { - private Habit habit; - public ModelObservable observable = new ModelObservable(); + protected final Habit habit; + + protected ModelObservable observable; - public StreakList(Habit habit) + protected StreakList(Habit habit) { this.habit = habit; + observable = new ModelObservable(); } - public List getAll(int limit) + public abstract List getAll(); + + public List getBest(int limit) { - rebuild(); + List streaks = getAll(); + Collections.sort(streaks, (s1, s2) -> s2.compareLonger(s1)); + streaks = streaks.subList(0, Math.min(streaks.size(), limit)); + Collections.sort(streaks, (s1, s2) -> s2.compareNewer(s1)); + return streaks; + } - String query = "select * from (select * from streak where habit=? " + - "order by end <> ?, length desc, end desc limit ?) order by end desc"; + public abstract Streak getNewestComputed(); - String params[] = {habit.getId().toString(), Long.toString(DateUtils.getStartOfToday()), - Integer.toString(limit)}; + public ModelObservable getObservable() + { + return observable; + } - SQLiteDatabase db = Cache.openDatabase(); - Cursor cursor = db.rawQuery(query, params); + public abstract void invalidateNewerThan(long timestamp); - if(!cursor.moveToFirst()) - { - cursor.close(); - return new LinkedList<>(); - } + public void rebuild() + { + long today = DateUtils.getStartOfToday(); - List streaks = new LinkedList<>(); + Long beginning = findBeginning(); + if (beginning == null || beginning > today) return; + + int checks[] = habit.getCheckmarks().getValues(beginning, today); + List streaks = checkmarksToStreaks(beginning, checks); - do + removeNewestComputed(); + insert(streaks); + } + + /** + * Converts a list of checkmark values to a list of streaks. + * + * @param beginning the timestamp corresponding to the first checkmark + * value. + * @param checks the checkmarks values, ordered by decreasing timestamp. + * @return the list of streaks. + */ + @NonNull + protected List checkmarksToStreaks(Long beginning, int[] checks) + { + ArrayList transitions = getTransitions(beginning, checks); + + List streaks = new LinkedList<>(); + for (int i = 0; i < transitions.size(); i += 2) { - Streak s = Streak.load(Streak.class, cursor.getInt(0)); - streaks.add(s); + long start = transitions.get(i); + long end = transitions.get(i + 1); + streaks.add(new Streak(habit, start, end)); } - while (cursor.moveToNext()); - cursor.close(); return streaks; - } - public Streak getNewest() + /** + * Finds the place where we should start when recomputing the streaks. + * + * @return + */ + @Nullable + protected Long findBeginning() { - return new Select().from(Streak.class) - .where("habit = ?", habit.getId()) - .orderBy("end desc") - .limit(1) - .executeSingle(); + Streak newestStreak = getNewestComputed(); + if (newestStreak != null) return newestStreak.getStart(); + + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep != null) return oldestRep.getTimestamp(); + + return null; } - public void rebuild() + /** + * Returns the timestamps where there was a transition from performing a + * habit to not performing a habit, and vice-versa. + * + * @param beginning the timestamp for the first checkmark + * @param checks the checkmarks, ordered by decresing timestamp + * @return the list of transitions + */ + @NonNull + protected ArrayList getTransitions(Long beginning, int[] checks) { - InterfaceUtils.throwIfMainThread(); - - long beginning; - long today = DateUtils.getStartOfToday(); long day = DateUtils.millisecondsInOneDay; + long current = beginning; - Streak newestStreak = getNewest(); - if (newestStreak != null) - { - beginning = newestStreak.start; - } - else - { - Repetition oldestRep = habit.repetitions.getOldest(); - if (oldestRep == null) return; - - beginning = oldestRep.timestamp; - } - - if (beginning > today) return; - - int checks[] = habit.checkmarks.getValues(beginning, today); ArrayList list = new ArrayList<>(); - - long current = beginning; list.add(current); for (int i = 1; i < checks.length; i++) @@ -126,38 +149,10 @@ public class StreakList if (list.size() % 2 == 1) list.add(current); - ActiveAndroid.beginTransaction(); - - if(newestStreak != null) newestStreak.delete(); - - try - { - for (int i = 0; i < list.size(); i += 2) - { - Streak streak = new Streak(); - streak.habit = habit; - streak.start = list.get(i); - streak.end = list.get(i + 1); - streak.length = (streak.end - streak.start) / day + 1; - streak.save(); - } - - ActiveAndroid.setTransactionSuccessful(); - } - finally - { - ActiveAndroid.endTransaction(); - } + return list; } + protected abstract void insert(List streaks); - public void deleteNewerThan(long timestamp) - { - new Delete().from(Streak.class) - .where("habit = ?", habit.getId()) - .and("end >= ?", timestamp - DateUtils.millisecondsInOneDay) - .execute(); - - observable.notifyListeners(); - } + protected abstract void removeNewestComputed(); } diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryCheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryCheckmarkList.java new file mode 100644 index 000000000..f42879deb --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryCheckmarkList.java @@ -0,0 +1,102 @@ +/* + * 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.models.memory; + +import org.isoron.uhabits.models.Checkmark; +import org.isoron.uhabits.models.CheckmarkList; +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.utils.DateUtils; + +import java.util.Collections; +import java.util.LinkedList; + +/** + * In-memory implementation of {@link CheckmarkList}. + */ +public class MemoryCheckmarkList extends CheckmarkList +{ + LinkedList list; + + public MemoryCheckmarkList(Habit habit) + { + super(habit); + list = new LinkedList<>(); + } + + @Override + public int[] getValues(long from, long to) + { + compute(from, to); + if (from > to) return new int[0]; + + int length = (int) ((to - from) / DateUtils.millisecondsInOneDay + 1); + int values[] = new int[length]; + + int k = 0; + for (Checkmark c : list) + if(c.getTimestamp() >= from && c.getTimestamp() <= to) + values[k++] = c.getValue(); + + return values; + } + + @Override + public void invalidateNewerThan(long timestamp) + { + LinkedList invalid = new LinkedList<>(); + + for (Checkmark c : list) + if (c.getTimestamp() >= timestamp) invalid.add(c); + + list.removeAll(invalid); + } + + @Override + protected Checkmark getNewest() + { + long newestTimestamp = 0; + Checkmark newestCheck = null; + + for (Checkmark c : list) + { + if (c.getTimestamp() > newestTimestamp) + { + newestCheck = c; + newestTimestamp = c.getTimestamp(); + } + } + + return newestCheck; + } + + @Override + protected void insert(long[] timestamps, int[] values) + { + for (int i = 0; i < timestamps.length; i++) + { + long t = timestamps[i]; + int v = values[i]; + list.add(new Checkmark(habit, t, v)); + } + + Collections.sort(list, + (c1, c2) -> (int) (c2.getTimestamp() - c1.getTimestamp())); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java new file mode 100644 index 000000000..deee26b0d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryHabitList.java @@ -0,0 +1,111 @@ +/* + * 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.models.memory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; + +import java.util.LinkedList; +import java.util.List; + +/** + * In-memory implementation of {@link HabitList}. + */ +public class MemoryHabitList extends HabitList +{ + @NonNull + private LinkedList list; + + public MemoryHabitList() + { + list = new LinkedList<>(); + } + + @Override + public void add(Habit habit) + { + list.addLast(habit); + } + + @Override + public int count() + { + int count = 0; + for (Habit h : list) if (!h.isArchived()) count++; + return count; + } + + @Override + public int countWithArchived() + { + return list.size(); + } + + @Override + public Habit getById(long id) + { + for (Habit h : list) if (h.getId() == id) return h; + return null; + } + + @NonNull + @Override + public List getAll(boolean includeArchive) + { + if (includeArchive) return new LinkedList<>(list); + return getFiltered(habit -> !habit.isArchived()); + } + + @Nullable + @Override + public Habit getByPosition(int position) + { + return list.get(position); + } + + @Override + public int indexOf(Habit h) + { + return list.indexOf(h); + } + + @Override + public void remove(@NonNull Habit habit) + { + list.remove(habit); + } + + @Override + public void reorder(Habit from, Habit to) + { + int toPos = indexOf(to); + list.remove(from); + list.add(toPos, from); + } + + @Override + public void update(List habits) + { + // NOP + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java new file mode 100644 index 000000000..389d7f59e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryModelFactory.java @@ -0,0 +1,61 @@ +/* + * 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.models.memory; + +import org.isoron.uhabits.models.CheckmarkList; +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; +import org.isoron.uhabits.models.ModelFactory; +import org.isoron.uhabits.models.RepetitionList; +import org.isoron.uhabits.models.ScoreList; +import org.isoron.uhabits.models.StreakList; + +public class MemoryModelFactory implements ModelFactory +{ + @Override + public RepetitionList buidRepetitionList(Habit habit) + { + return new MemoryRepetitionList(habit); + } + + @Override + public HabitList buildHabitList() + { + return new MemoryHabitList(); + } + + @Override + public CheckmarkList buildCheckmarkList(Habit habit) + { + return new MemoryCheckmarkList(habit); + } + + @Override + public ScoreList buildScoreList(Habit habit) + { + return null; + } + + @Override + public StreakList buildStreakList(Habit habit) + { + return new MemoryStreakList(habit); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java new file mode 100644 index 000000000..13016e8f2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryRepetitionList.java @@ -0,0 +1,106 @@ +/* + * 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.models.memory; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Repetition; +import org.isoron.uhabits.models.RepetitionList; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +/** + * In-memory implementation of {@link RepetitionList}. + */ +public class MemoryRepetitionList extends RepetitionList +{ + LinkedList list; + + public MemoryRepetitionList(Habit habit) + { + super(habit); + list = new LinkedList<>(); + } + + @Override + public void add(Repetition repetition) + { + list.add(repetition); + observable.notifyListeners(); + } + + @Override + public List getByInterval(long fromTimestamp, long toTimestamp) + { + LinkedList filtered = new LinkedList<>(); + for (Repetition r : list) + { + long t = r.getTimestamp(); + if (t >= fromTimestamp && t <= toTimestamp) filtered.add(r); + } + + Collections.sort(filtered, + (r1, r2) -> (int) (r1.getTimestamp() - r2.getTimestamp())); + + return filtered; + } + + @Nullable + @Override + public Repetition getByTimestamp(long timestamp) + { + for (Repetition r : list) + if (r.getTimestamp() == timestamp) return r; + + return null; + } + + @Nullable + @Override + public Repetition getOldest() + { + long oldestTime = Long.MAX_VALUE; + Repetition oldestRep = null; + + for (Repetition rep : list) + { + if (rep.getTimestamp() < oldestTime) + { + oldestRep = rep; + oldestTime = rep.getTimestamp(); + } + + } + + return oldestRep; + } + + @Override + public void remove(@NonNull Repetition repetition) + { + list.remove(repetition); + observable.notifyListeners(); + } + +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/MemoryStreakList.java b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryStreakList.java new file mode 100644 index 000000000..4efab4ecf --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/MemoryStreakList.java @@ -0,0 +1,86 @@ +/* + * 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.models.memory; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Streak; +import org.isoron.uhabits.models.StreakList; +import org.isoron.uhabits.utils.DateUtils; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +public class MemoryStreakList extends StreakList +{ + LinkedList list; + + public MemoryStreakList(Habit habit) + { + super(habit); + list = new LinkedList<>(); + } + + @Override + public Streak getNewestComputed() + { + Streak newest = null; + + for(Streak s : list) + if(newest == null || s.getEnd() > newest.getEnd()) + newest = s; + + return newest; + } + + @Override + public void invalidateNewerThan(long timestamp) + { + LinkedList discard = new LinkedList<>(); + + for(Streak s : list) + if(s.getEnd() >= timestamp - DateUtils.millisecondsInOneDay) + discard.add(s); + + list.removeAll(discard); + observable.notifyListeners(); + } + + @Override + protected void insert(List streaks) + { + list.addAll(streaks); + Collections.sort(list, (s1, s2) -> s2.compareNewer(s1)); + } + + @Override + protected void removeNewestComputed() + { + Streak newest = getNewestComputed(); + if(newest != null) list.remove(newest); + } + + @Override + public List getAll() + { + rebuild(); + return new LinkedList<>(list); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/memory/package-info.java b/app/src/main/java/org/isoron/uhabits/models/memory/package-info.java new file mode 100644 index 000000000..f8272f334 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/memory/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +/** + * Provides in-memory implementation of core models. + */ +package org.isoron.uhabits.models.memory; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/models/package-info.java b/app/src/main/java/org/isoron/uhabits/models/package-info.java new file mode 100644 index 000000000..9cb029b90 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/package-info.java @@ -0,0 +1,24 @@ +/* + * 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 Licenses along + * with this program. If not, see . + */ + +/** + * Provides core models classes, such as {@link org.isoron.uhabits.models.Habit} + * and {@link org.isoron.uhabits.models.Repetition}. + */ +package org.isoron.uhabits.models; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/CheckmarkRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/CheckmarkRecord.java new file mode 100644 index 000000000..a35d7a1c9 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/CheckmarkRecord.java @@ -0,0 +1,62 @@ +/* + * 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.models.sqlite; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; + +import org.isoron.uhabits.models.Checkmark; +import org.isoron.uhabits.models.Habit; + +/** + * The SQLite database record corresponding to a {@link Checkmark}. + */ +@Table(name = "Checkmarks") +public class CheckmarkRecord extends Model +{ + /** + * The habit to which this checkmark belongs. + */ + @Column(name = "habit") + public HabitRecord habit; + + /** + * Timestamp of the day to which this checkmark corresponds. Time of the day + * must be midnight (UTC). + */ + @Column(name = "timestamp") + public Long timestamp; + + /** + * Indicates whether there is a repetition at the given timestamp or not, + * and whether the repetition was expected. Assumes one of the values + * UNCHECKED, CHECKED_EXPLICITLY or CHECKED_IMPLICITLY. + */ + @Column(name = "value") + public Integer value; + + public Checkmark toCheckmark() + { + SQLiteHabitList habitList = SQLiteHabitList.getInstance(); + Habit h = habitList.getById(habit.getId()); + return new Checkmark(h, timestamp, value); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/HabitRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/HabitRecord.java new file mode 100644 index 000000000..052739ef5 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/HabitRecord.java @@ -0,0 +1,177 @@ +/* + * 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.models.sqlite; + +import android.annotation.SuppressLint; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; +import com.activeandroid.query.Delete; +import com.activeandroid.util.SQLiteUtils; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.utils.DatabaseUtils; + +/** + * The SQLite database record corresponding to a {@link Habit}. + */ +@Table(name = "Habits") +public class HabitRecord extends Model +{ + public static final String HABIT_URI_FORMAT = + "content://org.isoron.uhabits/habit/%d"; + + @Column(name = "name") + public String name; + + @Column(name = "description") + public String description; + + @Column(name = "freq_num") + public Integer freqNum; + + @Column(name = "freq_den") + public Integer freqDen; + + @Column(name = "color") + public Integer color; + + @Column(name = "position") + public Integer position; + + @Nullable + @Column(name = "reminder_hour") + public Integer reminderHour; + + @Nullable + @Column(name = "reminder_min") + public Integer reminderMin; + + @NonNull + @Column(name = "reminder_days") + public Integer reminderDays; + + @Column(name = "highlight") + public Integer highlight; + + @Column(name = "archived") + public Integer archived; + + public HabitRecord() + { + + } + + @Nullable + public static HabitRecord get(Long id) + { + return HabitRecord.load(HabitRecord.class, id); + } + + /** + * Changes the id of a habit on the database. + * + * @param oldId the original id + * @param newId the new id + */ + @SuppressLint("DefaultLocale") + public static void updateId(long oldId, long newId) + { + SQLiteUtils.execSql( + String.format("update Habits set Id = %d where Id = %d", newId, + oldId)); + } + + /** + * Deletes the habit and all data associated to it, including checkmarks, + * repetitions and scores. + */ + public void cascadeDelete() + { + Long id = getId(); + + DatabaseUtils.executeAsTransaction(() -> { + new Delete() + .from(CheckmarkRecord.class) + .where("habit = ?", id) + .execute(); + + new Delete() + .from(RepetitionRecord.class) + .where("habit = ?", id) + .execute(); + + new Delete() + .from(ScoreRecord.class) + .where("habit = ?", id) + .execute(); + + new Delete() + .from(StreakRecord.class) + .where("habit = ?", id) + .execute(); + + delete(); + }); + } + + public void copyFrom(Habit model) + { + this.name = model.getName(); + this.description = model.getDescription(); + this.freqNum = model.getFreqNum(); + this.freqDen = model.getFreqDen(); + this.color = model.getColor(); + this.reminderHour = model.getReminderHour(); + this.reminderMin = model.getReminderMin(); + this.reminderDays = model.getReminderDays(); + this.highlight = model.getHighlight(); + this.archived = model.getArchived(); + } + + public void copyTo(Habit habit) + { + habit.setName(this.name); + habit.setDescription(this.description); + habit.setFreqNum(this.freqNum); + habit.setFreqDen(this.freqDen); + habit.setColor(this.color); + habit.setReminderHour(this.reminderHour); + habit.setReminderMin(this.reminderMin); + habit.setReminderDays(this.reminderDays); + habit.setHighlight(this.highlight); + habit.setArchived(this.archived); + habit.setId(this.getId()); + } + + /** + * Saves the habit on the database, and assigns the specified id to it. + * + * @param id the id that the habit should receive + */ + public void save(long id) + { + save(); + updateId(getId(), id); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/RepetitionRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/RepetitionRecord.java new file mode 100644 index 000000000..e29f792de --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/RepetitionRecord.java @@ -0,0 +1,58 @@ +/* + * 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.models.sqlite; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Repetition; + +/** + * The SQLite database record corresponding to a {@link Repetition}. + */ +@Table(name = "Repetitions") +public class RepetitionRecord extends Model +{ + @Column(name = "habit") + public HabitRecord habit; + + @Column(name = "timestamp") + public Long timestamp; + + public void copyFrom(Repetition repetition) + { + habit = HabitRecord.get(repetition.getHabit().getId()); + timestamp = repetition.getTimestamp(); + } + + public static RepetitionRecord get(Long id) + { + return RepetitionRecord.load(RepetitionRecord.class, id); + } + + public Repetition toRepetition() + { + SQLiteHabitList habitList = SQLiteHabitList.getInstance(); + Habit h = habitList.getById(habit.getId()); + return new Repetition(h, timestamp); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java new file mode 100644 index 000000000..48db5fe95 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLModelFactory.java @@ -0,0 +1,64 @@ +/* + * 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.models.sqlite; + +import org.isoron.uhabits.models.CheckmarkList; +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; +import org.isoron.uhabits.models.ModelFactory; +import org.isoron.uhabits.models.RepetitionList; +import org.isoron.uhabits.models.ScoreList; +import org.isoron.uhabits.models.StreakList; + +/** + * Factory that provides models backed by an SQLite database. + */ +public class SQLModelFactory implements ModelFactory +{ + @Override + public RepetitionList buidRepetitionList(Habit habit) + { + return new SQLiteRepetitionList(habit); + } + + @Override + public CheckmarkList buildCheckmarkList(Habit habit) + { + return new SQLiteCheckmarkList(habit); + } + + @Override + public HabitList buildHabitList() + { + return new SQLiteHabitList(); + } + + @Override + public ScoreList buildScoreList(Habit habit) + { + return new SQLiteScoreList(habit); + } + + @Override + public StreakList buildStreakList(Habit habit) + { + return new SQLiteStreakList(habit); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java new file mode 100644 index 000000000..f3647e4d6 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteCheckmarkList.java @@ -0,0 +1,139 @@ +/* + * 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.models.sqlite; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.activeandroid.Cache; +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; + +import org.isoron.uhabits.models.Checkmark; +import org.isoron.uhabits.models.CheckmarkList; +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.utils.DateUtils; + +/** + * Implementation of a {@link CheckmarkList} that is backed by SQLite. + */ +public class SQLiteCheckmarkList extends CheckmarkList +{ + public SQLiteCheckmarkList(Habit habit) + { + super(habit); + } + + @Override + public void invalidateNewerThan(long timestamp) + { + new Delete() + .from(CheckmarkRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp >= ?", timestamp) + .execute(); + + observable.notifyListeners(); + } + + @Override + @NonNull + public int[] getValues(long fromTimestamp, long toTimestamp) + { + compute(fromTimestamp, toTimestamp); + + if (fromTimestamp > toTimestamp) return new int[0]; + + String query = "select value, timestamp from Checkmarks where " + + "habit = ? and timestamp >= ? and timestamp <= ?"; + + SQLiteDatabase db = Cache.openDatabase(); + String args[] = { + habit.getId().toString(), + Long.toString(fromTimestamp), + Long.toString(toTimestamp) + }; + Cursor cursor = db.rawQuery(query, args); + + long day = DateUtils.millisecondsInOneDay; + int nDays = (int) ((toTimestamp - fromTimestamp) / day) + 1; + int[] checks = new int[nDays]; + + if (cursor.moveToFirst()) + { + do + { + long timestamp = cursor.getLong(1); + int offset = (int) ((timestamp - fromTimestamp) / day); + checks[nDays - offset - 1] = cursor.getInt(0); + + } while (cursor.moveToNext()); + } + + cursor.close(); + return checks; + } + + @Override + @Nullable + protected Checkmark getNewest() + { + CheckmarkRecord record = new Select() + .from(CheckmarkRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp <= ?", DateUtils.getStartOfToday()) + .orderBy("timestamp desc") + .limit(1) + .executeSingle(); + + return record.toCheckmark(); + } + + @Override + protected void insert(long timestamps[], int values[]) + { + String query = + "insert into Checkmarks(habit, timestamp, value) values (?,?,?)"; + + SQLiteDatabase db = Cache.openDatabase(); + db.beginTransaction(); + try + { + SQLiteStatement statement = db.compileStatement(query); + + for (int i = 0; i < timestamps.length; i++) + { + statement.bindLong(1, habit.getId()); + statement.bindLong(2, timestamps[i]); + statement.bindLong(3, values[i]); + statement.execute(); + } + + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java new file mode 100644 index 000000000..2a8f33b91 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java @@ -0,0 +1,232 @@ +/* + * 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.models.sqlite; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.activeandroid.query.From; +import com.activeandroid.query.Select; +import com.activeandroid.query.Update; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * Implementation of a {@link HabitList} that is backed by SQLite. + */ +public class SQLiteHabitList extends HabitList +{ + private static SQLiteHabitList instance; + + private HashMap cache; + + public SQLiteHabitList() + { + cache = new HashMap<>(); + } + + public static SQLiteHabitList getInstance() + { + if (instance == null) instance = new SQLiteHabitList(); + return instance; + } + + @Override + public void add(Habit habit) + { + if(cache.containsValue(habit)) + throw new RuntimeException("habit already in cache"); + + HabitRecord record = new HabitRecord(); + record.copyFrom(habit); + record.position = countWithArchived(); + + Long id = habit.getId(); + if(id == null) id = record.save(); + else record.save(id); + + habit.setId(id); + cache.put(id, habit); + } + + @Override + public int count() + { + return select().count(); + } + + @Override + public int countWithArchived() + { + return selectWithArchived().count(); + } + + @Override + @NonNull + public List getAll(boolean includeArchive) + { + List recordList; + if (includeArchive) recordList = selectWithArchived().execute(); + else recordList = select().execute(); + + List habits = new LinkedList<>(); + for (HabitRecord record : recordList) + { + Habit habit = getById(record.getId()); + if (habit == null) + throw new RuntimeException("habit not in database"); + habits.add(habit); + } + + return habits; + } + + @Override + @Nullable + public Habit getById(long id) + { + if (!cache.containsKey(id)) + { + HabitRecord record = HabitRecord.get(id); + if (record == null) return null; + + Habit habit = new Habit(); + record.copyTo(habit); + cache.put(id, habit); + } + + return cache.get(id); + } + + @Override + @Nullable + public Habit getByPosition(int position) + { + HabitRecord record = selectWithArchived() + .where("position = ?", position) + .executeSingle(); + + return getById(record.getId()); + } + + @Override + public int indexOf(Habit h) + { + HabitRecord record = HabitRecord.get(h.getId()); + if (record == null) return -1; + return record.position; + } + + @Deprecated + public void rebuildOrder() + { + List habits = getAll(true); + + int i = 0; + for (Habit h : habits) + { + HabitRecord record = HabitRecord.get(h.getId()); + if (record == null) + throw new RuntimeException("habit not in database"); + + record.position = i++; + record.save(); + } + + update(habits); + } + + @Override + public void remove(@NonNull Habit habit) + { + if (!cache.containsKey(habit.getId())) + throw new RuntimeException("habit not in cache"); + + cache.remove(habit.getId()); + HabitRecord record = HabitRecord.get(habit.getId()); + if (record == null) throw new RuntimeException("habit not in database"); + record.cascadeDelete(); + rebuildOrder(); + } + + @Override + public void reorder(Habit from, Habit to) + { + if (from == to) return; + + Integer toPos = indexOf(to); + Integer fromPos = indexOf(from); + + if (toPos < fromPos) + { + new Update(HabitRecord.class) + .set("position = position + 1") + .where("position >= ? and position < ?", toPos, fromPos) + .execute(); + } + else + { + new Update(HabitRecord.class) + .set("position = position - 1") + .where("position > ? and position <= ?", fromPos, toPos) + .execute(); + } + + HabitRecord record = HabitRecord.get(from.getId()); + if (record == null) throw new RuntimeException("habit not in database"); + record.position = toPos; + record.save(); + + update(from); + } + + @Override + public void update(List habits) + { + for (Habit h : habits) + { + HabitRecord record = HabitRecord.get(h.getId()); + if (record == null) + throw new RuntimeException("habit not in database"); + record.copyFrom(h); + record.save(); + } + } + + @NonNull + private From select() + { + return new Select() + .from(HabitRecord.class) + .where("archived = 0") + .orderBy("position"); + } + + @NonNull + private From selectWithArchived() + { + return new Select().from(HabitRecord.class).orderBy("position"); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java new file mode 100644 index 000000000..ea468d32c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteRepetitionList.java @@ -0,0 +1,143 @@ +/* + * 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.models.sqlite; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.activeandroid.query.Delete; +import com.activeandroid.query.From; +import com.activeandroid.query.Select; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Repetition; +import org.isoron.uhabits.models.RepetitionList; +import org.isoron.uhabits.utils.DateUtils; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +/** + * Implementation of a {@link RepetitionList} that is backed by SQLite. + */ +public class SQLiteRepetitionList extends RepetitionList +{ + HashMap cache; + + public SQLiteRepetitionList(@NonNull Habit habit) + { + super(habit); + this.cache = new HashMap<>(); + } + + @Override + public void add(Repetition rep) + { + RepetitionRecord record = new RepetitionRecord(); + record.copyFrom(rep); + long id = record.save(); + cache.put(id, rep); + observable.notifyListeners(); + } + + @Override + public List getByInterval(long timeFrom, long timeTo) + { + return getFromRecord(selectFromTo(timeFrom, timeTo).execute()); + } + + @Override + public Repetition getByTimestamp(long timestamp) + { + RepetitionRecord record = + select().where("timestamp = ?", timestamp).executeSingle(); + return getFromRecord(record); + } + + @Override + public Repetition getOldest() + { + RepetitionRecord record = select().limit(1).executeSingle(); + return getFromRecord(record); + } + + @Override + public void remove(@NonNull Repetition repetition) + { + new Delete() + .from(RepetitionRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp = ?", repetition.getTimestamp()) + .execute(); + + observable.notifyListeners(); + } + + @NonNull + private List getFromRecord( + @Nullable List records) + { + List reps = new LinkedList<>(); + if (records == null) return reps; + + for (RepetitionRecord record : records) + { + Repetition rep = getFromRecord(record); + reps.add(rep); + } + + return reps; + } + + @Nullable + private Repetition getFromRecord(@Nullable RepetitionRecord record) + { + if (record == null) return null; + + Long id = record.getId(); + + if (!cache.containsKey(id)) + { + Repetition repetition = record.toRepetition(); + cache.put(id, repetition); + } + + return cache.get(id); + } + + @NonNull + private From select() + { + return new Select() + .from(RepetitionRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp <= ?", DateUtils.getStartOfToday()) + .orderBy("timestamp"); + } + + @NonNull + private From selectFromTo(long timeFrom, long timeTo) + { + return select() + .and("timestamp >= ?", timeFrom) + .and("timestamp <= ?", timeTo); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java new file mode 100644 index 000000000..e8d1603f3 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteScoreList.java @@ -0,0 +1,173 @@ +/* + * 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.models.sqlite; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.activeandroid.Cache; +import com.activeandroid.query.Delete; +import com.activeandroid.query.From; +import com.activeandroid.query.Select; +import com.activeandroid.util.SQLiteUtils; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Repetition; +import org.isoron.uhabits.models.Score; +import org.isoron.uhabits.models.ScoreList; +import org.isoron.uhabits.utils.DateUtils; + +import java.util.List; + +/** + * Implementation of a ScoreList that is backed by SQLite. + */ +public class SQLiteScoreList extends ScoreList +{ + /** + * Constructs a new ScoreList associated with the given habit. + * + * @param habit the habit this list should be associated with + */ + public SQLiteScoreList(@NonNull Habit habit) + { + super(habit); + } + + @Override + public int getValue(long timestamp) + { + computeAll(); + String[] args = {habit.getId().toString(), Long.toString(timestamp)}; + return SQLiteUtils.intQuery( + "select score from Score where habit = ? and timestamp = ?", args); + } + + @Override + public void invalidateNewerThan(long timestamp) + { + new Delete() + .from(ScoreRecord.class) + .where("habit = ?", habit.getId()) + .and("timestamp >= ?", timestamp) + .execute(); + } + + @Nullable + @Override + protected Score getNewestComputed() + { + ScoreRecord record = select().limit(1).executeSingle(); + return record.toScore(); + } + + @Override + @Nullable + protected Score get(long timestamp) + { + Repetition oldestRep = habit.getRepetitions().getOldest(); + if (oldestRep == null) return null; + compute(oldestRep.getTimestamp(), timestamp); + + ScoreRecord record = + select().where("timestamp = ?", timestamp).executeSingle(); + + return record.toScore(); + } + + @Override + @NonNull + protected int[] getValues(long from, long to, long divisor) + { + compute(from, to); + + divisor *= DateUtils.millisecondsInOneDay; + Long offset = to + divisor; + + String query = + "select ((timestamp - ?) / ?) as time, avg(score) from Score " + + "where habit = ? and timestamp >= ? and timestamp <= ? " + + "group by time order by time desc"; + + String params[] = { + offset.toString(), + Long.toString(divisor), + habit.getId().toString(), + Long.toString(from), + Long.toString(to) + }; + + SQLiteDatabase db = Cache.openDatabase(); + Cursor cursor = db.rawQuery(query, params); + + if (!cursor.moveToFirst()) return new int[0]; + + int k = 0; + int[] scores = new int[cursor.getCount()]; + + do + { + scores[k++] = (int) cursor.getFloat(1); + } while (cursor.moveToNext()); + + cursor.close(); + return scores; + } + + @Override + protected void add(List scores) + { + String query = + "insert into Score(habit, timestamp, score) values (?,?,?)"; + + SQLiteDatabase db = Cache.openDatabase(); + db.beginTransaction(); + + try + { + SQLiteStatement statement = db.compileStatement(query); + + for (Score s : scores) + { + statement.bindLong(1, habit.getId()); + statement.bindLong(2, s.getTimestamp()); + statement.bindLong(3, s.getValue()); + statement.execute(); + } + + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + } + + protected From select() + { + return new Select() + .from(ScoreRecord.class) + .where("habit = ?", habit.getId()) + .orderBy("timestamp desc"); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java new file mode 100644 index 000000000..e44daa300 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteStreakList.java @@ -0,0 +1,115 @@ +/* + * 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.models.sqlite; + +import com.activeandroid.query.Delete; +import com.activeandroid.query.Select; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Streak; +import org.isoron.uhabits.models.StreakList; +import org.isoron.uhabits.utils.DatabaseUtils; +import org.isoron.uhabits.utils.DateUtils; + +import java.util.LinkedList; +import java.util.List; + +/** + * Implementation of a StreakList that is backed by SQLite. + */ +public class SQLiteStreakList extends StreakList +{ + public SQLiteStreakList(Habit habit) + { + super(habit); + } + + @Override + public List getAll() + { + rebuild(); + List records = new Select() + .from(StreakRecord.class) + .where("habit = ?", habit.getId()) + .orderBy("end desc") + .execute(); + + return recordsToStreaks(records); + } + + @Override + public Streak getNewestComputed() + { + rebuild(); + return getNewestRecord().toStreak(); + } + + @Override + public void invalidateNewerThan(long timestamp) + { + new Delete() + .from(StreakRecord.class) + .where("habit = ?", habit.getId()) + .and("end >= ?", timestamp - DateUtils.millisecondsInOneDay) + .execute(); + + observable.notifyListeners(); + } + + private StreakRecord getNewestRecord() + { + return new Select() + .from(StreakRecord.class) + .where("habit = ?", habit.getId()) + .orderBy("end desc") + .limit(1) + .executeSingle(); + } + + @Override + protected void insert(List streaks) + { + DatabaseUtils.executeAsTransaction(() -> { + for (Streak streak : streaks) + { + StreakRecord record = new StreakRecord(); + record.copyFrom(streak); + record.save(); + } + }); + } + + private List recordsToStreaks(List records) + { + LinkedList streaks = new LinkedList<>(); + + for (StreakRecord record : records) + streaks.add(record.toStreak()); + + return streaks; + } + + @Override + protected void removeNewestComputed() + { + StreakRecord newestStreak = getNewestRecord(); + if (newestStreak != null) newestStreak.delete(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/ScoreRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/ScoreRecord.java new file mode 100644 index 000000000..2efc2e21e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/ScoreRecord.java @@ -0,0 +1,65 @@ +/* + * 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.models.sqlite; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Score; + +/** + * The SQLite database record corresponding to a Score. + */ +@Table(name = "Score") +public class ScoreRecord extends Model +{ + /** + * Habit to which this score belongs to. + */ + @Column(name = "habit") + public HabitRecord habit; + + /** + * Timestamp of the day to which this score applies. Time of day should be + * midnight (UTC). + */ + @Column(name = "timestamp") + public Long timestamp; + + /** + * Value of the score. + */ + @Column(name = "score") + public Integer score; + + /** + * Constructs and returns a {@link Score} based on this record's data. + * + * @return a {@link Score} with this record's data + */ + public Score toScore() + { + SQLiteHabitList habitList = SQLiteHabitList.getInstance(); + Habit h = habitList.getById(habit.getId()); + return new Score(h, timestamp, score); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/StreakRecord.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/StreakRecord.java new file mode 100644 index 000000000..fd4229073 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/StreakRecord.java @@ -0,0 +1,66 @@ +/* + * 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.models.sqlite; + +import com.activeandroid.Model; +import com.activeandroid.annotation.Column; +import com.activeandroid.annotation.Table; + +import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.Streak; + +/** + * The SQLite database record corresponding to a Streak. + */ +@Table(name = "Streak") +public class StreakRecord extends Model +{ + @Column(name = "habit") + public HabitRecord habit; + + @Column(name = "start") + public Long start; + + @Column(name = "end") + public Long end; + + @Column(name = "length") + public Long length; + + public static StreakRecord get(Long id) + { + return StreakRecord.load(StreakRecord.class, id); + } + + public void copyFrom(Streak streak) + { + habit = HabitRecord.get(streak.getHabit().getId()); + start = streak.getStart(); + end = streak.getEnd(); + length = streak.getLength(); + } + + public Streak toStreak() + { + SQLiteHabitList habitList = SQLiteHabitList.getInstance(); + Habit h = habitList.getById(habit.getId()); + return new Streak(h, start, end); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/package-info.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/package-info.java new file mode 100644 index 000000000..469894365 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +/** + * Provides SQLite implementations of the core models. + */ +package org.isoron.uhabits.models.sqlite; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/package-info.java b/app/src/main/java/org/isoron/uhabits/package-info.java new file mode 100644 index 000000000..b080842fc --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +/** + * Provides classes for the Loop Habit Tracker app. + */ +package org.isoron.uhabits; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/tasks/package-info.java b/app/src/main/java/org/isoron/uhabits/tasks/package-info.java new file mode 100644 index 000000000..cc837895e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/package-info.java @@ -0,0 +1,24 @@ +/* + * 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 . + */ + +/** + * Provides async tasks for useful operations such as {@link + * org.isoron.uhabits.tasks.ExportCSVTask}. + */ +package org.isoron.uhabits.tasks; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java b/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java index 8cd9d07e4..8dbb04681 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java +++ b/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java @@ -25,6 +25,8 @@ import android.support.annotation.NonNull; import android.view.WindowManager; import org.isoron.uhabits.BuildConfig; +import org.isoron.uhabits.HabitsApplication; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.FileUtils; @@ -38,13 +40,19 @@ import java.io.IOException; import java.io.InputStreamReader; import java.util.LinkedList; +import javax.inject.Inject; + public class BaseSystem { private Context context; + @Inject + HabitList habitList; + public BaseSystem(Context context) { this.context = context; + HabitsApplication.getComponent().inject(this); } public String getLogcat() throws IOException @@ -146,7 +154,7 @@ public class BaseSystem @Override protected void doInBackground() { - ReminderUtils.createReminderAlarms(context); + ReminderUtils.createReminderAlarms(context, habitList); } }.execute(); } diff --git a/app/src/main/java/org/isoron/uhabits/ui/about/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/about/package-info.java index bfdb10c80..483cc1927 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/about/package-info.java +++ b/app/src/main/java/org/isoron/uhabits/ui/about/package-info.java @@ -18,6 +18,6 @@ */ /** - * Contains classes for AboutActivity + * Provides activity that shows information about the app. */ package org.isoron.uhabits.ui.about; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java index 7dfb86e31..aecb2a5e4 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogFragment.java @@ -85,8 +85,8 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment if (position < 0 || position > 4) throw new IllegalArgumentException(); int freqNums[] = {1, 1, 2, 5, 3}; int freqDens[] = {1, 7, 7, 7, 7}; - modifiedHabit.freqNum = freqNums[position]; - modifiedHabit.freqDen = freqDens[position]; + modifiedHabit.setFreqNum(freqNums[position]); + modifiedHabit.setFreqDen(freqDens[position]); helper.populateFrequencyFields(modifiedHabit); } @@ -95,12 +95,12 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putInt("color", modifiedHabit.color); + outState.putInt("color", modifiedHabit.getColor()); if (modifiedHabit.hasReminder()) { - outState.putInt("reminderMin", modifiedHabit.reminderMin); - outState.putInt("reminderHour", modifiedHabit.reminderHour); - outState.putInt("reminderDays", modifiedHabit.reminderDays); + outState.putInt("reminderMin", modifiedHabit.getReminderMin()); + outState.putInt("reminderHour", modifiedHabit.getReminderHour()); + outState.putInt("reminderDays", modifiedHabit.getReminderDays()); } } @@ -123,8 +123,8 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment if (modifiedHabit.hasReminder()) { - defaultHour = modifiedHabit.reminderHour; - defaultMin = modifiedHabit.reminderMin; + defaultHour = modifiedHabit.getReminderHour(); + defaultMin = modifiedHabit.getReminderMin(); } showTimePicker(defaultHour, defaultMin); @@ -147,18 +147,19 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment WeekdayPickerDialog dialog = new WeekdayPickerDialog(); dialog.setListener(new OnWeekdaysPickedListener()); dialog.setSelectedDays( - DateUtils.unpackWeekdayList(modifiedHabit.reminderDays)); + DateUtils.unpackWeekdayList(modifiedHabit.getReminderDays())); dialog.show(getFragmentManager(), "weekdayPicker"); } protected void restoreSavedInstance(@Nullable Bundle bundle) { if (bundle == null) return; - modifiedHabit.color = bundle.getInt("color", modifiedHabit.color); - modifiedHabit.reminderMin = bundle.getInt("reminderMin", -1); - modifiedHabit.reminderHour = bundle.getInt("reminderHour", -1); - modifiedHabit.reminderDays = bundle.getInt("reminderDays", -1); - if (modifiedHabit.reminderMin < 0) modifiedHabit.clearReminder(); + modifiedHabit.setColor( + bundle.getInt("color", modifiedHabit.getColor())); + modifiedHabit.setReminderMin(bundle.getInt("reminderMin", -1)); + modifiedHabit.setReminderHour(bundle.getInt("reminderHour", -1)); + modifiedHabit.setReminderDays(bundle.getInt("reminderDays", -1)); + if (modifiedHabit.getReminderMin() < 0) modifiedHabit.clearReminder(); } protected abstract void saveHabit(); @@ -167,7 +168,7 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment void showColorPicker() { int androidColor = - ColorUtils.getColor(getContext(), modifiedHabit.color); + ColorUtils.getColor(getContext(), modifiedHabit.getColor()); ColorPickerDialog picker = ColorPickerDialog.newInstance(R.string.color_picker_default_title, @@ -196,7 +197,7 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment int paletteColor = ColorUtils.colorToPaletteIndex(getActivity(), androidColor); prefs.setDefaultHabitColor(paletteColor); - modifiedHabit.color = paletteColor; + modifiedHabit.setColor(paletteColor); helper.populateColor(paletteColor); } } @@ -214,9 +215,9 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment @Override public void onTimeSet(RadialPickerLayout view, int hour, int minute) { - modifiedHabit.reminderHour = hour; - modifiedHabit.reminderMin = minute; - modifiedHabit.reminderDays = DateUtils.ALL_WEEK_DAYS; + modifiedHabit.setReminderHour(hour); + modifiedHabit.setReminderMin(minute); + modifiedHabit.setReminderDays(DateUtils.ALL_WEEK_DAYS); helper.populateReminderFields(modifiedHabit); } } @@ -229,8 +230,8 @@ public abstract class BaseDialogFragment extends AppCompatDialogFragment { if (isSelectionEmpty(selectedDays)) Arrays.fill(selectedDays, true); - modifiedHabit.reminderDays = - DateUtils.packWeekdayList(selectedDays); + modifiedHabit.setReminderDays( + DateUtils.packWeekdayList(selectedDays)); helper.populateReminderFields(modifiedHabit); } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogHelper.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogHelper.java index a9f518b48..993ba6839 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogHelper.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/BaseDialogHelper.java @@ -73,12 +73,12 @@ public class BaseDialogHelper void parseFormIntoHabit(Habit habit) { - habit.name = tvName.getText().toString().trim(); - habit.description = tvDescription.getText().toString().trim(); + habit.setName(tvName.getText().toString().trim()); + habit.setDescription(tvDescription.getText().toString().trim()); String freqNum = tvFreqNum.getText().toString(); String freqDen = tvFreqDen.getText().toString(); - if (!freqNum.isEmpty()) habit.freqNum = Integer.parseInt(freqNum); - if (!freqDen.isEmpty()) habit.freqDen = Integer.parseInt(freqDen); + if (!freqNum.isEmpty()) habit.setFreqNum(Integer.parseInt(freqNum)); + if (!freqDen.isEmpty()) habit.setFreqDen(Integer.parseInt(freqDen)); } void populateColor(int paletteColor) @@ -89,10 +89,11 @@ public class BaseDialogHelper protected void populateForm(final Habit habit) { - if (habit.name != null) tvName.setText(habit.name); - if (habit.description != null) tvDescription.setText(habit.description); + if (habit.getName() != null) tvName.setText(habit.getName()); + if (habit.getDescription() != null) tvDescription.setText( + habit.getDescription()); - populateColor(habit.color); + populateColor(habit.getColor()); populateFrequencyFields(habit); populateReminderFields(habit); } @@ -102,15 +103,15 @@ public class BaseDialogHelper { int quickSelectPosition = -1; - if (habit.freqNum.equals(habit.freqDen)) quickSelectPosition = 0; + if (habit.getFreqNum().equals(habit.getFreqDen())) quickSelectPosition = 0; - else if (habit.freqNum == 1 && habit.freqDen == 7) + else if (habit.getFreqNum() == 1 && habit.getFreqDen() == 7) quickSelectPosition = 1; - else if (habit.freqNum == 2 && habit.freqDen == 7) + else if (habit.getFreqNum() == 2 && habit.getFreqDen() == 7) quickSelectPosition = 2; - else if (habit.freqNum == 5 && habit.freqDen == 7) + else if (habit.getFreqNum() == 5 && habit.getFreqDen() == 7) quickSelectPosition = 3; if (quickSelectPosition >= 0) @@ -118,8 +119,8 @@ public class BaseDialogHelper else showCustomFrequency(); - tvFreqNum.setText(habit.freqNum.toString()); - tvFreqDen.setText(habit.freqDen.toString()); + tvFreqNum.setText(habit.getFreqNum().toString()); + tvFreqDen.setText(habit.getFreqDen().toString()); } @SuppressWarnings("ConstantConditions") @@ -133,12 +134,13 @@ public class BaseDialogHelper } String time = - DateUtils.formatTime(frag.getContext(), habit.reminderHour, - habit.reminderMin); + DateUtils.formatTime(frag.getContext(), habit.getReminderHour(), + habit.getReminderMin()); tvReminderTime.setText(time); llReminderDays.setVisibility(View.VISIBLE); - boolean weekdays[] = DateUtils.unpackWeekdayList(habit.reminderDays); + boolean weekdays[] = DateUtils.unpackWeekdayList( + habit.getReminderDays()); tvReminderDays.setText( DateUtils.formatWeekdayList(frag.getContext(), weekdays)); } @@ -161,21 +163,21 @@ public class BaseDialogHelper { Boolean valid = true; - if (habit.name.length() == 0) + if (habit.getName().length() == 0) { tvName.setError( frag.getString(R.string.validation_name_should_not_be_blank)); valid = false; } - if (habit.freqNum <= 0) + if (habit.getFreqNum() <= 0) { tvFreqNum.setError( frag.getString(R.string.validation_number_should_be_positive)); valid = false; } - if (habit.freqNum > habit.freqDen) + if (habit.getFreqNum() > habit.getFreqDen()) { tvFreqNum.setError( frag.getString(R.string.validation_at_most_one_rep_per_day)); diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/CreateHabitDialogFragment.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/CreateHabitDialogFragment.java index b63a95f24..777c32f7c 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/CreateHabitDialogFragment.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/CreateHabitDialogFragment.java @@ -19,6 +19,7 @@ package org.isoron.uhabits.ui.habits.edit; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.CreateHabitCommand; @@ -36,9 +37,10 @@ public class CreateHabitDialogFragment extends BaseDialogFragment protected void initializeHabits() { modifiedHabit = new Habit(); - modifiedHabit.freqNum = 1; - modifiedHabit.freqDen = 1; - modifiedHabit.color = prefs.getDefaultHabitColor(modifiedHabit.color); + modifiedHabit.setFreqNum(1); + modifiedHabit.setFreqDen(1); + modifiedHabit.setColor( + prefs.getDefaultHabitColor(modifiedHabit.getColor())); } protected void saveHabit() diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java index 7adf8f3e8..2541100d6 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/EditHabitDialogFragment.java @@ -21,13 +21,20 @@ package org.isoron.uhabits.ui.habits.edit; import android.os.Bundle; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.EditHabitCommand; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; + +import javax.inject.Inject; public class EditHabitDialogFragment extends BaseDialogFragment { + @Inject + HabitList habitList; + public static EditHabitDialogFragment newInstance(long habitId) { EditHabitDialogFragment frag = new EditHabitDialogFragment(); @@ -46,14 +53,18 @@ public class EditHabitDialogFragment extends BaseDialogFragment @Override protected void initializeHabits() { + HabitsApplication.getComponent().inject(this); + Long habitId = (Long) getArguments().get("habitId"); if (habitId == null) throw new IllegalArgumentException("habitId must be specified"); - originalHabit = Habit.get(habitId); - modifiedHabit = new Habit(originalHabit); + originalHabit = habitList.getById(habitId); + modifiedHabit = new Habit(); + modifiedHabit.copyFrom(originalHabit); } + @Override protected void saveHabit() { Command command = new EditHabitCommand(originalHabit, modifiedHabit); diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/HistoryEditorDialog.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/HistoryEditorDialog.java index f1bd5c1f8..bdf08d88d 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/HistoryEditorDialog.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/HistoryEditorDialog.java @@ -27,10 +27,14 @@ import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatDialogFragment; import android.util.DisplayMetrics; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.views.HabitHistoryView; +import org.isoron.uhabits.ui.habits.show.views.HabitHistoryView; + +import javax.inject.Inject; public class HistoryEditorDialog extends AppCompatDialogFragment implements DialogInterface.OnClickListener @@ -41,16 +45,20 @@ public class HistoryEditorDialog extends AppCompatDialogFragment HabitHistoryView historyView; + @Inject + HabitList habitList; + @Override public Dialog onCreateDialog(Bundle savedInstanceState) { Context context = getActivity(); + HabitsApplication.getComponent().inject(this); historyView = new HabitHistoryView(context, null); if (savedInstanceState != null) { long id = savedInstanceState.getLong("habit", -1); - if (id > 0) this.habit = Habit.get(id); + if (id > 0) this.habit = habitList.getById(id); } int padding = diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/edit/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/package-info.java new file mode 100644 index 000000000..b5d29a4f4 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/edit/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +/** + * Provides dialogs for editing habits and related classes. + */ +package org.isoron.uhabits.ui.habits.edit; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java index a111df442..9ca8d433e 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java @@ -21,25 +21,33 @@ package org.isoron.uhabits.ui.habits.list; import android.os.Bundle; +import org.isoron.uhabits.HabitsApplication; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.ui.BaseActivity; import org.isoron.uhabits.ui.BaseSystem; +import javax.inject.Inject; + /** * Activity that allows the user to see and modify the list of habits. */ public class ListHabitsActivity extends BaseActivity { + @Inject + HabitList habitList; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + HabitsApplication.getComponent().inject(this); + BaseSystem system = new BaseSystem(this); ListHabitsScreen screen = new ListHabitsScreen(this); ListHabitsController controller = - new ListHabitsController(screen, system); + new ListHabitsController(screen, system, habitList); screen.setController(controller); - setScreen(screen); controller.onStartup(); } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java index b5c91775e..72c4a867b 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java @@ -26,6 +26,7 @@ import org.isoron.uhabits.R; import org.isoron.uhabits.commands.CommandRunner; import org.isoron.uhabits.commands.ToggleRepetitionCommand; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.tasks.ExportCSVTask; import org.isoron.uhabits.tasks.ExportDBTask; import org.isoron.uhabits.tasks.ImportDataTask; @@ -48,6 +49,9 @@ public class ListHabitsController @NonNull private final BaseSystem system; + @NonNull + private final HabitList habitList; + @Inject Preferences prefs; @@ -55,17 +59,19 @@ public class ListHabitsController CommandRunner commandRunner; public ListHabitsController(@NonNull ListHabitsScreen screen, - @NonNull BaseSystem system) + @NonNull BaseSystem system, + @NonNull HabitList habitList) { this.screen = screen; this.system = system; + this.habitList = habitList; HabitsApplication.getComponent().inject(this); } public void onExportCSV() { ExportCSVTask task = - new ExportCSVTask(Habit.getAll(true), screen.getProgressBar()); + new ExportCSVTask(habitList.getAll(true), screen.getProgressBar()); task.setListener(filename -> { if (filename != null) screen.showSendFileScreen(filename); else screen.showMessage(R.string.could_not_export); @@ -92,7 +98,7 @@ public class ListHabitsController @Override public void onHabitReorder(@NonNull Habit from, @NonNull Habit to) { - Habit.reorder(from, to); + habitList.reorder(from, to); } public void onImportData(File file) @@ -133,7 +139,8 @@ public class ListHabitsController try { system.dumpBugReportToFile(); - } catch (IOException e) + } + catch (IOException e) { // ignored } @@ -146,7 +153,8 @@ public class ListHabitsController String to = "dev@loophabits.org"; String subject = "Bug Report - Loop Habit Tracker"; screen.showSendEmailScreen(log, to, subject); - } catch (IOException e) + } + catch (IOException e) { e.printStackTrace(); screen.showMessage(R.string.bug_report_failed); diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java index 760233e37..b617164d2 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java @@ -115,7 +115,7 @@ public class ListHabitsScreen extends BaseScreen public void showColorPicker(Habit habit, OnColorSelectedListener callback) { - int color = ColorUtils.getColor(activity, habit.color); + int color = ColorUtils.getColor(activity, habit.getColor()); ColorPickerDialog picker = ColorPickerDialog.newInstance(R.string.color_picker_default_title, diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/package-info.java index d0650ac2d..46e9e09ce 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/package-info.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/package-info.java @@ -18,6 +18,6 @@ */ /** - * Contains controllers that are specific for ListHabitsActivity + * Provides controllers that are specific for {@link org.isoron.uhabits.ui.habits.list.ListHabitsActivity}. */ package org.isoron.uhabits.ui.habits.list.controllers; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java index 6aab91a27..306678390 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java @@ -37,9 +37,10 @@ import java.util.List; import javax.inject.Inject; /** - * Provides data that backs a {@link HabitCardListView}. The data if fetched and - * cached by a {@link HabitCardListCache}. This adapter also holds a list of - * items that have been selected. + * Provides data that backs a {@link HabitCardListView}. + *

+ * The data if fetched and cached by a {@link HabitCardListCache}. This adapter + * also holds a list of items that have been selected. */ public class HabitCardListAdapter extends BaseAdapter implements HabitCardListCache.Listener @@ -200,6 +201,12 @@ public class HabitCardListAdapter extends BaseAdapter this.listView = listView; } + public void setShowArchived(boolean showArchived) + { + cache.setIncludeArchived(showArchived); + cache.refreshAllHabits(true); + } + /** * Selects or deselects the item at a given position. * @@ -213,10 +220,4 @@ public class HabitCardListAdapter extends BaseAdapter else selected.remove(h); notifyDataSetChanged(); } - - public void setShowArchived(boolean showArchived) - { - cache.setIncludeArchived(showArchived); - cache.refreshAllHabits(true); - } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java index 0fb5711b1..874b01e46 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java @@ -26,6 +26,7 @@ import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.CommandRunner; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.utils.DateUtils; @@ -63,6 +64,9 @@ public class HabitCardListCache implements CommandRunner.Listener @Inject CommandRunner commandRunner; + @Inject + HabitList allHabits; + public HabitCardListCache() { data = new CacheData(); @@ -148,7 +152,7 @@ public class HabitCardListCache implements CommandRunner.Listener data.habitsList.remove(from); data.habitsList.add(to, fromHabit); - Habit.reorder(fromHabit, toHabit); + allHabits.reorder(fromHabit, toHabit); } public void setCheckmarkCount(int checkmarkCount) @@ -227,7 +231,7 @@ public class HabitCardListCache implements CommandRunner.Listener public void fetchHabits() { - habitsList = Habit.getAll(includeArchived); + habitsList = allHabits.getAll(includeArchived); for (Habit h : habitsList) habits.put(h.getId(), h); } @@ -272,9 +276,9 @@ public class HabitCardListCache implements CommandRunner.Listener if (isCancelled()) return; Long id = h.getId(); - newData.scores.put(id, h.scores.getTodayValue()); + newData.scores.put(id, h.getScores().getTodayValue()); newData.checkmarks.put(id, - h.checkmarks.getValues(dateFrom, dateTo)); + h.getCheckmarks().getValues(dateFrom, dateTo)); publishProgress(current++, newData.habits.size()); } @@ -316,12 +320,12 @@ public class HabitCardListCache implements CommandRunner.Listener long dateFrom = dateTo - (checkmarkCount - 1) * DateUtils.millisecondsInOneDay; - Habit h = Habit.get(id); + Habit h = allHabits.getById(id); if (h == null) return; data.habits.put(id, h); - data.scores.put(id, h.scores.getTodayValue()); - data.checkmarks.put(id, h.checkmarks.getValues(dateFrom, dateTo)); + data.scores.put(id, h.getScores().getTodayValue()); + data.checkmarks.put(id, h.getCheckmarks().getValues(dateFrom, dateTo)); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/package-info.java index 06a063daf..06b94b85b 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/package-info.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/package-info.java @@ -18,6 +18,6 @@ */ /** - * Contains model classes that are specific for ListHabitsActivity + * Provides models that are specific for {@link org.isoron.uhabits.ui.habits.list.ListHabitsActivity}. */ package org.isoron.uhabits.ui.habits.list.model; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/package-info.java index 5ae316016..2d3cc559b 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/package-info.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/package-info.java @@ -18,6 +18,6 @@ */ /** - * Contains classes for ListHabitsActivity. + * Provides acitivity for listing habits and related classes. */ package org.isoron.uhabits.ui.habits.list; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardView.java index e8e758929..2c12dc181 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/views/HabitCardView.java @@ -32,8 +32,8 @@ import android.widget.TextView; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Score; +import org.isoron.uhabits.ui.habits.show.views.RingView; import org.isoron.uhabits.utils.ColorUtils; -import org.isoron.uhabits.views.RingView; import java.util.Random; @@ -98,7 +98,7 @@ public class HabitCardView extends FrameLayout this.habit = habit; int color = getActiveColor(habit); - label.setText(habit.name); + label.setText(habit.getName()); label.setTextColor(color); scoreRing.setColor(color); checkmarkPanel.setColor(color); @@ -136,7 +136,7 @@ public class HabitCardView extends FrameLayout { int mediumContrastColor = getStyledColor(context, R.attr.mediumContrastTextColor); - int activeColor = ColorUtils.getColor(context, habit.color); + int activeColor = ColorUtils.getColor(context, habit.getColor()); if (habit.isArchived()) activeColor = mediumContrastColor; return activeColor; @@ -173,7 +173,7 @@ public class HabitCardView extends FrameLayout }; Random rand = new Random(); - int color = ColorUtils.CSV_PALETTE[rand.nextInt(10)]; + int color = ColorUtils.getAndroidTestColor(rand.nextInt(10)); int[] values = { rand.nextInt(3), rand.nextInt(3), diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java index 929e801c5..365495876 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java @@ -24,10 +24,14 @@ import android.net.Uri; import android.os.Bundle; import android.support.v7.app.ActionBar; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.ui.BaseActivity; +import javax.inject.Inject; + /** * Activity that allows the user to see more information about a single habit. * Shows all the metadata for the habit, in addition to several charts. @@ -36,13 +40,17 @@ public class ShowHabitActivity extends BaseActivity { private Habit habit; + @Inject + HabitList habitList; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + HabitsApplication.getComponent().inject(this); Uri data = getIntent().getData(); - habit = Habit.get(ContentUris.parseId(data)); + habit = habitList.getById(ContentUris.parseId(data)); setContentView(R.layout.show_habit_activity); // setupSupportActionBar(true); @@ -56,7 +64,7 @@ public class ShowHabitActivity extends BaseActivity ActionBar actionBar = getSupportActionBar(); if (actionBar == null) return; - actionBar.setTitle(habit.name); + actionBar.setTitle(habit.getName()); // setupActionBarColor(ColorUtils.getColor(this, habit.color)); } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitFragment.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitFragment.java index 3ba99c833..ac2c611fa 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitFragment.java @@ -39,11 +39,11 @@ import org.isoron.uhabits.ui.habits.edit.EditHabitDialogFragment; import org.isoron.uhabits.ui.habits.edit.HistoryEditorDialog; import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.InterfaceUtils; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitFrequencyView; -import org.isoron.uhabits.views.HabitHistoryView; -import org.isoron.uhabits.views.HabitScoreView; -import org.isoron.uhabits.views.HabitStreakView; +import org.isoron.uhabits.ui.habits.show.views.HabitDataView; +import org.isoron.uhabits.ui.habits.show.views.HabitFrequencyView; +import org.isoron.uhabits.ui.habits.show.views.HabitHistoryView; +import org.isoron.uhabits.ui.habits.show.views.HabitScoreView; +import org.isoron.uhabits.ui.habits.show.views.HabitStreakView; import java.util.LinkedList; import java.util.List; @@ -212,13 +212,13 @@ public class ShowHabitFragment extends Fragment public void onStart() { super.onStart(); - habit.observable.addListener(this); + habit.getObservable().addListener(this); } @Override public void onPause() { - habit.observable.removeListener(this); + habit.getObservable().removeListener(this); super.onPause(); } @@ -234,9 +234,9 @@ public class ShowHabitFragment extends Fragment long lastMonth = today - 30 * DateUtils.millisecondsInOneDay; long lastYear = today - 365 * DateUtils.millisecondsInOneDay; - todayScore = (float) habit.scores.getTodayValue(); - lastMonthScore = (float) habit.scores.getValue(lastMonth); - lastYearScore = (float) habit.scores.getValue(lastYear); + todayScore = (float) habit.getScores().getTodayValue(); + lastMonthScore = (float) habit.getScores().getValue(lastMonth); + lastYearScore = (float) habit.getScores().getValue(lastYear); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitHelper.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitHelper.java index 6a76d4f32..dabc9a3c9 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitHelper.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitHelper.java @@ -25,10 +25,10 @@ import android.widget.TextView; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Score; +import org.isoron.uhabits.ui.habits.show.views.RingView; import org.isoron.uhabits.utils.ColorUtils; import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.InterfaceUtils; -import org.isoron.uhabits.views.RingView; public class ShowHabitHelper { @@ -44,8 +44,8 @@ public class ShowHabitHelper if (fragment.habit == null) return ""; Resources resources = fragment.getResources(); - Integer freqNum = fragment.habit.freqNum; - Integer freqDen = fragment.habit.freqDen; + Integer freqNum = fragment.habit.getFreqNum(); + Integer freqDen = fragment.habit.getFreqDen(); if (freqNum.equals(freqDen)) return resources.getString(R.string.every_day); @@ -76,7 +76,8 @@ public class ShowHabitHelper RingView scoreRing = (RingView) view.findViewById(R.id.scoreRing); int androidColor = - ColorUtils.getColor(fragment.getActivity(), fragment.habit.color); + ColorUtils.getColor(fragment.getActivity(), + fragment.habit.getColor()); scoreRing.setColor(androidColor); scoreRing.setPercentage(todayPercentage); @@ -109,13 +110,14 @@ public class ShowHabitHelper TextView questionLabel = (TextView) view.findViewById(R.id.questionLabel); questionLabel.setTextColor(fragment.activeColor); - questionLabel.setText(fragment.habit.description); + questionLabel.setText(fragment.habit.getDescription()); TextView reminderLabel = (TextView) view.findViewById(R.id.reminderLabel); if (fragment.habit.hasReminder()) reminderLabel.setText( DateUtils.formatTime(fragment.getActivity(), - fragment.habit.reminderHour, fragment.habit.reminderMin)); + fragment.habit.getReminderHour(), + fragment.habit.getReminderMin())); else reminderLabel.setText( fragment.getResources().getString(R.string.reminder_off)); @@ -123,7 +125,7 @@ public class ShowHabitHelper (TextView) view.findViewById(R.id.frequencyLabel); frequencyLabel.setText(getFreqText()); - if (fragment.habit.description.isEmpty()) + if (fragment.habit.getDescription().isEmpty()) questionLabel.setVisibility(View.GONE); } @@ -143,14 +145,15 @@ public class ShowHabitHelper TextView textView = (TextView) view.findViewById(viewId); int androidColor = - ColorUtils.getColor(fragment.activity, fragment.habit.color); + ColorUtils.getColor(fragment.activity, fragment.habit.getColor()); textView.setTextColor(androidColor); } void updateColors() { fragment.activeColor = - ColorUtils.getColor(fragment.getContext(), fragment.habit.color); + ColorUtils.getColor(fragment.getContext(), + fragment.habit.getColor()); fragment.inactiveColor = InterfaceUtils.getStyledColor(fragment.getContext(), R.attr.mediumContrastTextColor); diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/package-info.java new file mode 100644 index 000000000..a3146ed7a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/package-info.java @@ -0,0 +1,24 @@ +/* + * 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 . + */ + +/** + * Provides activity that display detailed habit information and related + * classes. + */ +package org.isoron.uhabits.ui.habits.show; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitDataView.java similarity index 95% rename from app/src/main/java/org/isoron/uhabits/views/HabitDataView.java rename to app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitDataView.java index b1e239d5e..d9163f0e5 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitDataView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitDataView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.habits.show.views; import org.isoron.uhabits.models.Habit; diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitFrequencyView.java similarity index 96% rename from app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java rename to app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitFrequencyView.java index 212aaa570..dd8fbdb22 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitFrequencyView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.habits.show.views; import android.content.Context; import android.graphics.Canvas; @@ -26,12 +26,12 @@ import android.graphics.RectF; import android.util.AttributeSet; import org.isoron.uhabits.R; +import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.ModelObservable; import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.utils.ColorUtils; import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.InterfaceUtils; -import org.isoron.uhabits.models.Habit; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -101,7 +101,8 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV { if(habit != null) { - this.primaryColor = ColorUtils.getColor(getContext(), habit.color); + this.primaryColor = ColorUtils.getColor(getContext(), + habit.getColor()); } textColor = InterfaceUtils.getStyledColor(getContext(), R.attr.mediumContrastTextColor); @@ -177,7 +178,7 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV if(isInEditMode()) generateRandomData(); else if(habit != null) { - frequency = habit.repetitions.getWeekdayFrequency(); + frequency = habit.getRepetitions().getWeekdayFrequency(); createColors(); } @@ -314,15 +315,15 @@ public class HabitFrequencyView extends ScrollableDataView implements HabitDataV refreshData(); } }.execute(); - habit.observable.addListener(this); - habit.checkmarks.observable.addListener(this); + habit.getObservable().addListener(this); + habit.getCheckmarks().observable.addListener(this); } @Override protected void onDetachedFromWindow() { - habit.checkmarks.observable.removeListener(this); - habit.observable.removeListener(this); + habit.getCheckmarks().observable.removeListener(this); + habit.getObservable().removeListener(this); super.onDetachedFromWindow(); } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitHistoryView.java similarity index 97% rename from app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java rename to app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitHistoryView.java index d390fdb42..b855b8e79 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitHistoryView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.habits.show.views; import android.content.Context; import android.graphics.Canvas; @@ -167,7 +167,8 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie private void createColors() { if(habit != null) - this.primaryColor = ColorUtils.getColor(getContext(), habit.color); + this.primaryColor = ColorUtils.getColor(getContext(), + habit.getColor()); if(isBackgroundTransparent) primaryColor = ColorUtils.setMinValue(primaryColor, 0.75f); @@ -216,7 +217,7 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie else { if(habit == null) return; - checkmarks = habit.checkmarks.getAllValues(); + checkmarks = habit.getCheckmarks().getAllValues(); createColors(); } @@ -424,15 +425,15 @@ public class HabitHistoryView extends ScrollableDataView implements HabitDataVie refreshData(); } }.execute(); - habit.observable.addListener(this); - habit.checkmarks.observable.addListener(this); + habit.getObservable().addListener(this); + habit.getCheckmarks().observable.addListener(this); } @Override protected void onDetachedFromWindow() { - habit.checkmarks.observable.removeListener(this); - habit.observable.removeListener(this); + habit.getCheckmarks().observable.removeListener(this); + habit.getObservable().removeListener(this); super.onDetachedFromWindow(); } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitScoreView.java similarity index 80% rename from app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java rename to app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitScoreView.java index 5a336d101..0b7cf2611 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitScoreView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.habits.show.views; import android.content.Context; import android.graphics.Bitmap; @@ -45,47 +45,69 @@ import java.util.GregorianCalendar; import java.util.Random; public class HabitScoreView extends ScrollableDataView - implements HabitDataView, ModelObservable.Listener + implements HabitDataView, ModelObservable.Listener { public static final PorterDuffXfermode XFERMODE_CLEAR = - new PorterDuffXfermode(PorterDuff.Mode.CLEAR); + new PorterDuffXfermode(PorterDuff.Mode.CLEAR); + public static final PorterDuffXfermode XFERMODE_SRC = - new PorterDuffXfermode(PorterDuff.Mode.SRC); + new PorterDuffXfermode(PorterDuff.Mode.SRC); - public static int DEFAULT_BUCKET_SIZES[] = { 1, 7, 31, 92, 365 }; + public static int DEFAULT_BUCKET_SIZES[] = {1, 7, 31, 92, 365}; private Paint pGrid; + private float em; + private Habit habit; private SimpleDateFormat dfMonth; + private SimpleDateFormat dfDay; + private SimpleDateFormat dfYear; private Paint pText, pGraph; + private RectF rect, prevRect; + private int baseSize; + private int paddingTop; private float columnWidth; + private int columnHeight; + private int nColumns; private int textColor; + private int gridColor; @Nullable private int[] scores; private int primaryColor; + private int bucketSize = 7; + private int footerHeight; + private int backgroundColor; private Bitmap drawingCache; + private Canvas cacheCanvas; + private boolean isTransparencyEnabled; + private int skipYear = 0; + + private String previousYearText; + + private String previousMonthText; + public HabitScoreView(Context context) { super(context); @@ -99,105 +121,20 @@ public class HabitScoreView extends ScrollableDataView init(); } - public void setHabit(Habit habit) - { - this.habit = habit; - createColors(); - } - - private void init() - { - createPaints(); - createColors(); - - dfYear = DateUtils.getDateFormat("yyyy"); - dfMonth = DateUtils.getDateFormat("MMM"); - dfDay = DateUtils.getDateFormat("d"); - - rect = new RectF(); - prevRect = new RectF(); - } - - private void createColors() - { - if(habit != null) - this.primaryColor = ColorUtils.getColor(getContext(), habit.color); - - textColor = InterfaceUtils.getStyledColor(getContext(), R.attr.mediumContrastTextColor); - gridColor = InterfaceUtils.getStyledColor(getContext(), R.attr.lowContrastTextColor); - backgroundColor = InterfaceUtils.getStyledColor(getContext(), R.attr.cardBackgroundColor); - } - - protected void createPaints() - { - pText = new Paint(); - pText.setAntiAlias(true); - - pGraph = new Paint(); - pGraph.setTextAlign(Paint.Align.CENTER); - pGraph.setAntiAlias(true); - - pGrid = new Paint(); - pGrid.setAntiAlias(true); - } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + public void onModelChange() { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); + refreshData(); } @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) - { - if(height < 9) height = 200; - - float maxTextSize = getResources().getDimension(R.dimen.tinyTextSize); - float textSize = height * 0.06f; - pText.setTextSize(Math.min(textSize, maxTextSize)); - em = pText.getFontSpacing(); - - footerHeight = (int)(3 * em); - paddingTop = (int) (em); - - baseSize = (height - footerHeight - paddingTop) / 8; - setScrollerBucketSize(baseSize); - - columnWidth = baseSize; - columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f); - columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); - - nColumns = (int) (width / columnWidth); - columnWidth = (float) width / nColumns; - - columnHeight = 8 * baseSize; - - float minStrokeWidth = InterfaceUtils.dpToPixels(getContext(), 1); - pGraph.setTextSize(baseSize * 0.5f); - pGraph.setStrokeWidth(baseSize * 0.1f); - pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f)); - - if(isTransparencyEnabled) - initCache(width, height); - } - - private void initCache(int width, int height) - { - if (drawingCache != null) drawingCache.recycle(); - drawingCache = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - cacheCanvas = new Canvas(drawingCache); - } - public void refreshData() { - if(isInEditMode()) - generateRandomData(); + if (isInEditMode()) generateRandomData(); else { if (habit == null) return; - scores = habit.scores.getAllValues(bucketSize); + scores = habit.getScores().getAllValues(bucketSize); createColors(); } @@ -209,96 +146,46 @@ public class HabitScoreView extends ScrollableDataView this.bucketSize = bucketSize; } - private void generateRandomData() + @Override + public void setHabit(Habit habit) { - Random random = new Random(); - scores = new int[100]; - scores[0] = Score.MAX_VALUE / 2; - - for(int i = 1; i < 100; i++) - { - 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_VALUE, scores[i])); - } + this.habit = habit; + createColors(); } - @Override - protected void onDraw(Canvas canvas) + public void setIsTransparencyEnabled(boolean enabled) { - super.onDraw(canvas); - Canvas activeCanvas; - - if(isTransparencyEnabled) - { - if(drawingCache == null) initCache(getWidth(), getHeight()); - - activeCanvas = cacheCanvas; - drawingCache.eraseColor(Color.TRANSPARENT); - } - else - { - activeCanvas = canvas; - } - - if (habit == null || scores == null) return; - - rect.set(0, 0, nColumns * columnWidth, columnHeight); - rect.offset(0, paddingTop); - - drawGrid(activeCanvas, rect); - - pText.setColor(textColor); - pGraph.setColor(primaryColor); - prevRect.setEmpty(); - - previousMonthText = ""; - previousYearText = ""; - skipYear = 0; - - long currentDate = DateUtils.getStartOfToday(); - - for(int k = 0; k < nColumns + getDataOffset() - 1; k++) - currentDate -= bucketSize * DateUtils.millisecondsInOneDay; - - for (int k = 0; k < nColumns; k++) - { - int score = 0; - int offset = nColumns - k - 1 + getDataOffset(); - if(offset < scores.length) score = scores[offset]; - - double relativeScore = ((double) score) / Score.MAX_VALUE; - int height = (int) (columnHeight * relativeScore); - - rect.set(0, 0, baseSize, baseSize); - rect.offset(k * columnWidth + (columnWidth - baseSize) / 2, - paddingTop + columnHeight - height - baseSize / 2); - - if (!prevRect.isEmpty()) - { - drawLine(activeCanvas, prevRect, rect); - drawMarker(activeCanvas, prevRect); - } - - if (k == nColumns - 1) drawMarker(activeCanvas, rect); + this.isTransparencyEnabled = enabled; + createColors(); + requestLayout(); + } - prevRect.set(rect); - rect.set(0, 0, columnWidth, columnHeight); - rect.offset(k * columnWidth, paddingTop); + private void createColors() + { + if (habit != null) this.primaryColor = + ColorUtils.getColor(getContext(), habit.getColor()); + + textColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.mediumContrastTextColor); + gridColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.lowContrastTextColor); + backgroundColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.cardBackgroundColor); + } - drawFooter(activeCanvas, rect, currentDate); + protected void createPaints() + { + pText = new Paint(); + pText.setAntiAlias(true); - currentDate += bucketSize * DateUtils.millisecondsInOneDay; - } + pGraph = new Paint(); + pGraph.setTextAlign(Paint.Align.CENTER); + pGraph.setAntiAlias(true); - if(activeCanvas != canvas) - canvas.drawBitmap(drawingCache, 0, 0, null); + pGrid = new Paint(); + pGrid.setAntiAlias(true); } - private int skipYear = 0; - private String previousYearText; - private String previousMonthText; - private void drawFooter(Canvas canvas, RectF rect, long currentDate) { String yearText = dfYear.format(currentDate); @@ -311,29 +198,30 @@ public class HabitScoreView extends ScrollableDataView int year = calendar.get(Calendar.YEAR); boolean shouldPrintYear = true; - if(yearText.equals(previousYearText)) shouldPrintYear = false; - if(bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false; + if (yearText.equals(previousYearText)) shouldPrintYear = false; + if (bucketSize >= 365 && (year % 2) != 0) shouldPrintYear = false; - if(skipYear > 0) + if (skipYear > 0) { skipYear--; shouldPrintYear = false; } - if(shouldPrintYear) + if (shouldPrintYear) { previousYearText = yearText; previousMonthText = ""; pText.setTextAlign(Paint.Align.CENTER); - canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, pText); + canvas.drawText(yearText, rect.centerX(), rect.bottom + em * 2.2f, + pText); skipYear = 1; } - if(bucketSize < 365) + if (bucketSize < 365) { - if(!monthText.equals(previousMonthText)) + if (!monthText.equals(previousMonthText)) { previousMonthText = monthText; text = monthText; @@ -344,11 +232,11 @@ public class HabitScoreView extends ScrollableDataView } pText.setTextAlign(Paint.Align.CENTER); - canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f, pText); + canvas.drawText(text, rect.centerX(), rect.bottom + em * 1.2f, + pText); } } - private void drawGrid(Canvas canvas, RectF rGrid) { int nRows = 5; @@ -360,9 +248,10 @@ public class HabitScoreView extends ScrollableDataView for (int i = 0; i < nRows; i++) { - canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), rGrid.left + 0.5f * em, - rGrid.top + 1f * em, pText); - canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, pGrid); + canvas.drawText(String.format("%d%%", (100 - i * 100 / nRows)), + rGrid.left + 0.5f * em, rGrid.top + 1f * em, pText); + canvas.drawLine(rGrid.left, rGrid.top, rGrid.right, rGrid.top, + pGrid); rGrid.offset(0, rowHeight); } @@ -372,8 +261,8 @@ public class HabitScoreView extends ScrollableDataView private void drawLine(Canvas canvas, RectF rectFrom, RectF rectTo) { pGraph.setColor(primaryColor); - canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), rectTo.centerX(), rectTo.centerY(), - pGraph); + canvas.drawLine(rectFrom.centerX(), rectFrom.centerY(), + rectTo.centerX(), rectTo.centerY(), pGraph); } private void drawMarker(Canvas canvas, RectF rect) @@ -390,23 +279,36 @@ public class HabitScoreView extends ScrollableDataView setModeOrColor(pGraph, XFERMODE_CLEAR, backgroundColor); canvas.drawOval(rect, pGraph); - if(isTransparencyEnabled) - pGraph.setXfermode(XFERMODE_SRC); + if (isTransparencyEnabled) pGraph.setXfermode(XFERMODE_SRC); } - public void setIsTransparencyEnabled(boolean enabled) + private void generateRandomData() { - this.isTransparencyEnabled = enabled; - createColors(); - requestLayout(); + Random random = new Random(); + scores = new int[100]; + scores[0] = Score.MAX_VALUE / 2; + + for (int i = 1; i < 100; i++) + { + 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_VALUE, scores[i])); + } } - private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color) + private float getMaxDayWidth() { - if(isTransparencyEnabled) - p.setXfermode(mode); - else - p.setColor(color); + float maxDayWidth = 0; + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); + + for (int i = 0; i < 28; i++) + { + day.set(Calendar.DAY_OF_MONTH, i); + float monthWidth = pText.measureText(dfMonth.format(day.getTime())); + maxDayWidth = Math.max(maxDayWidth, monthWidth); + } + + return maxDayWidth; } private float getMaxMonthWidth() @@ -414,7 +316,7 @@ public class HabitScoreView extends ScrollableDataView float maxMonthWidth = 0; GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); - for(int i = 0; i < 12; i++) + for (int i = 0; i < 12; i++) { day.set(Calendar.MONTH, i); float monthWidth = pText.measureText(dfMonth.format(day.getTime())); @@ -424,19 +326,25 @@ public class HabitScoreView extends ScrollableDataView return maxMonthWidth; } - private float getMaxDayWidth() + private void init() { - float maxDayWidth = 0; - GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); + createPaints(); + createColors(); - for(int i = 0; i < 28; i++) - { - day.set(Calendar.DAY_OF_MONTH, i); - float monthWidth = pText.measureText(dfMonth.format(day.getTime())); - maxDayWidth = Math.max(maxDayWidth, monthWidth); - } + dfYear = DateUtils.getDateFormat("yyyy"); + dfMonth = DateUtils.getDateFormat("MMM"); + dfDay = DateUtils.getDateFormat("d"); - return maxDayWidth; + rect = new RectF(); + prevRect = new RectF(); + } + + private void initCache(int width, int height) + { + if (drawingCache != null) drawingCache.recycle(); + drawingCache = + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + cacheCanvas = new Canvas(drawingCache); } @Override @@ -451,21 +359,136 @@ public class HabitScoreView extends ScrollableDataView refreshData(); } }.execute(); - habit.observable.addListener(this); - habit.scores.observable.addListener(this); + habit.getObservable().addListener(this); + habit.getScores().getObservable().addListener(this); } @Override protected void onDetachedFromWindow() { - habit.scores.observable.removeListener(this); - habit.observable.removeListener(this); + habit.getScores().getObservable().removeListener(this); + habit.getObservable().removeListener(this); super.onDetachedFromWindow(); } @Override - public void onModelChange() + protected void onDraw(Canvas canvas) { - refreshData(); + super.onDraw(canvas); + Canvas activeCanvas; + + if (isTransparencyEnabled) + { + if (drawingCache == null) initCache(getWidth(), getHeight()); + + activeCanvas = cacheCanvas; + drawingCache.eraseColor(Color.TRANSPARENT); + } + else + { + activeCanvas = canvas; + } + + if (habit == null || scores == null) return; + + rect.set(0, 0, nColumns * columnWidth, columnHeight); + rect.offset(0, paddingTop); + + drawGrid(activeCanvas, rect); + + pText.setColor(textColor); + pGraph.setColor(primaryColor); + prevRect.setEmpty(); + + previousMonthText = ""; + previousYearText = ""; + skipYear = 0; + + long currentDate = DateUtils.getStartOfToday(); + + for (int k = 0; k < nColumns + getDataOffset() - 1; k++) + currentDate -= bucketSize * DateUtils.millisecondsInOneDay; + + for (int k = 0; k < nColumns; k++) + { + int score = 0; + int offset = nColumns - k - 1 + getDataOffset(); + if (offset < scores.length) score = scores[offset]; + + double relativeScore = ((double) score) / Score.MAX_VALUE; + int height = (int) (columnHeight * relativeScore); + + rect.set(0, 0, baseSize, baseSize); + rect.offset(k * columnWidth + (columnWidth - baseSize) / 2, + paddingTop + columnHeight - height - baseSize / 2); + + if (!prevRect.isEmpty()) + { + drawLine(activeCanvas, prevRect, rect); + drawMarker(activeCanvas, prevRect); + } + + if (k == nColumns - 1) drawMarker(activeCanvas, rect); + + prevRect.set(rect); + rect.set(0, 0, columnWidth, columnHeight); + rect.offset(k * columnWidth, paddingTop); + + drawFooter(activeCanvas, rect, currentDate); + + currentDate += bucketSize * DateUtils.millisecondsInOneDay; + } + + if (activeCanvas != canvas) canvas.drawBitmap(drawingCache, 0, 0, null); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int width, + int height, + int oldWidth, + int oldHeight) + { + if (height < 9) height = 200; + + float maxTextSize = getResources().getDimension(R.dimen.tinyTextSize); + float textSize = height * 0.06f; + pText.setTextSize(Math.min(textSize, maxTextSize)); + em = pText.getFontSpacing(); + + footerHeight = (int) (3 * em); + paddingTop = (int) (em); + + baseSize = (height - footerHeight - paddingTop) / 8; + setScrollerBucketSize(baseSize); + + columnWidth = baseSize; + columnWidth = Math.max(columnWidth, getMaxDayWidth() * 1.5f); + columnWidth = Math.max(columnWidth, getMaxMonthWidth() * 1.2f); + + nColumns = (int) (width / columnWidth); + columnWidth = (float) width / nColumns; + + columnHeight = 8 * baseSize; + + float minStrokeWidth = InterfaceUtils.dpToPixels(getContext(), 1); + pGraph.setTextSize(baseSize * 0.5f); + pGraph.setStrokeWidth(baseSize * 0.1f); + pGrid.setStrokeWidth(Math.min(minStrokeWidth, baseSize * 0.05f)); + + if (isTransparencyEnabled) initCache(width, height); + } + + private void setModeOrColor(Paint p, PorterDuffXfermode mode, int color) + { + if (isTransparencyEnabled) p.setXfermode(mode); + else p.setColor(color); } } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitStreakView.java similarity index 73% rename from app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java rename to app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitStreakView.java index 6019ef4a1..59d2bc78a 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitStreakView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.habits.show.views; import android.content.Context; import android.graphics.Canvas; @@ -28,12 +28,12 @@ import android.util.AttributeSet; import android.view.View; import org.isoron.uhabits.R; +import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.ModelObservable; +import org.isoron.uhabits.models.Streak; import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.utils.ColorUtils; import org.isoron.uhabits.utils.InterfaceUtils; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Streak; import java.text.DateFormat; import java.util.Collections; @@ -41,29 +41,45 @@ import java.util.Date; import java.util.List; import java.util.TimeZone; -public class HabitStreakView extends View implements HabitDataView, ModelObservable.Listener +public class HabitStreakView extends View + implements HabitDataView, ModelObservable.Listener { private Habit habit; + private Paint paint; private long minLength; + private long maxLength; private int[] colors; + private RectF rect; + private int baseSize; + private int primaryColor; + private List streaks; private boolean isBackgroundTransparent; + private DateFormat dateFormat; + private int width; + private float em; + private float maxLabelWidth; + private float textMargin; + private boolean shouldShowLabels; + private int maxStreakCount; + private int textColor; + private int reverseTextColor; public HabitStreakView(Context context) @@ -79,55 +95,39 @@ public class HabitStreakView extends View implements HabitDataView, ModelObserva init(); } - public void setHabit(Habit habit) + @Override + public void onModelChange() { - this.habit = habit; - createColors(); + refreshData(); } - private void init() + @Override + public void refreshData() { - createPaints(); + if (habit == null) return; + streaks = habit.getStreaks().getBest(maxStreakCount); createColors(); - - streaks = Collections.emptyList(); - - dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); - dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - rect = new RectF(); - maxStreakCount = 10; - baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); + updateMaxMin(); + postInvalidate(); } @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + public void setHabit(Habit habit) { - int width = MeasureSpec.getSize(widthMeasureSpec); - int height = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(width, height); + this.habit = habit; + createColors(); } - @Override - protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) + public void setIsBackgroundTransparent(boolean isBackgroundTransparent) { - maxStreakCount = height / baseSize; - this.width = width; - - float minTextSize = getResources().getDimension(R.dimen.tinyTextSize); - float maxTextSize = getResources().getDimension(R.dimen.regularTextSize); - float textSize = baseSize * 0.5f; - - paint.setTextSize(Math.max(Math.min(textSize, maxTextSize), minTextSize)); - em = paint.getFontSpacing(); - textMargin = 0.5f * em; - - updateMaxMin(); + this.isBackgroundTransparent = isBackgroundTransparent; + createColors(); } private void createColors() { - if(habit != null) - this.primaryColor = ColorUtils.getColor(getContext(), habit.color); + if (habit != null) this.primaryColor = + ColorUtils.getColor(getContext(), habit.getColor()); int red = Color.red(primaryColor); int green = Color.green(primaryColor); @@ -137,9 +137,12 @@ public class HabitStreakView extends View implements HabitDataView, ModelObserva colors[3] = primaryColor; colors[2] = Color.argb(192, red, green, blue); colors[1] = Color.argb(96, red, green, blue); - colors[0] = InterfaceUtils.getStyledColor(getContext(), R.attr.lowContrastTextColor); - textColor = InterfaceUtils.getStyledColor(getContext(), R.attr.mediumContrastTextColor); - reverseTextColor = InterfaceUtils.getStyledColor(getContext(), R.attr.highContrastReverseTextColor); + colors[0] = InterfaceUtils.getStyledColor(getContext(), + R.attr.lowContrastTextColor); + textColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.mediumContrastTextColor); + reverseTextColor = InterfaceUtils.getStyledColor(getContext(), + R.attr.highContrastReverseTextColor); } protected void createPaints() @@ -149,63 +152,17 @@ public class HabitStreakView extends View implements HabitDataView, ModelObserva paint.setAntiAlias(true); } - public void refreshData() - { - if(habit == null) return; - streaks = habit.streaks.getAll(maxStreakCount); - createColors(); - updateMaxMin(); - postInvalidate(); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - if(streaks.size() == 0) return; - - rect.set(0, 0, width, baseSize); - - for(Streak s : streaks) - { - drawRow(canvas, s, rect); - rect.offset(0, baseSize); - } - } - - private void updateMaxMin() - { - maxLength = 0; - minLength = Long.MAX_VALUE; - shouldShowLabels = true; - - for (Streak s : streaks) - { - maxLength = Math.max(maxLength, s.length); - minLength = Math.min(minLength, s.length); - - float lw1 = paint.measureText(dateFormat.format(new Date(s.start))); - float lw2 = paint.measureText(dateFormat.format(new Date(s.end))); - maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)); - } - - if(width - 2 * maxLabelWidth < width * 0.25f) - { - maxLabelWidth = 0; - shouldShowLabels = false; - } - } - private void drawRow(Canvas canvas, Streak streak, RectF rect) { - if(maxLength == 0) return; + if (maxLength == 0) return; - float percentage = (float) streak.length / maxLength; + float percentage = (float) streak.getLength() / maxLength; float availableWidth = width - 2 * maxLabelWidth; - if(shouldShowLabels) availableWidth -= 2 * textMargin; + if (shouldShowLabels) availableWidth -= 2 * textMargin; float barWidth = percentage * availableWidth; - float minBarWidth = paint.measureText(streak.length.toString()) + em; + float minBarWidth = + paint.measureText(Long.toString(streak.getLength())) + em; barWidth = Math.max(barWidth, minBarWidth); float gap = (width - barWidth) / 2; @@ -213,19 +170,20 @@ public class HabitStreakView extends View implements HabitDataView, ModelObserva paint.setColor(percentageToColor(percentage)); - canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom, rect.right - gap, - rect.bottom - paddingTopBottom, paint); + canvas.drawRect(rect.left + gap, rect.top + paddingTopBottom, + rect.right - gap, rect.bottom - paddingTopBottom, paint); float yOffset = rect.centerY() + 0.3f * em; paint.setColor(reverseTextColor); paint.setTextAlign(Paint.Align.CENTER); - canvas.drawText(streak.length.toString(), rect.centerX(), yOffset, paint); + canvas.drawText(Long.toString(streak.getLength()), rect.centerX(), + yOffset, paint); - if(shouldShowLabels) + if (shouldShowLabels) { - String startLabel = dateFormat.format(new Date(streak.start)); - String endLabel = dateFormat.format(new Date(streak.end)); + String startLabel = dateFormat.format(new Date(streak.getStart())); + String endLabel = dateFormat.format(new Date(streak.getEnd())); paint.setColor(textColor); paint.setTextAlign(Paint.Align.RIGHT); @@ -236,20 +194,19 @@ public class HabitStreakView extends View implements HabitDataView, ModelObserva } } - private int percentageToColor(float percentage) - { - if(percentage >= 1.0f) return colors[3]; - if(percentage >= 0.8f) return colors[2]; - if(percentage >= 0.5f) return colors[1]; - return colors[0]; - } - - public void setIsBackgroundTransparent(boolean isBackgroundTransparent) + private void init() { - this.isBackgroundTransparent = isBackgroundTransparent; + createPaints(); createColors(); - } + streaks = Collections.emptyList(); + + dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); + rect = new RectF(); + maxStreakCount = 10; + baseSize = getResources().getDimensionPixelSize(R.dimen.baseSize); + } @Override protected void onAttachedToWindow() @@ -263,21 +220,93 @@ public class HabitStreakView extends View implements HabitDataView, ModelObserva refreshData(); } }.execute(); - habit.observable.addListener(this); - habit.streaks.observable.addListener(this); + habit.getObservable().addListener(this); + habit.getStreaks().getObservable().addListener(this); } @Override protected void onDetachedFromWindow() { - habit.streaks.observable.removeListener(this); - habit.observable.removeListener(this); + habit.getStreaks().getObservable().removeListener(this); + habit.getObservable().removeListener(this); super.onDetachedFromWindow(); } @Override - public void onModelChange() + protected void onDraw(Canvas canvas) { - refreshData(); + super.onDraw(canvas); + if (streaks.size() == 0) return; + + rect.set(0, 0, width, baseSize); + + for (Streak s : streaks) + { + drawRow(canvas, s, rect); + rect.offset(0, baseSize); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); + } + + @Override + protected void onSizeChanged(int width, + int height, + int oldWidth, + int oldHeight) + { + maxStreakCount = height / baseSize; + this.width = width; + + float minTextSize = getResources().getDimension(R.dimen.tinyTextSize); + float maxTextSize = + getResources().getDimension(R.dimen.regularTextSize); + float textSize = baseSize * 0.5f; + + paint.setTextSize( + Math.max(Math.min(textSize, maxTextSize), minTextSize)); + em = paint.getFontSpacing(); + textMargin = 0.5f * em; + + updateMaxMin(); + } + + private int percentageToColor(float percentage) + { + if (percentage >= 1.0f) return colors[3]; + if (percentage >= 0.8f) return colors[2]; + if (percentage >= 0.5f) return colors[1]; + return colors[0]; + } + + private void updateMaxMin() + { + maxLength = 0; + minLength = Long.MAX_VALUE; + shouldShowLabels = true; + + for (Streak s : streaks) + { + maxLength = Math.max(maxLength, s.getLength()); + minLength = Math.min(minLength, s.getLength()); + + float lw1 = + paint.measureText(dateFormat.format(new Date(s.getStart()))); + float lw2 = + paint.measureText(dateFormat.format(new Date(s.getEnd()))); + maxLabelWidth = Math.max(maxLabelWidth, Math.max(lw1, lw2)); + } + + if (width - 2 * maxLabelWidth < width * 0.25f) + { + maxLabelWidth = 0; + shouldShowLabels = false; + } } } diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitWidgetView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitWidgetView.java similarity index 98% rename from app/src/main/java/org/isoron/uhabits/views/HabitWidgetView.java rename to app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitWidgetView.java index b7dc4a918..6b161f016 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HabitWidgetView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.habits.show.views; import android.content.Context; import android.graphics.Color; @@ -32,8 +32,8 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import org.isoron.uhabits.R; -import org.isoron.uhabits.utils.InterfaceUtils; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.utils.InterfaceUtils; import java.util.Arrays; diff --git a/app/src/main/java/org/isoron/uhabits/views/RingView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/RingView.java similarity index 98% rename from app/src/main/java/org/isoron/uhabits/views/RingView.java rename to app/src/main/java/org/isoron/uhabits/ui/habits/show/views/RingView.java index f521fc47d..f471dfce9 100644 --- a/app/src/main/java/org/isoron/uhabits/views/RingView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/RingView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.habits.show.views; import android.content.Context; import android.graphics.Bitmap; @@ -70,7 +70,7 @@ public class RingView extends View percentage = 0.0f; precision = 0.01f; - color = ColorUtils.CSV_PALETTE[0]; + color = ColorUtils.getAndroidTestColor(0); thickness = InterfaceUtils.dpToPixels(getContext(), 2); text = ""; textSize = context.getResources().getDimension(R.dimen.smallTextSize); diff --git a/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScrollableDataView.java similarity index 83% rename from app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java rename to app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScrollableDataView.java index fbae274b7..746d06cd0 100644 --- a/app/src/main/java/org/isoron/uhabits/views/ScrollableDataView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScrollableDataView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.ui.habits.show.views; import android.animation.ValueAnimator; import android.content.Context; @@ -28,15 +28,19 @@ import android.view.View; import android.view.ViewParent; import android.widget.Scroller; -public abstract class ScrollableDataView extends View implements GestureDetector.OnGestureListener, - ValueAnimator.AnimatorUpdateListener +public abstract class ScrollableDataView extends View + implements GestureDetector.OnGestureListener, + ValueAnimator.AnimatorUpdateListener { private int dataOffset; + private int scrollerBucketSize; private GestureDetector detector; + private Scroller scroller; + private ValueAnimator scrollAnimator; public ScrollableDataView(Context context) @@ -51,18 +55,24 @@ public abstract class ScrollableDataView extends View implements GestureDetector init(context); } - private void init(Context context) + public int getDataOffset() { - detector = new GestureDetector(context, this); - scroller = new Scroller(context, null, true); - scrollAnimator = ValueAnimator.ofFloat(0, 1); - scrollAnimator.addUpdateListener(this); + return dataOffset; } @Override - public boolean onTouchEvent(MotionEvent event) + public void onAnimationUpdate(ValueAnimator animation) { - return detector.onTouchEvent(event); + if (!scroller.isFinished()) + { + scroller.computeScrollOffset(); + dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize); + postInvalidate(); + } + else + { + scrollAnimator.cancel(); + } } @Override @@ -72,30 +82,40 @@ public abstract class ScrollableDataView extends View implements GestureDetector } @Override - public void onShowPress(MotionEvent e) + public boolean onFling(MotionEvent e1, + MotionEvent e2, + float velocityX, + float velocityY) { + scroller.fling(scroller.getCurrX(), scroller.getCurrY(), + (int) velocityX / 2, 0, 0, 100000, 0, 0); + invalidate(); + scrollAnimator.setDuration(scroller.getDuration()); + scrollAnimator.start(); + + return false; } @Override - public boolean onSingleTapUp(MotionEvent e) + public void onLongPress(MotionEvent e) { - return false; + } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float dx, float dy) { - if(scrollerBucketSize == 0) - return false; + if (scrollerBucketSize == 0) return false; - if(Math.abs(dx) > Math.abs(dy)) + if (Math.abs(dx) > Math.abs(dy)) { ViewParent parent = getParent(); - if(parent != null) parent.requestDisallowInterceptTouchEvent(true); + if (parent != null) parent.requestDisallowInterceptTouchEvent(true); } - scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), (int) -dx, (int) dy, 0); + scroller.startScroll(scroller.getCurrX(), scroller.getCurrY(), + (int) -dx, (int) dy, 0); scroller.computeScrollOffset(); dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize); postInvalidate(); @@ -104,46 +124,33 @@ public abstract class ScrollableDataView extends View implements GestureDetector } @Override - public void onLongPress(MotionEvent e) + public void onShowPress(MotionEvent e) { } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) + public boolean onSingleTapUp(MotionEvent e) { - scroller.fling(scroller.getCurrX(), scroller.getCurrY(), (int) velocityX / 2, 0, 0, 100000, - 0, 0); - invalidate(); - - scrollAnimator.setDuration(scroller.getDuration()); - scrollAnimator.start(); - return false; } @Override - public void onAnimationUpdate(ValueAnimator animation) + public boolean onTouchEvent(MotionEvent event) { - if (!scroller.isFinished()) - { - scroller.computeScrollOffset(); - dataOffset = Math.max(0, scroller.getCurrX() / scrollerBucketSize); - postInvalidate(); - } - else - { - scrollAnimator.cancel(); - } + return detector.onTouchEvent(event); } - public int getDataOffset() + public void setScrollerBucketSize(int scrollerBucketSize) { - return dataOffset; + this.scrollerBucketSize = scrollerBucketSize; } - public void setScrollerBucketSize(int scrollerBucketSize) + private void init(Context context) { - this.scrollerBucketSize = scrollerBucketSize; + detector = new GestureDetector(context, this); + scroller = new Scroller(context, null, true); + scrollAnimator = ValueAnimator.ofFloat(0, 1); + scrollAnimator.addUpdateListener(this); } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/package-info.java new file mode 100644 index 000000000..5442e7bcf --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/package-info.java @@ -0,0 +1,24 @@ +/* + * 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 . + */ + +/** + * Provides custom views that are used primarily on {@link + * org.isoron.uhabits.ui.habits.show.ShowHabitActivity}. + */ +package org.isoron.uhabits.ui.habits.show.views; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/intro/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/intro/package-info.java index 68eee2b1e..200ce960c 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/intro/package-info.java +++ b/app/src/main/java/org/isoron/uhabits/ui/intro/package-info.java @@ -18,6 +18,6 @@ */ /** - * Contains classes for the IntroActivity. + * Provides activity that introduces app to the user and related classes. */ package org.isoron.uhabits.ui.intro; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/package-info.java new file mode 100644 index 000000000..6eb8588d5 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +/** + * Provides classes for the Android user interface. + */ +package org.isoron.uhabits.ui; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/settings/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/settings/package-info.java index d7254b864..28a65b5ea 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/settings/package-info.java +++ b/app/src/main/java/org/isoron/uhabits/ui/settings/package-info.java @@ -18,6 +18,6 @@ */ /** - * Contains classes for the SettingsActivity. + * Provides activity for changing the settings. */ package org.isoron.uhabits.ui.settings; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/utils/ColorUtils.java b/app/src/main/java/org/isoron/uhabits/utils/ColorUtils.java index 1e4656540..e8116c1b0 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/ColorUtils.java +++ b/app/src/main/java/org/isoron/uhabits/utils/ColorUtils.java @@ -27,55 +27,79 @@ import org.isoron.uhabits.R; public abstract class ColorUtils { - public static int CSV_PALETTE[] = - { - Color.parseColor("#D32F2F"), // 0 red - Color.parseColor("#E64A19"), // 1 orange - Color.parseColor("#F9A825"), // 2 yellow - Color.parseColor("#AFB42B"), // 3 light green - Color.parseColor("#388E3C"), // 4 dark green - Color.parseColor("#00897B"), // 5 teal - Color.parseColor("#00ACC1"), // 6 cyan - Color.parseColor("#039BE5"), // 7 blue - Color.parseColor("#5E35B1"), // 8 deep purple - Color.parseColor("#8E24AA"), // 9 purple - Color.parseColor("#D81B60"), // 10 pink - Color.parseColor("#303030"), // 11 dark grey - Color.parseColor("#aaaaaa") // 12 light grey + public static String CSV_PALETTE[] = { + "#D32F2F", // 0 red + "#E64A19", // 1 orange + "#F9A825", // 2 yellow + "#AFB42B", // 3 light green + "#388E3C", // 4 dark green + "#00897B", // 5 teal + "#00ACC1", // 6 cyan + "#039BE5", // 7 blue + "#5E35B1", // 8 deep purple + "#8E24AA", // 9 purple + "#D81B60", // 10 pink + "#303030", // 11 dark grey + "#aaaaaa" // 12 light grey }; public static int colorToPaletteIndex(Context context, int color) { int[] palette = getPalette(context); - for(int k = 0; k < palette.length; k++) - if(palette[k] == color) return k; + for (int k = 0; k < palette.length; k++) + if (palette[k] == color) return k; return -1; } - public static int[] getPalette(Context context) + public static int getAndroidTestColor(int index) { - int resourceId = InterfaceUtils.getStyleResource(context, R.attr.palette); - if(resourceId < 0) return CSV_PALETTE; - - return context.getResources().getIntArray(resourceId); + int palette[] = { + Color.parseColor("#D32F2F"), // 0 red + Color.parseColor("#E64A19"), // 1 orange + Color.parseColor("#F9A825"), // 2 yellow + Color.parseColor("#AFB42B"), // 3 light green + Color.parseColor("#388E3C"), // 4 dark green + Color.parseColor("#00897B"), // 5 teal + Color.parseColor("#00ACC1"), // 6 cyan + Color.parseColor("#039BE5"), // 7 blue + Color.parseColor("#5E35B1"), // 8 deep purple + Color.parseColor("#8E24AA"), // 9 purple + Color.parseColor("#D81B60"), // 10 pink + Color.parseColor("#303030"), // 11 dark grey + Color.parseColor("#aaaaaa") // 12 light grey + }; + + return palette[index]; } public static int getColor(Context context, int paletteColor) { - if(context == null) throw new IllegalArgumentException("Context is null"); + if (context == null) + throw new IllegalArgumentException("Context is null"); int palette[] = getPalette(context); - if(paletteColor < 0 || paletteColor >= palette.length) + if (paletteColor < 0 || paletteColor >= palette.length) { - Log.w("ColorHelper", String.format("Invalid color: %d. Returning default.", paletteColor)); + Log.w("ColorHelper", + String.format("Invalid color: %d. Returning default.", + paletteColor)); paletteColor = 0; } return palette[paletteColor]; } + public static int[] getPalette(Context context) + { + int resourceId = + InterfaceUtils.getStyleResource(context, R.attr.palette); + if (resourceId < 0) throw new RuntimeException("resource not found"); + + return context.getResources().getIntArray(resourceId); + } + public static int mixColors(int color1, int color2, float amount) { final byte ALPHA_CHANNEL = 24; @@ -86,36 +110,26 @@ public abstract class ColorUtils final float inverseAmount = 1.0f - amount; int a = ((int) (((float) (color1 >> ALPHA_CHANNEL & 0xff) * amount) + - ((float) (color2 >> ALPHA_CHANNEL & 0xff) * inverseAmount))) & 0xff; + ((float) (color2 >> ALPHA_CHANNEL & 0xff) * + inverseAmount))) & 0xff; int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) + - ((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff; + ((float) (color2 >> RED_CHANNEL & 0xff) * + inverseAmount))) & 0xff; int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) + - ((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff; + ((float) (color2 >> GREEN_CHANNEL & 0xff) * + inverseAmount))) & 0xff; int b = ((int) (((float) (color1 & 0xff) * amount) + - ((float) (color2 & 0xff) * inverseAmount))) & 0xff; + ((float) (color2 & 0xff) * inverseAmount))) & 0xff; - return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b << BLUE_CHANNEL; - } - - public static int setHue(int color, float newHue) - { - return setHSVParameter(color, newHue, 0); - } - - public static int setSaturation(int color, float newSaturation) - { - return setHSVParameter(color, newSaturation, 1); - } - - public static int setValue(int color, float newValue) - { - return setHSVParameter(color, newValue, 2); + return a << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | + b << BLUE_CHANNEL; } public static int setAlpha(int color, float newAlpha) { int intAlpha = (int) (newAlpha * 255); - return Color.argb(intAlpha, Color.red(color), Color.green(color), Color.blue(color)); + return Color.argb(intAlpha, Color.red(color), Color.green(color), + Color.blue(color)); } public static int setMinValue(int color, float newValue) @@ -126,16 +140,4 @@ public abstract class ColorUtils return Color.HSVToColor(hsv); } - private static int setHSVParameter(int color, float newValue, int index) - { - float hsv[] = new float[3]; - Color.colorToHSV(color, hsv); - hsv[index] = newValue; - return Color.HSVToColor(hsv); - } - - public static String toHTML(int color) - { - return String.format("#%06X", 0xFFFFFF & color); - } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java b/app/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java index f8b5a9778..5381590f8 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java +++ b/app/src/main/java/org/isoron/uhabits/utils/DatabaseUtils.java @@ -29,11 +29,11 @@ import com.activeandroid.Configuration; import org.isoron.uhabits.BuildConfig; import org.isoron.uhabits.HabitsApplication; -import org.isoron.uhabits.models.Checkmark; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Repetition; -import org.isoron.uhabits.models.Score; -import org.isoron.uhabits.models.Streak; +import org.isoron.uhabits.models.sqlite.CheckmarkRecord; +import org.isoron.uhabits.models.sqlite.HabitRecord; +import org.isoron.uhabits.models.sqlite.RepetitionRecord; +import org.isoron.uhabits.models.sqlite.ScoreRecord; +import org.isoron.uhabits.models.sqlite.StreakRecord; import java.io.File; import java.io.IOException; @@ -41,17 +41,12 @@ import java.text.SimpleDateFormat; public abstract class DatabaseUtils { - public interface Command - { - void execute(); - } - - public static void executeAsTransaction(Command command) + public static void executeAsTransaction(Callback callback) { ActiveAndroid.beginTransaction(); try { - command.execute(); + callback.execute(); ActiveAndroid.setTransactionSuccessful(); } finally @@ -60,30 +55,18 @@ public abstract class DatabaseUtils } } - @SuppressWarnings("ResultOfMethodCallIgnored") - public static String saveDatabaseCopy(File dir) throws IOException - { - File db = getDatabaseFile(); - - SimpleDateFormat dateFormat = DateUtils.getBackupDateFormat(); - String date = dateFormat.format(DateUtils.getLocalTime()); - File dbCopy = new File(String.format("%s/Loop Habits Backup %s.db", dir.getAbsolutePath(), date)); - - FileUtils.copy(db, dbCopy); - - return dbCopy.getAbsolutePath(); - } - @NonNull public static File getDatabaseFile() { Context context = HabitsApplication.getContext(); - if(context == null) throw new RuntimeException("No application context found"); + if (context == null) + throw new RuntimeException("No application context found"); String databaseFilename = getDatabaseFilename(); return new File(String.format("%s/../databases/%s", - context.getApplicationContext().getFilesDir().getPath(), databaseFilename)); + context.getApplicationContext().getFilesDir().getPath(), + databaseFilename)); } @NonNull @@ -91,8 +74,7 @@ public abstract class DatabaseUtils { String databaseFilename = BuildConfig.databaseFilename; - if (HabitsApplication.isTestMode()) - databaseFilename = "test.db"; + if (HabitsApplication.isTestMode()) databaseFilename = "test.db"; return databaseFilename; } @@ -101,14 +83,15 @@ public abstract class DatabaseUtils public static void initializeActiveAndroid() { Context context = HabitsApplication.getContext(); - if(context == null) throw new RuntimeException("application context should not be null"); + if (context == null) throw new RuntimeException( + "application context should not be null"); Configuration dbConfig = new Configuration.Builder(context) - .setDatabaseName(getDatabaseFilename()) - .setDatabaseVersion(BuildConfig.databaseVersion) - .addModelClasses(Checkmark.class, Habit.class, Repetition.class, Score.class, - Streak.class) - .create(); + .setDatabaseName(getDatabaseFilename()) + .setDatabaseVersion(BuildConfig.databaseVersion) + .addModelClasses(CheckmarkRecord.class, HabitRecord.class, + RepetitionRecord.class, ScoreRecord.class, StreakRecord.class) + .create(); ActiveAndroid.initialize(dbConfig); } @@ -125,7 +108,28 @@ public abstract class DatabaseUtils } finally { - if(c != null) c.close(); + if (c != null) c.close(); } } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static String saveDatabaseCopy(File dir) throws IOException + { + File db = getDatabaseFile(); + + SimpleDateFormat dateFormat = DateUtils.getBackupDateFormat(); + String date = dateFormat.format(DateUtils.getLocalTime()); + File dbCopy = new File( + String.format("%s/Loop Habits Backup %s.db", dir.getAbsolutePath(), + date)); + + FileUtils.copy(db, dbCopy); + + return dbCopy.getAbsolutePath(); + } + + public interface Callback + { + void execute(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/utils/ReminderUtils.java b/app/src/main/java/org/isoron/uhabits/utils/ReminderUtils.java index 43d07c3f0..a770c6c7c 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/ReminderUtils.java +++ b/app/src/main/java/org/isoron/uhabits/utils/ReminderUtils.java @@ -37,6 +37,7 @@ import android.util.Log; import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.text.DateFormat; import java.util.Calendar; @@ -44,24 +45,20 @@ import java.util.Date; public abstract class ReminderUtils { - public static void createReminderAlarms(Context context) + public static void createReminderAlarm(Context context, + Habit habit, + @Nullable Long reminderTime) { - for (Habit habit : Habit.getHabitsWithReminder()) - createReminderAlarm(context, habit, null); - } - - public static void createReminderAlarm(Context context, Habit habit, @Nullable Long reminderTime) - { - if(!habit.hasReminder()) return; + 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); + calendar.set(Calendar.HOUR_OF_DAY, habit.getReminderHour()); //noinspection ConstantConditions - calendar.set(Calendar.MINUTE, habit.reminderMin); + calendar.set(Calendar.MINUTE, habit.getReminderMin()); calendar.set(Calendar.SECOND, 0); reminderTime = calendar.getTimeInMillis(); @@ -70,7 +67,8 @@ public abstract class ReminderUtils reminderTime += AlarmManager.INTERVAL_DAY; } - long timestamp = DateUtils.getStartOfDay(DateUtils.toLocalTime(reminderTime)); + long timestamp = + DateUtils.getStartOfDay(DateUtils.toLocalTime(reminderTime)); Uri uri = habit.getUri(); @@ -80,22 +78,61 @@ public abstract class ReminderUtils alarmIntent.putExtra("timestamp", timestamp); alarmIntent.putExtra("reminderTime", reminderTime); - PendingIntent pendingIntent = - PendingIntent.getBroadcast(context, ((int) (habit.getId() % Integer.MAX_VALUE)) + 1, - alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, + ((int) (habit.getId() % Integer.MAX_VALUE)) + 1, alarmIntent, + PendingIntent.FLAG_UPDATE_CURRENT); - AlarmManager manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + AlarmManager manager = + (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); if (Build.VERSION.SDK_INT >= 23) - manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent); + manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, + reminderTime, pendingIntent); else if (Build.VERSION.SDK_INT >= 19) - manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent); - else - manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent); + manager.setExact(AlarmManager.RTC_WAKEUP, reminderTime, + pendingIntent); + else manager.set(AlarmManager.RTC_WAKEUP, reminderTime, pendingIntent); - String name = habit.name.substring(0, Math.min(3, habit.name.length())); + String name = habit.getName().substring(0, Math.min(3, habit.getName().length())); Log.d("ReminderHelper", String.format("Setting alarm (%s): %s", - DateFormat.getDateTimeInstance().format(new Date(reminderTime)), name)); + DateFormat.getDateTimeInstance().format(new Date(reminderTime)), + name)); + } + + public static void createReminderAlarms(Context context, + HabitList habitList) + { + for (Habit habit : habitList.getWithReminder()) + createReminderAlarm(context, habit, null); + } + + @Nullable + public static String getRingtoneName(Context context) + { + try + { + Uri ringtoneUri = getRingtoneUri(context); + String ringtoneName = + context.getResources().getString(R.string.none); + + if (ringtoneUri != null) + { + Ringtone ringtone = + RingtoneManager.getRingtone(context, ringtoneUri); + if (ringtone != null) + { + ringtoneName = ringtone.getTitle(context); + ringtone.stop(); + } + } + + return ringtoneName; + } + catch (RuntimeException e) + { + e.printStackTrace(); + return null; + } } @Nullable @@ -104,70 +141,57 @@ public abstract class ReminderUtils Uri ringtoneUri = null; Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - String prefRingtoneUri = prefs.getString("pref_ringtone_uri", defaultRingtoneUri.toString()); - if (prefRingtoneUri.length() > 0) ringtoneUri = Uri.parse(prefRingtoneUri); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context); + String prefRingtoneUri = + prefs.getString("pref_ringtone_uri", defaultRingtoneUri.toString()); + if (prefRingtoneUri.length() > 0) + ringtoneUri = Uri.parse(prefRingtoneUri); return ringtoneUri; } public static void parseRingtoneData(Context context, @Nullable Intent data) { - if(data == null) return; + if (data == null) return; - Uri ringtoneUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + Uri ringtoneUri = + data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); if (ringtoneUri != null) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - prefs.edit().putString("pref_ringtone_uri", ringtoneUri.toString()).apply(); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context); + prefs + .edit() + .putString("pref_ringtone_uri", ringtoneUri.toString()) + .apply(); } else { String off = context.getResources().getString(R.string.none); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context); prefs.edit().putString("pref_ringtone_uri", "").apply(); } } - public static void startRingtonePickerActivity(Fragment fragment, int requestCode) + public static void startRingtonePickerActivity(Fragment fragment, + int requestCode) { - Uri existingRingtoneUri = ReminderUtils.getRingtoneUri(fragment.getContext()); + Uri existingRingtoneUri = + ReminderUtils.getRingtoneUri(fragment.getContext()); Uri defaultRingtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI; Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, + RingtoneManager.TYPE_NOTIFICATION); intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, defaultRingtoneUri); - intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, existingRingtoneUri); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, + defaultRingtoneUri); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, + existingRingtoneUri); fragment.startActivityForResult(intent, requestCode); } - - @Nullable - public static String getRingtoneName(Context context) - { - try - { - Uri ringtoneUri = getRingtoneUri(context); - String ringtoneName = context.getResources().getString(R.string.none); - - if (ringtoneUri != null) - { - Ringtone ringtone = RingtoneManager.getRingtone(context, ringtoneUri); - if (ringtone != null) - { - ringtoneName = ringtone.getTitle(context); - ringtone.stop(); - } - } - - return ringtoneName; - } - catch (RuntimeException e) - { - e.printStackTrace(); - return null; - } - } } diff --git a/app/src/main/java/org/isoron/uhabits/utils/package-info.java b/app/src/main/java/org/isoron/uhabits/utils/package-info.java new file mode 100644 index 000000000..51db3c7a3 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +/** + * Provides various utilities classes, such as {@link org.isoron.uhabits.utils.ColorUtils}. + */ +package org.isoron.uhabits.utils; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/views/NumberView.java b/app/src/main/java/org/isoron/uhabits/views/NumberView.java deleted file mode 100644 index 7b788d12a..000000000 --- a/app/src/main/java/org/isoron/uhabits/views/NumberView.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * 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.uhabits.R; -import org.isoron.uhabits.utils.ColorUtils; -import org.isoron.uhabits.utils.InterfaceUtils; - -public class NumberView extends View -{ - 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) - { - super(context); - this.textSize = getResources().getDimension(R.dimen.regularTextSize); - init(); - } - - public NumberView(Context context, AttributeSet attrs) - { - super(context, attrs); - - this.textSize = getResources().getDimension(R.dimen.regularTextSize); - - this.label = InterfaceUtils.getAttribute(context, attrs, "label", "Number"); - this.number = InterfaceUtils.getIntAttribute(context, attrs, "number", 0); - this.textSize = InterfaceUtils.getFloatAttribute(context, attrs, "textSize", - getResources().getDimension(R.dimen.regularTextSize)); - - this.color = ColorUtils.getColor(getContext(), 7); - init(); - } - - public void setColor(int color) - { - this.color = color; - pText.setColor(color); - postInvalidate(); - } - - public void setLabel(String label) - { - this.label = label; - requestLayout(); - postInvalidate(); - } - - public void setNumber(int number) - { - this.number = number; - postInvalidate(); - } - - public void setTextSize(float textSize) - { - this.textSize = textSize; - requestLayout(); - 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; - labelMarginTop = textSize * 0.35f; - numberTextSize = textSize * 2.85f; - - pText.setTextSize(numberTextSize); - numberLayout = new StaticLayout(Integer.toString(number), pText, width, - Layout.Alignment.ALIGN_NORMAL, 1.0f, 0f, false); - - 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); - } - - @Override - protected void onDraw(Canvas canvas) - { - super.onDraw(canvas); - 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 deleted file mode 100644 index 0ddfcea58..000000000 --- a/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.uhabits.R; -import org.isoron.uhabits.utils.ColorUtils; -import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.utils.InterfaceUtils; -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 = InterfaceUtils.getIntAttribute(context, attrs, "interval", 7); - int labelValue = InterfaceUtils.getIntAttribute(context, attrs, "labelValue", 7); - String labelFormat = InterfaceUtils.getAttribute(context, attrs, "labelFormat", - getResources().getString(R.string.last_x_days)); - - setLabel(String.format(labelFormat, labelValue)); - } - - @Override - public void refreshData() - { - if(isInEditMode()) - { - setNumber(interval); - return; - } - - long to = DateUtils.getStartOfToday(); - long from; - - if(interval == 0) - { - from = 0; - } - else - { - GregorianCalendar fromCalendar = DateUtils.getStartOfTodayCalendar(); - fromCalendar.add(Calendar.DAY_OF_YEAR, -interval + 1); - from = fromCalendar.getTimeInMillis(); - } - - if(habit != null) - setNumber(habit.repetitions.count(from, to)); - - postInvalidate(); - } - - @Override - public void setHabit(Habit habit) - { - this.habit = habit; - setColor(ColorUtils.getColor(getContext(), habit.color)); - } -} 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 686255ece..a94809ff4 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java @@ -35,16 +35,23 @@ import android.widget.ImageView; import android.widget.RemoteViews; import android.widget.TextView; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; -import org.isoron.uhabits.utils.InterfaceUtils; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.tasks.BaseTask; +import org.isoron.uhabits.utils.InterfaceUtils; import java.io.FileOutputStream; import java.io.IOException; +import javax.inject.Inject; + public abstract class BaseWidgetProvider extends AppWidgetProvider { + @Inject + HabitList habitList; + private class WidgetDimensions { public int portraitWidth, portraitHeight; @@ -100,6 +107,7 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider private void updateWidget(Context context, AppWidgetManager manager, int widgetId, Bundle options) { + HabitsApplication.getComponent().inject(this); WidgetDimensions dim = getWidgetDimensions(context, options); Context appContext = context.getApplicationContext(); @@ -108,7 +116,7 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1L); if(habitId < 0) return; - Habit habit = Habit.get(habitId); + Habit habit = habitList.getById(habitId); if(habit == null) { drawErrorWidget(context, manager, widgetId); @@ -288,7 +296,7 @@ public abstract class BaseWidgetProvider extends AppWidgetProvider widgetView.setDrawingCacheEnabled(true); widgetView.buildDrawingCache(true); Bitmap drawingCache = widgetView.getDrawingCache(); - remoteViews.setTextViewText(R.id.label, habit.name); + remoteViews.setTextViewText(R.id.label, habit.getName()); remoteViews.setImageViewBitmap(R.id.imageView, drawingCache); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) diff --git a/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java index cc392ba7e..4447ac2c9 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java @@ -25,8 +25,8 @@ import android.view.View; import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.CheckmarkWidgetView; -import org.isoron.uhabits.views.HabitDataView; +import org.isoron.uhabits.widgets.views.CheckmarkWidgetView; +import org.isoron.uhabits.ui.habits.show.views.HabitDataView; public class CheckmarkWidgetProvider extends BaseWidgetProvider { diff --git a/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java index 2fdbedb71..e186d3224 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java @@ -26,9 +26,9 @@ import android.view.View; import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.GraphWidgetView; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitFrequencyView; +import org.isoron.uhabits.widgets.views.GraphWidgetView; +import org.isoron.uhabits.ui.habits.show.views.HabitDataView; +import org.isoron.uhabits.ui.habits.show.views.HabitFrequencyView; public class FrequencyWidgetProvider extends BaseWidgetProvider { diff --git a/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java b/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java index 6082f0a2b..ffe682065 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java @@ -30,16 +30,23 @@ import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; +import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.models.HabitList; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; + public class HabitPickerDialog extends Activity implements AdapterView.OnItemClickListener { + @Inject + HabitList habitList; private Integer widgetId; + private ArrayList habitIds; @Override @@ -47,6 +54,7 @@ public class HabitPickerDialog extends Activity implements AdapterView.OnItemCli { super.onCreate(savedInstanceState); setContentView(R.layout.widget_configure_activity); + HabitsApplication.getComponent().inject(this); Intent intent = getIntent(); Bundle extras = intent.getExtras(); @@ -59,15 +67,15 @@ public class HabitPickerDialog extends Activity implements AdapterView.OnItemCli habitIds = new ArrayList<>(); ArrayList habitNames = new ArrayList<>(); - List habits = Habit.getAll(false); - for(Habit h : habits) + List habits = habitList.getAll(false); + for (Habit h : habits) { habitIds.add(h.getId()); - habitNames.add(h.name); + habitNames.add(h.getName()); } - ArrayAdapter adapter = - new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, habitNames); + ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, + habitNames); listView.setAdapter(adapter); listView.setOnItemClickListener(this); } @@ -77,8 +85,11 @@ public class HabitPickerDialog extends Activity implements AdapterView.OnItemCli { Long habitId = habitIds.get(position); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( - getApplicationContext()); - prefs.edit().putLong(BaseWidgetProvider.getHabitIdKey(widgetId), habitId).commit(); + getApplicationContext()); + prefs + .edit() + .putLong(BaseWidgetProvider.getHabitIdKey(widgetId), habitId) + .commit(); WidgetManager.updateWidgets(this); diff --git a/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java index bb8be7e25..b9d98f2e0 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java @@ -25,9 +25,9 @@ import android.view.View; import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.GraphWidgetView; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitHistoryView; +import org.isoron.uhabits.widgets.views.GraphWidgetView; +import org.isoron.uhabits.ui.habits.show.views.HabitDataView; +import org.isoron.uhabits.ui.habits.show.views.HabitHistoryView; public class HistoryWidgetProvider extends BaseWidgetProvider { diff --git a/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java index a046dc70d..ee998de8d 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java @@ -24,11 +24,11 @@ import android.view.View; import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.R; -import org.isoron.uhabits.utils.InterfaceUtils; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.GraphWidgetView; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitScoreView; +import org.isoron.uhabits.utils.InterfaceUtils; +import org.isoron.uhabits.widgets.views.GraphWidgetView; +import org.isoron.uhabits.ui.habits.show.views.HabitDataView; +import org.isoron.uhabits.ui.habits.show.views.HabitScoreView; public class ScoreWidgetProvider extends BaseWidgetProvider { diff --git a/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java index f0455d00a..59af7a256 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java @@ -25,9 +25,9 @@ import android.view.View; import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.views.GraphWidgetView; -import org.isoron.uhabits.views.HabitDataView; -import org.isoron.uhabits.views.HabitStreakView; +import org.isoron.uhabits.widgets.views.GraphWidgetView; +import org.isoron.uhabits.ui.habits.show.views.HabitDataView; +import org.isoron.uhabits.ui.habits.show.views.HabitStreakView; public class StreakWidgetProvider extends BaseWidgetProvider { diff --git a/app/src/main/java/org/isoron/uhabits/widgets/package-info.java b/app/src/main/java/org/isoron/uhabits/widgets/package-info.java new file mode 100644 index 000000000..5616e64ad --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +/** + * Provides home-screen Android widgets and related classes. + */ +package org.isoron.uhabits.widgets; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/views/CheckmarkWidgetView.java b/app/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.java similarity index 74% rename from app/src/main/java/org/isoron/uhabits/views/CheckmarkWidgetView.java rename to app/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.java index 0f6046e1b..47a8b2587 100644 --- a/app/src/main/java/org/isoron/uhabits/views/CheckmarkWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.widgets.views; import android.content.Context; import android.support.annotation.NonNull; @@ -27,22 +27,29 @@ import android.util.TypedValue; import android.widget.TextView; import org.isoron.uhabits.R; -import org.isoron.uhabits.utils.ColorUtils; -import org.isoron.uhabits.utils.InterfaceUtils; import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Score; +import org.isoron.uhabits.ui.habits.show.views.HabitDataView; +import org.isoron.uhabits.ui.habits.show.views.HabitWidgetView; +import org.isoron.uhabits.ui.habits.show.views.RingView; +import org.isoron.uhabits.utils.ColorUtils; +import org.isoron.uhabits.utils.InterfaceUtils; -public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataView +public class CheckmarkWidgetView extends HabitWidgetView + implements HabitDataView { private int activeColor; + private float percentage; @Nullable private String name; private RingView ring; + private TextView label; + private int checkmarkValue; public CheckmarkWidgetView(Context context) @@ -57,32 +64,6 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie init(); } - private void init() - { - ring = (RingView) findViewById(R.id.scoreRing); - label = (TextView) findViewById(R.id.label); - - if(ring != null) ring.setIsTransparencyEnabled(true); - - if(isInEditMode()) - { - percentage = 0.75f; - name = "Wake up early"; - activeColor = ColorUtils.CSV_PALETTE[6]; - checkmarkValue = Checkmark.CHECKED_EXPLICITLY; - refresh(); - } - } - - @Override - public void setHabit(@NonNull Habit habit) - { - super.setHabit(habit); - this.name = habit.name; - this.activeColor = ColorUtils.getColor(getContext(), habit.color); - refresh(); - } - public void refresh() { if (backgroundPaint == null || frame == null || ring == null) return; @@ -98,8 +79,8 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie case Checkmark.CHECKED_EXPLICITLY: text = getResources().getString(R.string.fa_check); backgroundColor = activeColor; - foregroundColor = - InterfaceUtils.getStyledColor(context, R.attr.highContrastReverseTextColor); + foregroundColor = InterfaceUtils.getStyledColor(context, + R.attr.highContrastReverseTextColor); setShadowAlpha(0x4f); rebuildBackground(); @@ -110,15 +91,19 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie case Checkmark.CHECKED_IMPLICITLY: text = getResources().getString(R.string.fa_check); - backgroundColor = InterfaceUtils.getStyledColor(context, R.attr.cardBackgroundColor); - foregroundColor = InterfaceUtils.getStyledColor(context, R.attr.mediumContrastTextColor); + backgroundColor = InterfaceUtils.getStyledColor(context, + R.attr.cardBackgroundColor); + foregroundColor = InterfaceUtils.getStyledColor(context, + R.attr.mediumContrastTextColor); break; case Checkmark.UNCHECKED: default: text = getResources().getString(R.string.fa_times); - backgroundColor = InterfaceUtils.getStyledColor(context, R.attr.cardBackgroundColor); - foregroundColor = InterfaceUtils.getStyledColor(context, R.attr.mediumContrastTextColor); + backgroundColor = InterfaceUtils.getStyledColor(context, + R.attr.cardBackgroundColor); + foregroundColor = InterfaceUtils.getStyledColor(context, + R.attr.mediumContrastTextColor); break; } @@ -134,6 +119,49 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie postInvalidate(); } + @Override + public void refreshData() + { + if (habit == null) return; + this.percentage = + (float) habit.getScores().getTodayValue() / Score.MAX_VALUE; + this.checkmarkValue = habit.getCheckmarks().getTodayValue(); + refresh(); + } + + @Override + public void setHabit(@NonNull Habit habit) + { + super.setHabit(habit); + this.name = habit.getName(); + this.activeColor = ColorUtils.getColor(getContext(), habit.getColor()); + refresh(); + } + + @Override + @NonNull + protected Integer getInnerLayoutId() + { + return R.layout.widget_checkmark; + } + + private void init() + { + ring = (RingView) findViewById(R.id.scoreRing); + label = (TextView) findViewById(R.id.label); + + if (ring != null) ring.setIsTransparencyEnabled(true); + + if (isInEditMode()) + { + percentage = 0.75f; + name = "Wake up early"; + activeColor = ColorUtils.getAndroidTestColor(6); + checkmarkValue = Checkmark.CHECKED_EXPLICITLY; + refresh(); + } + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @@ -147,16 +175,18 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie w *= scale; h *= scale; - if(h < getResources().getDimension(R.dimen.checkmarkWidget_heightBreakpoint)) - ring.setVisibility(GONE); - else - ring.setVisibility(VISIBLE); + if (h < getResources().getDimension( + R.dimen.checkmarkWidget_heightBreakpoint)) ring.setVisibility(GONE); + else ring.setVisibility(VISIBLE); - widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) w, MeasureSpec.EXACTLY); - heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) h, MeasureSpec.EXACTLY); + widthMeasureSpec = + MeasureSpec.makeMeasureSpec((int) w, MeasureSpec.EXACTLY); + heightMeasureSpec = + MeasureSpec.makeMeasureSpec((int) h, MeasureSpec.EXACTLY); float textSize = 0.15f * h; - float maxTextSize = getResources().getDimension(R.dimen.smallerTextSize); + float maxTextSize = + getResources().getDimension(R.dimen.smallerTextSize); textSize = Math.min(textSize, maxTextSize); label.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); @@ -165,19 +195,4 @@ public class CheckmarkWidgetView extends HabitWidgetView implements HabitDataVie super.onMeasure(widthMeasureSpec, heightMeasureSpec); } - - @Override - public void refreshData() - { - if(habit == null) return; - this.percentage = (float) habit.scores.getTodayValue() / Score.MAX_VALUE; - this.checkmarkValue = habit.checkmarks.getTodayValue(); - refresh(); - } - - @NonNull - protected Integer getInnerLayoutId() - { - return R.layout.widget_checkmark; - } } diff --git a/app/src/main/java/org/isoron/uhabits/views/GraphWidgetView.java b/app/src/main/java/org/isoron/uhabits/widgets/views/GraphWidgetView.java similarity index 91% rename from app/src/main/java/org/isoron/uhabits/views/GraphWidgetView.java rename to app/src/main/java/org/isoron/uhabits/widgets/views/GraphWidgetView.java index bb142ca7d..4198d84b1 100644 --- a/app/src/main/java/org/isoron/uhabits/views/GraphWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/views/GraphWidgetView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.views; +package org.isoron.uhabits.widgets.views; import android.content.Context; import android.support.annotation.NonNull; @@ -27,6 +27,8 @@ import android.widget.TextView; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; +import org.isoron.uhabits.ui.habits.show.views.HabitDataView; +import org.isoron.uhabits.ui.habits.show.views.HabitWidgetView; public class GraphWidgetView extends HabitWidgetView implements HabitDataView { @@ -60,7 +62,7 @@ public class GraphWidgetView extends HabitWidgetView implements HabitDataView { super.setHabit(habit); dataView.setHabit(habit); - title.setText(habit.name); + title.setText(habit.getName()); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/widgets/views/package-info.java b/app/src/main/java/org/isoron/uhabits/widgets/views/package-info.java new file mode 100644 index 000000000..2fbf2799c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/widgets/views/package-info.java @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +/** + * Provides views that are specific for the home-screen widgets. + */ +package org.isoron.uhabits.widgets.views; \ No newline at end of file diff --git a/app/src/main/res/layout/list_habits_card.xml b/app/src/main/res/layout/list_habits_card.xml index 3320a995c..2aef23840 100644 --- a/app/src/main/res/layout/list_habits_card.xml +++ b/app/src/main/res/layout/list_habits_card.xml @@ -29,7 +29,7 @@ style="@style/ListHabits.HabitCard" android:layout_width="match_parent"> - - - - @@ -272,7 +272,7 @@ style="@style/CardHeader" android:text="@string/best_streaks"/> - @@ -287,7 +287,7 @@ style="@style/CardHeader" android:text="@string/frequency"/> - diff --git a/app/src/main/res/layout/widget_checkmark.xml b/app/src/main/res/layout/widget_checkmark.xml index a6d0c01b7..da88149a4 100644 --- a/app/src/main/res/layout/widget_checkmark.xml +++ b/app/src/main/res/layout/widget_checkmark.xml @@ -27,7 +27,7 @@ android:gravity="center" android:orientation="vertical"> - + * + * 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.models; + +import org.isoron.uhabits.BaseUnitTest; +import org.isoron.uhabits.utils.DateUtils; +import org.junit.Test; + +import java.io.IOException; +import java.io.StringWriter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.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; + +public class CheckmarkListTest extends BaseUnitTest +{ + private Habit nonDailyHabit; + + private Habit emptyHabit; + + @Override + public void setUp() + { + super.setUp(); + + fixtures.createShortHabit(); + nonDailyHabit = fixtures.createShortHabit(); + emptyHabit = fixtures.createEmptyHabit(); + } + + @Test + public void test_getAllValues_moveBackwardsInTime() + { + travelInTime(-3); + + int[] expectedValues = { + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY, + UNCHECKED, + CHECKED_IMPLICITLY, + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY + }; + + int[] actualValues = nonDailyHabit.getCheckmarks().getAllValues(); + + assertThat(actualValues, equalTo(expectedValues)); + } + + + @Test + public void test_getAllValues_moveForwardInTime() + { + travelInTime(3); + + int[] expectedValues = { + UNCHECKED, + UNCHECKED, + UNCHECKED, + CHECKED_EXPLICITLY, + UNCHECKED, + CHECKED_IMPLICITLY, + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY, + UNCHECKED, + CHECKED_IMPLICITLY, + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY + }; + + int[] actualValues = nonDailyHabit.getCheckmarks().getAllValues(); + + assertThat(actualValues, equalTo(expectedValues)); + } + + @Test + public void test_getAllValues_withEmptyHabit() + { + int[] expectedValues = new int[0]; + int[] actualValues = emptyHabit.getCheckmarks().getAllValues(); + + assertThat(actualValues, equalTo(expectedValues)); + } + + @Test + public void test_getAllValues_withNonDailyHabit() + { + int[] expectedValues = { + CHECKED_EXPLICITLY, + UNCHECKED, + CHECKED_IMPLICITLY, + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY, + UNCHECKED, + CHECKED_IMPLICITLY, + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY + }; + + int[] actualValues = nonDailyHabit.getCheckmarks().getAllValues(); + + assertThat(actualValues, equalTo(expectedValues)); + } + + @Test + public void test_getTodayValue() + { + travelInTime(-1); + assertThat(nonDailyHabit.getCheckmarks().getTodayValue(), + equalTo(UNCHECKED)); + + travelInTime(0); + assertThat(nonDailyHabit.getCheckmarks().getTodayValue(), + equalTo(CHECKED_EXPLICITLY)); + + travelInTime(1); + assertThat(nonDailyHabit.getCheckmarks().getTodayValue(), + equalTo(UNCHECKED)); + } + + @Test + public void test_getValues_withInvalidInterval() + { + int values[] = nonDailyHabit.getCheckmarks().getValues(100L, -100L); + assertThat(values, equalTo(new int[0])); + } + + @Test + public void test_getValues_withValidInterval() + { + long from = + DateUtils.getStartOfToday() - 15 * DateUtils.millisecondsInOneDay; + long to = + DateUtils.getStartOfToday() - 5 * DateUtils.millisecondsInOneDay; + + int[] expectedValues = { + CHECKED_EXPLICITLY, + UNCHECKED, + CHECKED_IMPLICITLY, + CHECKED_EXPLICITLY, + CHECKED_EXPLICITLY, + UNCHECKED, + UNCHECKED, + UNCHECKED, + UNCHECKED, + UNCHECKED, + UNCHECKED + }; + + int[] actualValues = nonDailyHabit.getCheckmarks().getValues(from, to); + + assertThat(actualValues, equalTo(expectedValues)); + } + + @Test + public void test_writeCSV() throws IOException + { + String expectedCSV = "2015-01-25,2\n" + + "2015-01-24,0\n" + + "2015-01-23,1\n" + + "2015-01-22,2\n" + + "2015-01-21,2\n" + + "2015-01-20,2\n" + + "2015-01-19,0\n" + + "2015-01-18,1\n" + + "2015-01-17,2\n" + + "2015-01-16,2\n"; + + + StringWriter writer = new StringWriter(); + nonDailyHabit.getCheckmarks().writeCSV(writer); + + assertThat(writer.toString(), equalTo(expectedCSV)); + } + + private void travelInTime(int days) + { + DateUtils.setFixedLocalTime( + FIXED_LOCAL_TIME + days * DateUtils.millisecondsInOneDay); + } +} diff --git a/app/src/test/java/org/isoron/uhabits/models/HabitFixtures.java b/app/src/test/java/org/isoron/uhabits/models/HabitFixtures.java new file mode 100644 index 000000000..70f7f1131 --- /dev/null +++ b/app/src/test/java/org/isoron/uhabits/models/HabitFixtures.java @@ -0,0 +1,92 @@ +/* + * 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.models; + +import org.isoron.uhabits.utils.DateUtils; + +public class HabitFixtures +{ + public boolean NON_DAILY_HABIT_CHECKS[] = { + true, false, false, true, true, true, false, false, true, true + }; + + private final HabitList habitList; + + public HabitFixtures(HabitList habitList) + { + this.habitList = habitList; + } + + public Habit createEmptyHabit() + { + Habit habit = new Habit(); + habit.setName("Meditate"); + habit.setDescription("Did you meditate this morning?"); + habit.setColor(3); + habit.setFreqNum(1); + habit.setFreqDen(1); + habitList.add(habit); + return habit; + } + + public Habit createLongHabit() + { + Habit habit = createEmptyHabit(); + habit.setFreqNum(3); + habit.setFreqDen(7); + habit.setColor(4); + + long day = DateUtils.millisecondsInOneDay; + long today = DateUtils.getStartOfToday(); + int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27, + 28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80, + 81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120}; + + for (int mark : marks) + habit.getRepetitions().toggleTimestamp(today - mark * day); + + return habit; + } + + public Habit createShortHabit() + { + Habit habit = new Habit(); + habit.setName("Wake up early"); + habit.setDescription("Did you wake up before 6am?"); + habit.setFreqNum(2); + habit.setFreqDen(3); + habitList.add(habit); + + long timestamp = DateUtils.getStartOfToday(); + for (boolean c : NON_DAILY_HABIT_CHECKS) + { + if (c) habit.getRepetitions().toggleTimestamp(timestamp); + timestamp -= DateUtils.millisecondsInOneDay; + } + + return habit; + } + + public void purgeHabits() + { + for (Habit h : habitList.getAll(true)) + habitList.remove(h); + } +} diff --git a/app/src/test/java/org/isoron/uhabits/models/HabitListTest.java b/app/src/test/java/org/isoron/uhabits/models/HabitListTest.java new file mode 100644 index 000000000..a79ad5959 --- /dev/null +++ b/app/src/test/java/org/isoron/uhabits/models/HabitListTest.java @@ -0,0 +1,200 @@ +/* + * 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.models; + +import org.hamcrest.MatcherAssert; +import org.isoron.uhabits.BaseUnitTest; +import org.isoron.uhabits.utils.DateUtils; +import org.junit.Test; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import static junit.framework.Assert.fail; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; + +public class HabitListTest extends BaseUnitTest +{ + private HabitList list; + + private ArrayList habits; + + @Override + public void setUp() + { + super.setUp(); + + list = modelFactory.buildHabitList(); + habits = new ArrayList<>(); + + for (int i = 0; i < 10; i++) + { + Habit habit = new Habit(); + habit.setId((long) i); + habits.add(habit); + list.add(habit); + + if (i % 3 == 0) + { + habit.setReminderDays(DateUtils.ALL_WEEK_DAYS); + habit.setReminderHour(8); + habit.setReminderMin(30); + } + } + + habits.get(0).setArchived(1); + habits.get(1).setArchived(1); + habits.get(4).setArchived(1); + habits.get(7).setArchived(1); + } + + @Test + public void test_count() + { + assertThat(list.count(), equalTo(6)); + } + + @Test + public void test_countWithArchived() + { + assertThat(list.countWithArchived(), equalTo(10)); + } + + @Test + public void test_getAll() + { + List filteredList = list.getAll(false); + + assertThat(filteredList.size(), equalTo(6)); + assertThat(filteredList.contains(habits.get(2)), is(true)); + assertThat(filteredList.contains(habits.get(4)), is(false)); + + filteredList = list.getAll(true); + + assertThat(filteredList.size(), equalTo(10)); + assertThat(filteredList.contains(habits.get(2)), is(true)); + assertThat(filteredList.contains(habits.get(4)), is(true)); + } + + @Test + public void test_getByPosition() + { + assertThat(list.getByPosition(0), equalTo(habits.get(0))); + assertThat(list.getByPosition(3), equalTo(habits.get(3))); + assertThat(list.getByPosition(9), equalTo(habits.get(9))); + } + + @Test + public void test_getHabitsWithReminder() + { + List filtered = list.getWithReminder(); + assertThat(filtered.size(), equalTo(4)); + assertThat(filtered.contains(habits.get(0)), equalTo(true)); + assertThat(filtered.contains(habits.get(1)), equalTo(false)); + } + + @Test + public void test_get_withInvalidId() + { + assertThat(list.getById(100L), is(nullValue())); + } + + @Test + public void test_get_withValidId() + { + Habit habit1 = habits.get(0); + Habit habit2 = list.getById(habit1.getId()); + assertThat(habit1, equalTo(habit2)); + } + + @Test + public void test_reorder() + { + int operations[][] = { + {5, 2}, {3, 7}, {4, 4}, {3, 2} + }; + + int expectedPosition[][] = { + {0, 1, 3, 4, 5, 2, 6, 7, 8, 9}, + {0, 1, 7, 3, 4, 2, 5, 6, 8, 9}, + {0, 1, 7, 3, 4, 2, 5, 6, 8, 9}, + {0, 1, 7, 2, 4, 3, 5, 6, 8, 9}, + }; + + for (int i = 0; i < operations.length; i++) + { + int from = operations[i][0]; + int to = operations[i][1]; + + Habit fromHabit = list.getByPosition(from); + Habit toHabit = list.getByPosition(to); + list.reorder(fromHabit, toHabit); + + int actualPositions[] = new int[10]; + + for (int j = 0; j < 10; j++) + { + Habit h = list.getById(j); + if (h == null) fail(); + actualPositions[j] = list.indexOf(h); + } + + assertThat(actualPositions, equalTo(expectedPosition[i])); + } + } + + @Test + public void test_writeCSV() throws IOException + { + HabitList list = modelFactory.buildHabitList(); + + Habit h1 = new Habit(); + h1.setName("Meditate"); + h1.setDescription("Did you meditate this morning?"); + h1.setFreqNum(1); + h1.setFreqDen(1); + h1.setColor(3); + + Habit h2 = new Habit(); + h2.setName("Wake up early"); + h2.setDescription("Did you wake up before 6am?"); + h2.setFreqNum(2); + h2.setFreqDen(3); + h2.setColor(5); + + list.add(h1); + list.add(h2); + + String expectedCSV = + "Position,Name,Description,NumRepetitions,Interval,Color\n" + + "001,Meditate,Did you meditate this morning?,1,1,#AFB42B\n" + + "002,Wake up early,Did you wake up before 6am?,2,3,#00897B\n"; + + StringWriter writer = new StringWriter(); + list.writeCSV(writer); + + MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV)); + } +} \ No newline at end of file diff --git a/app/src/test/java/org/isoron/uhabits/models/HabitTest.java b/app/src/test/java/org/isoron/uhabits/models/HabitTest.java new file mode 100644 index 000000000..443e97cf8 --- /dev/null +++ b/app/src/test/java/org/isoron/uhabits/models/HabitTest.java @@ -0,0 +1,114 @@ +/* + * 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.models; + +import org.isoron.uhabits.BaseUnitTest; +import org.isoron.uhabits.utils.DateUtils; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.assertThat; + +public class HabitTest extends BaseUnitTest +{ + + @Test + public void testConstructor_default() + { + Habit habit = new Habit(); + assertThat(habit.getArchived(), is(0)); + assertThat(habit.getHighlight(), is(0)); + + assertThat(habit.getReminderHour(), is(nullValue())); + assertThat(habit.getReminderMin(), is(nullValue())); + + assertThat(habit.getReminderDays(), is(not(nullValue()))); +// assertThat(habit.getStreaks(), is(not(nullValue()))); +// assertThat(habit.getScores(), is(not(nullValue()))); + assertThat(habit.getRepetitions(), is(not(nullValue()))); + assertThat(habit.getCheckmarks(), is(not(nullValue()))); + } + + @Test + public void test_copyAttributes() + { + Habit model = new Habit(); + model.setArchived(1); + model.setHighlight(1); + model.setColor(0); + model.setFreqNum(10); + model.setFreqDen(20); + model.setReminderDays(1); + model.setReminderHour(8); + model.setReminderMin(30); + + Habit habit = new Habit(); + habit.copyFrom(model); + assertThat(habit.getArchived(), is(model.getArchived())); + assertThat(habit.getHighlight(), is(model.getHighlight())); + assertThat(habit.getColor(), is(model.getColor())); + assertThat(habit.getFreqNum(), is(model.getFreqNum())); + assertThat(habit.getFreqDen(), is(model.getFreqDen())); + assertThat(habit.getReminderDays(), is(model.getReminderDays())); + assertThat(habit.getReminderHour(), is(model.getReminderHour())); + assertThat(habit.getReminderMin(), is(model.getReminderMin())); + } + +// @Test +// public void test_rebuildOrder() +// { +// List ids = new LinkedList<>(); +// int originalPositions[] = { 0, 1, 1, 4, 6, 8, 10, 10, 13}; +// +// for (int p : originalPositions) +// { +// Habit h = new Habit(); +// habitList.insert(h); +// ids.add(h.getId()); +// } +// +// ((SQLiteHabitList) habitList).rebuildOrder(); +// +// for (int i = 0; i < originalPositions.length; i++) +// { +// Habit h = habitList.get(ids.get(i)); +// if(h == null) fail(); +// assertThat(habitList.indexOf(h), is(i)); +// } +// } + + + @Test + public void test_hasReminder_clearReminder() + { + Habit h = new Habit(); + assertThat(h.hasReminder(), is(false)); + + h.setReminderDays(DateUtils.ALL_WEEK_DAYS); + h.setReminderHour(8); + h.setReminderMin(30); + assertThat(h.hasReminder(), is(true)); + + h.clearReminder(); + assertThat(h.hasReminder(), is(false)); + } +} \ No newline at end of file diff --git a/app/src/test/java/org/isoron/uhabits/models/RepetitionListTest.java b/app/src/test/java/org/isoron/uhabits/models/RepetitionListTest.java new file mode 100644 index 000000000..51dae4c14 --- /dev/null +++ b/app/src/test/java/org/isoron/uhabits/models/RepetitionListTest.java @@ -0,0 +1,179 @@ +/* + * 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.models; + +import android.support.annotation.NonNull; + +import org.isoron.uhabits.BaseUnitTest; +import org.isoron.uhabits.utils.DateUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +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.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; + +public class RepetitionListTest extends BaseUnitTest +{ + @NonNull + private RepetitionList reps; + + @NonNull + private Habit habit; + + private long today; + + private long day; + + @NonNull + private ModelObservable.Listener listener; + + @Override + @Before + public void setUp() + { + super.setUp(); + habit = new Habit(); + reps = habit.getRepetitions(); + + today = DateUtils.getStartOfToday(); + day = DateUtils.millisecondsInOneDay; + + reps.toggleTimestamp(today - 3 * day); + reps.toggleTimestamp(today - 2 * day); + reps.toggleTimestamp(today); + reps.toggleTimestamp(today - 7 * day); + reps.toggleTimestamp(today - 5 * day); + + listener = mock(ModelObservable.Listener.class); + reps.getObservable().addListener(listener); + } + + @Override + @After + public void tearDown() + { + super.tearDown(); + } + + @Test + public void test_contains() + { + assertThat(reps.containsTimestamp(today), is(true)); + assertThat(reps.containsTimestamp(today - 2 * day), is(true)); + assertThat(reps.containsTimestamp(today - 3 * day), is(true)); + + assertThat(reps.containsTimestamp(today - day), is(false)); + assertThat(reps.containsTimestamp(today - 4 * day), is(false)); + } + + @Test + public void test_getOldest() + { + Repetition rep = reps.getOldest(); + assertThat(rep.getTimestamp(), is(equalTo(today - 7 * day))); + } + + @Test + public void test_getWeekDayFrequency() + { + habit = new Habit(); + reps = habit.getRepetitions(); + + Random random = new Random(); + Integer weekdayCount[][] = new Integer[12][7]; + Integer monthCount[] = new Integer[12]; + + Arrays.fill(monthCount, 0); + for (Integer row[] : weekdayCount) + Arrays.fill(row, 0); + + GregorianCalendar day = DateUtils.getStartOfTodayCalendar(); + + // Sets the current date to the end of November + day.set(2015, 10, 30); + DateUtils.setFixedLocalTime(day.getTimeInMillis()); + + // Add repetitions randomly from January to December + // Leaves the month of March empty, to check that it returns null + day.set(2015, 0, 1); + for (int i = 0; i < 365; i++) + { + if (random.nextBoolean()) + { + int month = day.get(Calendar.MONTH); + int week = day.get(Calendar.DAY_OF_WEEK) % 7; + + if (month != 2) + { + if (month <= 10) + { + weekdayCount[month][week]++; + monthCount[month]++; + } + reps.toggleTimestamp(day.getTimeInMillis()); + } + } + + day.add(Calendar.DAY_OF_YEAR, 1); + } + + HashMap freq = + reps.getWeekdayFrequency(); + + // Repetitions until November should be counted correctly + for (int month = 0; month < 11; month++) + { + day.set(2015, month, 1); + Integer actualCount[] = freq.get(day.getTimeInMillis()); + if (monthCount[month] == 0) assertThat(actualCount, equalTo(null)); + else assertThat(actualCount, equalTo(weekdayCount[month])); + } + + // Repetitions in December should be discarded + day.set(2015, 11, 1); + assertThat(freq.get(day.getTimeInMillis()), equalTo(null)); + } + + @Test + public void test_toggleTimestamp() + { + assertThat(reps.containsTimestamp(today), equalTo(true)); + reps.toggleTimestamp(today); + assertThat(reps.containsTimestamp(today), equalTo(false)); + verify(listener).onModelChange(); + reset(listener); + + assertThat(reps.containsTimestamp(today - day), equalTo(false)); + reps.toggleTimestamp(today - day); + assertThat(reps.containsTimestamp(today - day), equalTo(true)); + verify(listener).onModelChange(); + } +} \ No newline at end of file diff --git a/app/src/test/java/org/isoron/uhabits/models/StreakListTest.java b/app/src/test/java/org/isoron/uhabits/models/StreakListTest.java new file mode 100644 index 000000000..3107b6a1b --- /dev/null +++ b/app/src/test/java/org/isoron/uhabits/models/StreakListTest.java @@ -0,0 +1,149 @@ +/* + * 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.models; + +import org.isoron.uhabits.BaseUnitTest; +import org.isoron.uhabits.utils.DateUtils; +import org.junit.Test; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class StreakListTest extends BaseUnitTest +{ + private Habit habit; + + private StreakList streaks; + + private long day; + + private long today; + + private ModelObservable.Listener listener; + + @Override + public void setUp() + { + super.setUp(); + habit = fixtures.createLongHabit(); + habit.setFreqDen(1); + habit.setFreqNum(1); + + streaks = habit.getStreaks(); + streaks.rebuild(); + + listener = mock(ModelObservable.Listener.class); + streaks.getObservable().addListener(listener); + + today = DateUtils.getStartOfToday(); + day = DateUtils.millisecondsInOneDay; + } + + @Test + public void testFindBeginning_withEmptyHistory() + { + Habit habit2 = fixtures.createEmptyHabit(); + Long beginning = habit2.getStreaks().findBeginning(); + assertThat(beginning, is(nullValue())); + } + + @Test + public void testFindBeginning_withLongHistory() + { + streaks.rebuild(); + assertThat(streaks.findBeginning(), equalTo(today - day)); + + streaks.invalidateNewerThan(today - 20 * day); + assertThat(streaks.findBeginning(), equalTo(today - 28 * day)); + + streaks.invalidateNewerThan(0); + assertThat(streaks.findBeginning(), equalTo(today - 120 * day)); + } + + @Test + public void testGetAll() throws Exception + { + List all = streaks.getAll(); + + assertThat(all.size(), equalTo(22)); + + assertThat(all.get(3).getEnd(), equalTo(today - 7 * day)); + assertThat(all.get(3).getStart(), equalTo(today - 10 * day)); + + assertThat(all.get(17).getEnd(), equalTo(today - 89 * day)); + assertThat(all.get(17).getStart(), equalTo(today - 91 * day)); + } + + @Test + public void testGetBest() throws Exception + { + List best = streaks.getBest(4); + assertThat(best.size(), equalTo(4)); + + assertThat(best.get(0).getLength(), equalTo(4L)); + assertThat(best.get(1).getLength(), equalTo(3L)); + assertThat(best.get(2).getLength(), equalTo(5L)); + assertThat(best.get(3).getLength(), equalTo(6L)); + + best = streaks.getBest(2); + assertThat(best.size(), equalTo(2)); + + assertThat(best.get(0).getLength(), equalTo(5L)); + assertThat(best.get(1).getLength(), equalTo(6L)); + } + + @Test + public void testGetNewestComputed() throws Exception + { + Streak s = streaks.getNewestComputed(); + assertThat(s.getEnd(), equalTo(today)); + assertThat(s.getStart(), equalTo(today - day)); + + streaks.invalidateNewerThan(today - 8 * day); + + s = streaks.getNewestComputed(); + assertThat(s.getEnd(), equalTo(today - 12 * day)); + assertThat(s.getStart(), equalTo(today - 12 * day)); + } + + @Test + public void testInvalidateNewer() + { + Streak s = streaks.getNewestComputed(); + assertThat(s.getEnd(), equalTo(today)); + + streaks.invalidateNewerThan(today - 8 * day); + verify(listener).onModelChange(); + + s = streaks.getNewestComputed(); + assertThat(s.getEnd(), equalTo(today - 12 * day)); + } + + private void log(List sl) + { + for (Streak s : sl) + log("%5d -- %5d (%d)", (today - s.getEnd()) / day, + (today - s.getStart()) / day, s.getLength()); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0382b84b4..aa8cddbe1 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0' + classpath 'com.android.tools.build:gradle:2.1.2' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.5.2' classpath 'me.tatarka:gradle-retrolambda:3.2.5' @@ -19,15 +19,3 @@ allprojects { maven { url "https://oss.sonatype.org/content/repositories/snapshots/" } } } - -project.ext.preDexLibs = !project.hasProperty('disablePreDex') - -subprojects { - project.plugins.whenPluginAdded { plugin -> - if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) { - project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs - } else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) { - project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs - } - } -} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d57051703..dd1562ebd 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 10 15:27:10 PDT 2013 +#Thu Jun 09 17:53:47 EDT 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip