Merge branch 'release/1.4.0'

pull/114/head v1.4.0
Alinson S. Xavier 10 years ago
commit 64c4367706

1
.gitignore vendored

@ -33,3 +33,4 @@ build/
*.iml *.iml
art/ art/
*.actual.png

@ -1,5 +1,18 @@
# Changelog # Changelog
### 1.4.0 (April 4, 2016)
* Ability to import data from third-party apps
* Ability to save and restore full database backup
* Show more information on streak chart
* Simplify interface for creating habits
* Add link to Frequently Asked Questions (FAQ)
* Reduce app loading time and lag on widgets
* Generate bug reports on crash and from settings screen
* Disable vibration according to phone settings
* Add Czech translation
* Fix wrong month names for some languages
### 1.3.3 (March 20, 2016) ### 1.3.3 (March 20, 2016)
* Add Spanish and Korean translations * Add Spanish and Korean translations

@ -89,3 +89,22 @@ Material design icons are the official icon set from Google that are designed
under the material design guidelines. Available under the Creative Common under the material design guidelines. Available under the Creative Common
Attribution 4.0 International License (CC-BY 4.0). Attribution 4.0 International License (CC-BY 4.0).
### Android Flow Layout
<https://github.com/ApmeM/android-flowlayout>
Extended linear layout that wrap its content when there is no place in the current line.
Copyright 2011, Artem Votincev (apmem.org)
Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy
of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.

@ -1,12 +1,20 @@
# Loop Habit Tracker # Loop Habit Tracker
<a href="https://circleci.com/gh/iSoron/uhabits/tree/dev">
<img src="https://img.shields.io/circleci/project/iSoron/uhabits/dev.svg">
</a>
<a href="https://codecov.io/github/iSoron/uhabits?branch=dev">
<img src="https://img.shields.io/codecov/c/github/iSoron/uhabits.svg" alt="Coverage via Codecov" />
</a>
Loop is a simple Android app that helps you create and maintain good habits, Loop is a simple Android app that helps you create and maintain good habits,
allowing you to achieve your long-term goals. Detailed graphs and statistics allowing you to achieve your long-term goals. Detailed graphs and statistics
show you how your habits improved over time. It is completely ad-free and open show you how your habits improved over time. It is completely ad-free and open
source. source.
<p align="center">
<a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a> <a href="https://play.google.com/store/apps/details?id=org.isoron.uhabits&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge-border.png" height="75px"/></a>
<a href="http://f-droid.org/app/org.isoron.uhabits"><img alt="Git if on F-Droid" src="http://i.imgur.com/baSPE7X.png" height="75px"/></a> <a href="http://f-droid.org/app/org.isoron.uhabits"><img alt="Git if on F-Droid" src="http://i.imgur.com/baSPE7X.png" height="75px"/></a>
</p>
## Features ## Features
@ -65,9 +73,14 @@ contribute, even if you are not a software developer.
to improve it. You can either use the link inside the app, or open an issue to improve it. You can either use the link inside the app, or open an issue
at GitHub. at GitHub.
* **Spread the word.** If you like the app, share it with your family, friends
and colleagues. You can also rate and review the app on Google Play Store, to help
other users find it more easily.
* **Translate the app into your own language.** If you are not a native English * **Translate the app into your own language.** If you are not a native English
speaker, and would like to see the app translated into your own language, speaker, and would like to see the app translated into your own language,
please join our [open translation project at POEditor][poedit]. please join our [open translation project at POEditor][poedit]. If the translation
is already completed, you are also very welcome to join and proofread it.
* **Write some code.** If you are an Android developer, you are very welcome to * **Write some code.** If you are an Android developer, you are very welcome to
contribute with code. Please, see the [developer guidelines][dev-guide] for more details. contribute with code. Please, see the [developer guidelines][dev-guide] for more details.

@ -2,14 +2,18 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion 23 compileSdkVersion 23
buildToolsVersion "21.1.2" buildToolsVersion "23.0.1"
defaultConfig { defaultConfig {
applicationId "org.isoron.uhabits" applicationId "org.isoron.uhabits"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 23 targetSdkVersion 23
buildConfigField "Integer", "databaseVersion", "13"
buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//testInstrumentationRunnerArgument "size", "small"
} }
buildTypes { buildTypes {
@ -30,6 +34,8 @@ android {
dependencies { dependencies {
compile 'com.android.support:support-v4:23.1.1' compile 'com.android.support:support-v4:23.1.1'
compile 'com.github.paolorotolo:appintro:3.4.0' compile 'com.github.paolorotolo:appintro:3.4.0'
compile 'org.apmem.tools:layouts:1.10@aar'
compile 'com.opencsv:opencsv:3.7'
compile project(':libs:drag-sort-listview:library') compile project(':libs:drag-sort-listview:library')
compile files('libs/ActiveAndroid.jar') compile files('libs/ActiveAndroid.jar')
@ -40,3 +46,13 @@ dependencies {
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1' androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1'
} }
task grantAnimationPermission(type: Exec, dependsOn: 'installDebug') {
commandLine "adb shell pm grant org.isoron.uhabits android.permission.SET_ANIMATION_SCALE".split(' ')
}
tasks.whenTaskAdded { task ->
if (task.name.startsWith('connected')) {
task.dependsOn grantAnimationPermission
}
}

@ -0,0 +1,19 @@
HabitName,HabitDescription,HabitCategory,CalendarDate,Value,CommentText
Breed dragons,with love and fire,Diet & Food,2016-03-18,1,
Breed dragons,with love and fire,Diet & Food,2016-03-19,1,
Breed dragons,with love and fire,Diet & Food,2016-03-21,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-15,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-16,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-17,1,
Reduce sleep,only 2 hours per day,Time Management,2016-03-21,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-15,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-16,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-18,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-21,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-15,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-16,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-18,1,
No-arms pushup,Become like water my friend!,Fitness,2016-03-21,1,
Grow spiritually,"transcend ego, practice compassion, smile and breath",Meditation,2016-03-15,1,
Grow spiritually,"transcend ego, practice compassion, smile and breath",Meditation,2016-03-17,1,
Grow spiritually,"transcend ego, practice compassion, smile and breath",Meditation,2016-03-21,1,
1 HabitName HabitDescription HabitCategory CalendarDate Value CommentText
2 Breed dragons with love and fire Diet & Food 2016-03-18 1
3 Breed dragons with love and fire Diet & Food 2016-03-19 1
4 Breed dragons with love and fire Diet & Food 2016-03-21 1
5 Reduce sleep only 2 hours per day Time Management 2016-03-15 1
6 Reduce sleep only 2 hours per day Time Management 2016-03-16 1
7 Reduce sleep only 2 hours per day Time Management 2016-03-17 1
8 Reduce sleep only 2 hours per day Time Management 2016-03-21 1
9 No-arms pushup Become like water my friend! Fitness 2016-03-15 1
10 No-arms pushup Become like water my friend! Fitness 2016-03-16 1
11 No-arms pushup Become like water my friend! Fitness 2016-03-18 1
12 No-arms pushup Become like water my friend! Fitness 2016-03-21 1
13 No-arms pushup Become like water my friend! Fitness 2016-03-15 1
14 No-arms pushup Become like water my friend! Fitness 2016-03-16 1
15 No-arms pushup Become like water my friend! Fitness 2016-03-18 1
16 No-arms pushup Become like water my friend! Fitness 2016-03-21 1
17 Grow spiritually transcend ego, practice compassion, smile and breath Meditation 2016-03-15 1
18 Grow spiritually transcend ego, practice compassion, smile and breath Meditation 2016-03-17 1
19 Grow spiritually transcend ego, practice compassion, smile and breath Meditation 2016-03-21 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@ -0,0 +1,5 @@
#!/bin/bash
P=/sdcard/Android/data/org.isoron.uhabits/cache/Failed/
adb pull $P Failed/
adb shell rm -r $P

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -0,0 +1,66 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import android.content.Context;
import android.os.Build;
import android.os.Looper;
import android.support.test.InstrumentationRegistry;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.tasks.BaseTask;
import org.junit.Before;
import java.util.concurrent.TimeoutException;
public class BaseTest
{
protected Context testContext;
protected Context targetContext;
private static boolean isLooperPrepared;
public static final long FIXED_LOCAL_TIME = 1422172800000L; // 8:00am, January 25th, 2015 (UTC)
@Before
public void setup()
{
if(!isLooperPrepared)
{
Looper.prepare();
isLooperPrepared = true;
}
targetContext = InstrumentationRegistry.getTargetContext();
testContext = InstrumentationRegistry.getContext();
DateHelper.setFixedLocalTime(FIXED_LOCAL_TIME);
}
protected void waitForAsyncTasks() throws InterruptedException, TimeoutException
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
{
Thread.sleep(1000);
return;
}
BaseTask.waitForTasks(10000);
}
}

@ -1,200 +0,0 @@
package org.isoron.uhabits;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.intent.rule.IntentsTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import org.isoron.uhabits.models.Habit;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static android.support.test.espresso.Espresso.pressBack;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.longClick;
import static android.support.test.espresso.action.ViewActions.scrollTo;
import static android.support.test.espresso.action.ViewActions.swipeLeft;
import static android.support.test.espresso.action.ViewActions.swipeRight;
import static android.support.test.espresso.action.ViewActions.swipeUp;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.isoron.uhabits.HabitMatchers.withName;
import static org.isoron.uhabits.HabitViewActions.clickAtRandomLocations;
import static org.isoron.uhabits.HabitViewActions.toggleAllCheckmarks;
import static org.isoron.uhabits.MainActivityActions.addHabit;
import static org.isoron.uhabits.MainActivityActions.assertHabitExists;
import static org.isoron.uhabits.MainActivityActions.assertHabitsDontExist;
import static org.isoron.uhabits.MainActivityActions.assertHabitsExist;
import static org.isoron.uhabits.MainActivityActions.clickActionModeMenuItem;
import static org.isoron.uhabits.MainActivityActions.deleteHabit;
import static org.isoron.uhabits.MainActivityActions.deleteHabits;
import static org.isoron.uhabits.MainActivityActions.selectHabit;
import static org.isoron.uhabits.MainActivityActions.selectHabits;
import static org.isoron.uhabits.MainActivityActions.typeHabitData;
import static org.isoron.uhabits.ShowHabitActivityActions.openHistoryEditor;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainTest
{
@Rule
public IntentsTestRule<MainActivity> activityRule = new IntentsTestRule<>(
MainActivity.class);
@Before
public void skipTutorial()
{
try
{
for (int i = 0; i < 10; i++)
onView(allOf(withClassName(endsWith("AppCompatImageButton")),
isDisplayed())).perform(click());
}
catch (NoMatchingViewException e)
{
// ignored
}
}
@Test
public void testArchiveHabits()
{
List<String> names = new LinkedList<>();
Context context = InstrumentationRegistry.getTargetContext();
for(int i = 0; i < 3; i++)
names.add(addHabit());
selectHabits(names);
clickActionModeMenuItem(R.string.archive);
assertHabitsDontExist(names);
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.show_archived))
.perform(click());
assertHabitsExist(names);
selectHabits(names);
clickActionModeMenuItem(R.string.unarchive);
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.show_archived))
.perform(click());
assertHabitsExist(names);
deleteHabits(names);
}
@Test
public void testAddInvalidHabit()
{
onView(withId(R.id.action_add))
.perform(click());
typeHabitData("", "", "15", "7");
onView(withId(R.id.buttonSave)).perform(click());
onView(withId(R.id.input_name)).check(matches(isDisplayed()));
}
@Test
public void testAddHabitAndViewStats()
{
String name = addHabit(true);
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.llButtons))
.perform(toggleAllCheckmarks());
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(click());
onView(withId(R.id.scoreView))
.perform(swipeRight());
onView(withId(R.id.punchcardView))
.perform(scrollTo());
}
@Test
public void testEditHabit()
{
String name = addHabit();
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(longClick());
clickActionModeMenuItem(R.string.edit);
String modifiedName = "Modified " + new Random().nextInt(10000);
typeHabitData(modifiedName, "", "1", "1");
onView(withId(R.id.buttonSave))
.perform(click());
assertHabitExists(modifiedName);
selectHabit(modifiedName);
clickActionModeMenuItem(R.string.color_picker_default_title);
pressBack();
deleteHabit(modifiedName);
}
@Test
public void testEditHistory()
{
String name = addHabit();
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(click());
openHistoryEditor();
onView(withClassName(endsWith("HabitHistoryView")))
.perform(clickAtRandomLocations(20));
pressBack();
onView(withId(R.id.historyView))
.perform(scrollTo(), swipeRight(), swipeLeft());
}
@Test
public void testSettings()
{
Context context = InstrumentationRegistry.getContext();
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.settings)).perform(click());
}
@Test
public void testAbout()
{
Context context = InstrumentationRegistry.getContext();
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.about)).perform(click());
onView(isRoot()).perform(swipeUp());
}
}

@ -17,12 +17,14 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits; package org.isoron.uhabits.ui;
import android.preference.Preference;
import android.view.View; import android.view.View;
import android.widget.Adapter; import android.widget.Adapter;
import android.widget.AdapterView; import android.widget.AdapterView;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description; import org.hamcrest.Description;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher; import org.hamcrest.TypeSafeMatcher;
@ -76,4 +78,23 @@ public class HabitMatchers
} }
}; };
} }
public static Matcher<?> isPreferenceWithText(final String text)
{
return (Matcher<?>) new BaseMatcher()
{
@Override
public boolean matches(Object o)
{
if(!(o instanceof Preference)) return false;
return o.toString().contains(text);
}
@Override
public void describeTo(Description description)
{
description.appendText(String.format("is preference with text '%s'", text));
}
};
}
} }

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits; package org.isoron.uhabits.ui;
import android.support.test.espresso.UiController; import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction; import android.support.test.espresso.ViewAction;
@ -32,6 +32,7 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.isoron.uhabits.R;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.Random; import java.util.Random;

@ -17,11 +17,11 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits; package org.isoron.uhabits.ui;
import android.content.Context; import android.support.test.espresso.NoMatchingViewException;
import android.support.test.InstrumentationRegistry;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.util.Collections; import java.util.Collections;
@ -37,16 +37,20 @@ import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.longClick; import static android.support.test.espresso.action.ViewActions.longClick;
import static android.support.test.espresso.action.ViewActions.replaceText; import static android.support.test.espresso.action.ViewActions.replaceText;
import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.RootMatchers.isPlatformPopup;
import static android.support.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription; import static android.support.test.espresso.matcher.ViewMatchers.withContentDescription;
import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText; import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.isoron.uhabits.HabitMatchers.containsHabit; import static org.hamcrest.Matchers.startsWith;
import static org.isoron.uhabits.HabitMatchers.withName; import static org.isoron.uhabits.ui.HabitMatchers.containsHabit;
import static org.isoron.uhabits.ui.HabitMatchers.withName;
public class MainActivityActions public class MainActivityActions
{ {
@ -97,6 +101,20 @@ public class MainActivityActions
.perform(replaceText(name)); .perform(replaceText(name));
onView(withId(R.id.input_description)) onView(withId(R.id.input_description))
.perform(replaceText(description)); .perform(replaceText(description));
try
{
onView(allOf(withId(R.id.sFrequency), withEffectiveVisibility(VISIBLE)))
.perform(click());
onData(allOf(instanceOf(String.class), startsWith("Custom")))
.inRoot(isPlatformPopup())
.perform(click());
}
catch(NoMatchingViewException e)
{
// ignored
}
onView(withId(R.id.input_freq_num)) onView(withId(R.id.input_freq_num))
.perform(replaceText(num)); .perform(replaceText(num));
onView(withId(R.id.input_freq_den)) onView(withId(R.id.input_freq_den))

@ -0,0 +1,359 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.content.Intent;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.intent.rule.IntentsTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.LargeTest;
import org.isoron.uhabits.MainActivity;
import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static android.support.test.espresso.Espresso.pressBack;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.longClick;
import static android.support.test.espresso.action.ViewActions.scrollTo;
import static android.support.test.espresso.action.ViewActions.swipeLeft;
import static android.support.test.espresso.action.ViewActions.swipeRight;
import static android.support.test.espresso.action.ViewActions.swipeUp;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.intent.Intents.intended;
import static android.support.test.espresso.intent.Intents.intending;
import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.isoron.uhabits.ui.HabitMatchers.isPreferenceWithText;
import static org.isoron.uhabits.ui.HabitMatchers.withName;
import static org.isoron.uhabits.ui.HabitViewActions.clickAtRandomLocations;
import static org.isoron.uhabits.ui.HabitViewActions.toggleAllCheckmarks;
import static org.isoron.uhabits.ui.MainActivityActions.addHabit;
import static org.isoron.uhabits.ui.MainActivityActions.assertHabitExists;
import static org.isoron.uhabits.ui.MainActivityActions.assertHabitsDontExist;
import static org.isoron.uhabits.ui.MainActivityActions.assertHabitsExist;
import static org.isoron.uhabits.ui.MainActivityActions.clickActionModeMenuItem;
import static org.isoron.uhabits.ui.MainActivityActions.deleteHabit;
import static org.isoron.uhabits.ui.MainActivityActions.deleteHabits;
import static org.isoron.uhabits.ui.MainActivityActions.selectHabit;
import static org.isoron.uhabits.ui.MainActivityActions.selectHabits;
import static org.isoron.uhabits.ui.MainActivityActions.typeHabitData;
import static org.isoron.uhabits.ui.ShowHabitActivityActions.openHistoryEditor;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainTest
{
private SystemHelper sys;
@Rule
public IntentsTestRule<MainActivity> activityRule = new IntentsTestRule<>(
MainActivity.class);
private Context targetContext;
@Before
public void setup()
{
Context context = InstrumentationRegistry.getInstrumentation().getContext();
sys = new SystemHelper(context);
sys.disableAllAnimations();
sys.acquireWakeLock();
sys.unlockScreen();
targetContext = InstrumentationRegistry.getTargetContext();
Instrumentation.ActivityResult okResult = new Instrumentation.ActivityResult(
Activity.RESULT_OK, new Intent());
intending(hasAction(equalTo(Intent.ACTION_SEND))).respondWith(okResult);
intending(hasAction(equalTo(Intent.ACTION_SENDTO))).respondWith(okResult);
intending(hasAction(equalTo(Intent.ACTION_VIEW))).respondWith(okResult);
skipTutorial();
}
@After
public void tearDown()
{
sys.releaseWakeLock();
}
public void skipTutorial()
{
try
{
for (int i = 0; i < 10; i++)
onView(allOf(withClassName(endsWith("AppCompatImageButton")),
isDisplayed())).perform(click());
}
catch (NoMatchingViewException e)
{
// ignored
}
}
/**
* User opens the app, creates some habits, selects them, archives them, select 'show archived'
* on the menu, selects the previously archived habits and then deletes them.
*/
@Test
public void testArchiveHabits()
{
List<String> names = new LinkedList<>();
for(int i = 0; i < 3; i++)
names.add(addHabit());
selectHabits(names);
clickActionModeMenuItem(R.string.archive);
assertHabitsDontExist(names);
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.show_archived))
.perform(click());
assertHabitsExist(names);
selectHabits(names);
clickActionModeMenuItem(R.string.unarchive);
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.show_archived))
.perform(click());
assertHabitsExist(names);
deleteHabits(names);
}
/**
* User opens the app, clicks the add button, types some bogus information, tries to save,
* dialog displays an error.
*/
@Test
public void testAddInvalidHabit()
{
onView(withId(R.id.action_add))
.perform(click());
typeHabitData("", "", "15", "7");
onView(withId(R.id.buttonSave)).perform(click());
onView(withId(R.id.input_name)).check(matches(isDisplayed()));
}
/**
* User creates a habit, toggles a bunch of checkmarks, clicks the habit to open the statistics
* screen, scrolls down to some views, then scrolls the views backwards and forwards in time.
*/
@Test
public void testAddHabitAndViewStats() throws InterruptedException
{
String name = addHabit(true);
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.llButtons))
.perform(toggleAllCheckmarks());
Thread.sleep(1200);
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(click());
onView(withId(R.id.scoreView))
.perform(scrollTo(), swipeRight());
onView(withId(R.id.punchcardView))
.perform(scrollTo(), swipeRight());
}
/**
* User creates a habit, selects the habit, clicks edit button, changes some information about
* the habit, click save button, sees changes on the main window, selects habit again,
* changes color, then deletes the habit.
*/
@Test
public void testEditHabit()
{
String name = addHabit();
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(longClick());
clickActionModeMenuItem(R.string.edit);
String modifiedName = "Modified " + new Random().nextInt(10000);
typeHabitData(modifiedName, "", "1", "1");
onView(withId(R.id.buttonSave))
.perform(click());
assertHabitExists(modifiedName);
selectHabit(modifiedName);
clickActionModeMenuItem(R.string.color_picker_default_title);
pressBack();
deleteHabit(modifiedName);
}
/**
* User creates a habit, opens statistics page, clicks button to edit history, adds some
* checkmarks, closes dialog, sees the modified history calendar.
*/
@Test
public void testEditHistory()
{
String name = addHabit();
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(click());
openHistoryEditor();
onView(withClassName(endsWith("HabitHistoryView")))
.perform(clickAtRandomLocations(20));
pressBack();
onView(withId(R.id.historyView))
.perform(scrollTo(), swipeRight(), swipeLeft());
}
/**
* User opens menu, clicks settings, sees settings screen.
*/
@Test
public void testSettings()
{
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.settings)).perform(click());
}
/**
* User opens menu, clicks about, sees about screen.
*/
@Test
public void testAbout()
{
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.about)).perform(click());
onView(isRoot()).perform(swipeUp());
}
/**
* User opens menu, clicks Help, sees website.
*/
@Test
public void testHelp()
{
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.help)).perform(click());
intended(hasAction(Intent.ACTION_VIEW));
}
/**
* User creates a habit, exports full backup, deletes the habit, restores backup, sees that the
* previously created habit has appeared back.
*/
@Test
public void testExportImportDB()
{
String name = addHabit();
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.settings)).perform(click());
String date = DateHelper.getBackupDateFormat().format(DateHelper.getLocalTime());
date = date.substring(0, date.length() - 2);
onData(isPreferenceWithText("Export full backup")).perform(click());
intended(hasAction(Intent.ACTION_SEND));
deleteHabit(name);
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.settings)).perform(click());
onData(isPreferenceWithText("Import data")).perform(click());
onData(allOf(is(instanceOf(String.class)), startsWith("Backups")))
.perform(click());
onData(allOf(is(instanceOf(String.class)), containsString(date)))
.perform(click());
selectHabit(name);
}
/**
* User creates a habit, opens settings, clicks export as CSV, is asked what activity should
* handle the file.
*/
@Test
public void testExportCSV()
{
addHabit();
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.settings)).perform(click());
onData(isPreferenceWithText("Export as CSV")).perform(click());
intended(hasAction(Intent.ACTION_SEND));
}
/**
* User opens the settings and generates a bug report.
*/
@Test
public void testGenerateBugReport()
{
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.settings)).perform(click());
onData(isPreferenceWithText("Generate bug report")).perform(click());
intended(hasAction(Intent.ACTION_SENDTO));
}
}

@ -17,18 +17,21 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits; package org.isoron.uhabits.ui;
import android.support.test.espresso.matcher.ViewMatchers;
import org.isoron.uhabits.R;
import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click; import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.scrollTo; import static android.support.test.espresso.action.ViewActions.scrollTo;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
public class ShowHabitActivityActions public class ShowHabitActivityActions
{ {
public static void openHistoryEditor() public static void openHistoryEditor()
{ {
onView(withId(R.id.btEditHistory)) onView(ViewMatchers.withId(R.id.btEditHistory))
.perform(scrollTo(), click()); .perform(scrollTo(), click());
} }
} }

@ -0,0 +1,105 @@
package org.isoron.uhabits.ui;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.test.runner.AndroidJUnitRunner;
import android.util.Log;
import java.lang.reflect.Method;
public final class SystemHelper extends AndroidJUnitRunner
{
private static final String ANIMATION_PERMISSION = "android.permission.SET_ANIMATION_SCALE";
private static final float DISABLED = 0.0f;
private static final float DEFAULT = 1.0f;
private final Context context;
private PowerManager.WakeLock wakeLock;
SystemHelper(Context context)
{
this.context = context;
}
void acquireWakeLock()
{
PowerManager power = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
wakeLock = power.newWakeLock(PowerManager.FULL_WAKE_LOCK |
PowerManager.ACQUIRE_CAUSES_WAKEUP |
PowerManager.ON_AFTER_RELEASE, getClass().getSimpleName());
wakeLock.acquire();
}
void releaseWakeLock()
{
if(wakeLock != null)
wakeLock.release();
}
void unlockScreen()
{
Log.i("SystemHelper", "Trying to unlock screen");
try
{
KeyguardManager mKeyGuardManager =
(KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
KeyguardManager.KeyguardLock mLock = mKeyGuardManager.newKeyguardLock("lock");
mLock.disableKeyguard();
Log.e("SystemHelper", "Successfully unlocked screen");
} catch (Exception e)
{
Log.e("SystemHelper", "Could not unlock screen");
e.printStackTrace();
}
}
void disableAllAnimations()
{
Log.i("SystemHelper", "Trying to disable animations");
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
if (permStatus == PackageManager.PERMISSION_GRANTED) setSystemAnimationsScale(DISABLED);
else Log.e("SystemHelper", "Permission denied");
}
void enableAllAnimations()
{
int permStatus = context.checkCallingOrSelfPermission(ANIMATION_PERMISSION);
if (permStatus == PackageManager.PERMISSION_GRANTED)
{
setSystemAnimationsScale(DEFAULT);
}
}
private void setSystemAnimationsScale(float animationScale)
{
try
{
Class<?> windowManagerStubClazz = Class.forName("android.view.IWindowManager$Stub");
Method asInterface =
windowManagerStubClazz.getDeclaredMethod("asInterface", IBinder.class);
Class<?> serviceManagerClazz = Class.forName("android.os.ServiceManager");
Method getService = serviceManagerClazz.getDeclaredMethod("getService", String.class);
Class<?> windowManagerClazz = Class.forName("android.view.IWindowManager");
Method setAnimationScales =
windowManagerClazz.getDeclaredMethod("setAnimationScales", float[].class);
Method getAnimationScales = windowManagerClazz.getDeclaredMethod("getAnimationScales");
IBinder windowManagerBinder = (IBinder) getService.invoke(null, "window");
Object windowManagerObj = asInterface.invoke(null, windowManagerBinder);
float[] currentScales = (float[]) getAnimationScales.invoke(windowManagerObj);
for (int i = 0; i < currentScales.length; i++)
currentScales[i] = animationScale;
setAnimationScales.invoke(windowManagerObj, new Object[]{currentScales});
Log.i("SystemHelper", "All animations successfully disabled");
}
catch (Exception e)
{
Log.e("SystemHelper", "Could not change animation scale to " + animationScale + " :'(");
}
}
}

@ -0,0 +1,169 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper;
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;
public class HabitFixtures
{
public static 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();
long timestamp = DateHelper.getStartOfToday();
for(boolean c : NON_DAILY_HABIT_CHECKS)
{
if(c) habit.repetitions.toggle(timestamp);
timestamp -= DateHelper.millisecondsInOneDay;
}
return habit;
}
public static Habit createEmptyHabit()
{
Habit habit = new Habit();
habit.name = "Meditate";
habit.description = "Did you meditate this morning?";
habit.color = ColorHelper.palette[3];
habit.freqNum = 1;
habit.freqDen = 1;
habit.save();
return habit;
}
public static Habit createLongHabit()
{
Habit habit = createEmptyHabit();
habit.freqNum = 3;
habit.freqDen = 7;
habit.color = ColorHelper.palette[4];
habit.save();
long day = DateHelper.millisecondsInOneDay;
long today = DateHelper.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.repetitions.toggle(today - mark * day);
return habit;
}
public static void generateHugeDataSet() throws Throwable
{
final int nHabits = 30;
final int nYears = 5;
DatabaseHelper.executeAsTransaction(new DatabaseHelper.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 = DateHelper.getStartOfToday();
long day = DateHelper.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);
}
}
});
ExportDBTask task = new ExportDBTask(null);
task.setListener(new ExportDBTask.Listener()
{
@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 = DatabaseHelper.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");
DatabaseHelper.copy(in, dst);
ImportDataTask task = new ImportDataTask(dst, null);
task.execute();
BaseTask.waitForTasks(30000);
}
public static void purgeHabits()
{
for(Habit h : Habit.getAll(true))
h.cascadeDelete();
}
}

@ -0,0 +1,51 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit;
import android.os.Build;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.HabitsApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitsApplicationTest
{
@Test
public void test_getLogcat() throws IOException
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
return;
String msg = "LOGCAT TEST";
new RuntimeException(msg).printStackTrace();
String log = HabitsApplication.getLogcat();
assertThat(log, containsString(msg));
}
}

@ -0,0 +1,69 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.commands.ArchiveHabitsCommand;
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.util.Collections;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ArchiveHabitsCommandTest extends BaseTest
{
private ArchiveHabitsCommand command;
private Habit habit;
@Before
public void setup()
{
super.setup();
habit = HabitFixtures.createShortHabit();
command = new ArchiveHabitsCommand(Collections.singletonList(habit));
}
@Test
public void testExecuteUndoRedo()
{
assertFalse(habit.isArchived());
command.execute();
assertTrue(habit.isArchived());
command.undo();
assertFalse(habit.isArchived());
command.execute();
assertTrue(habit.isArchived());
}
}

@ -0,0 +1,91 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.helpers.ColorHelper;
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.util.LinkedList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ChangeHabitColorCommandTest extends BaseTest
{
private ChangeHabitColorCommand command;
private LinkedList<Habit> habits;
@Before
public void setup()
{
super.setup();
habits = new LinkedList<>();
for(int i = 0; i < 3; i ++)
{
Habit habit = HabitFixtures.createShortHabit();
habit.color = ColorHelper.palette[i+1];
habit.save();
habits.add(habit);
}
command = new ChangeHabitColorCommand(habits, ColorHelper.palette[0]);
}
@Test
public void testExecuteUndoRedo()
{
checkOriginalColors();
command.execute();
checkNewColors();
command.undo();
checkOriginalColors();
command.execute();
checkNewColors();
}
private void checkOriginalColors()
{
int k = 0;
for(Habit h : habits)
assertThat(h.color, equalTo(ColorHelper.palette[++k]));
}
private void checkNewColors()
{
for(Habit h : habits)
assertThat(h.color, equalTo(ColorHelper.palette[0]));
}
}

@ -0,0 +1,85 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
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;
import java.util.List;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CreateHabitCommandTest extends BaseTest
{
private CreateHabitCommand command;
private Habit model;
@Before
public void setup()
{
super.setup();
model = new Habit();
model.name = "New habit";
command = new CreateHabitCommand(model);
HabitFixtures.purgeHabits();
}
@Test
public void testExecuteUndoRedo()
{
assertTrue(Habit.getAll(true).isEmpty());
command.execute();
List<Habit> allHabits = Habit.getAll(true);
assertThat(allHabits.size(), equalTo(1));
Habit habit = allHabits.get(0);
Long id = habit.getId();
assertThat(habit.name, equalTo(model.name));
command.undo();
assertTrue(Habit.getAll(true).isEmpty());
command.execute();
allHabits = Habit.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));
}
}

@ -0,0 +1,85 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
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;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import java.util.LinkedList;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class DeleteHabitsCommandTest extends BaseTest
{
private DeleteHabitsCommand command;
private LinkedList<Habit> habits;
@Rule
public ExpectedException thrown = ExpectedException.none();
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
habits = new LinkedList<>();
// Habits that shuold be deleted
for(int i = 0; i < 3; i ++)
{
Habit habit = HabitFixtures.createShortHabit();
habits.add(habit);
}
// Extra habit that should not be deleted
Habit extraHabit = HabitFixtures.createShortHabit();
extraHabit.name = "extra";
extraHabit.save();
command = new DeleteHabitsCommand(habits);
}
@Test
public void testExecuteUndoRedo()
{
assertThat(Habit.getAll(true).size(), equalTo(4));
command.execute();
assertThat(Habit.getAll(true).size(), equalTo(1));
assertThat(Habit.getAll(true).get(0).name, equalTo("extra"));
thrown.expect(UnsupportedOperationException.class);
command.undo();
}
}

@ -0,0 +1,120 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
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;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class EditHabitCommandTest extends BaseTest
{
private EditHabitCommand command;
private Habit habit;
private Habit modified;
private Long id;
@Before
public void setup()
{
super.setup();
habit = HabitFixtures.createShortHabit();
habit.name = "original";
habit.freqDen = 1;
habit.freqNum = 1;
habit.save();
id = habit.getId();
modified = new Habit(habit);
modified.name = "modified";
}
@Test
public void testExecuteUndoRedo()
{
command = new EditHabitCommand(habit, modified);
int originalScore = habit.scores.getTodayValue();
assertThat(habit.name, equalTo("original"));
command.execute();
refreshHabit();
assertThat(habit.name, equalTo("modified"));
assertThat(habit.scores.getTodayValue(), equalTo(originalScore));
command.undo();
refreshHabit();
assertThat(habit.name, equalTo("original"));
assertThat(habit.scores.getTodayValue(), equalTo(originalScore));
command.execute();
refreshHabit();
assertThat(habit.name, equalTo("modified"));
assertThat(habit.scores.getTodayValue(), equalTo(originalScore));
}
@Test
public void testExecuteUndoRedo_withModifiedInterval()
{
modified.freqNum = 1;
modified.freqDen = 7;
command = new EditHabitCommand(habit, modified);
int originalScore = habit.scores.getTodayValue();
assertThat(habit.name, equalTo("original"));
command.execute();
refreshHabit();
assertThat(habit.name, equalTo("modified"));
assertThat(habit.scores.getTodayValue(), greaterThan(originalScore));
command.undo();
refreshHabit();
assertThat(habit.name, equalTo("original"));
assertThat(habit.scores.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);
}
}

@ -0,0 +1,71 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.helpers.DateHelper;
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.assertFalse;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ToggleRepetitionCommandTest extends BaseTest
{
private ToggleRepetitionCommand command;
private Habit habit;
private long today;
@Before
public void setup()
{
super.setup();
habit = HabitFixtures.createShortHabit();
today = DateHelper.getStartOfToday();
command = new ToggleRepetitionCommand(habit, today);
}
@Test
public void testExecuteUndoRedo()
{
assertTrue(habit.repetitions.contains(today));
command.execute();
assertFalse(habit.repetitions.contains(today));
command.undo();
assertTrue(habit.repetitions.contains(today));
command.execute();
assertFalse(habit.repetitions.contains(today));
}
}

@ -0,0 +1,71 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.commands;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
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;
import java.util.Collections;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class UnarchiveHabitsCommandTest extends BaseTest
{
private UnarchiveHabitsCommand command;
private Habit habit;
@Before
public void setup()
{
super.setup();
habit = HabitFixtures.createShortHabit();
Habit.archive(Collections.singletonList(habit));
command = new UnarchiveHabitsCommand(Collections.singletonList(habit));
}
@Test
public void testExecuteUndoRedo()
{
assertTrue(habit.isArchived());
command.execute();
assertFalse(habit.isArchived());
command.undo();
assertTrue(habit.isArchived());
command.execute();
assertFalse(habit.isArchived());
}
}

@ -0,0 +1,118 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.io;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.io.HabitsCSVExporter;
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.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitsCSVExporterTest extends BaseTest
{
private File baseDir;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
HabitFixtures.createShortHabit();
HabitFixtures.createEmptyHabit();
Context targetContext = InstrumentationRegistry.getTargetContext();
baseDir = targetContext.getCacheDir();
}
private void unzip(File file) throws IOException
{
ZipFile zip = new ZipFile(file);
Enumeration<? extends ZipEntry> 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();
DatabaseHelper.copy(stream, outputFile);
}
zip.close();
}
@Test
public void testExportCSV() throws IOException
{
List<Habit> habits = Habit.getAll(true);
HabitsCSVExporter exporter = new HabitsCSVExporter(habits, baseDir);
String filename = exporter.writeArchive();
assertAbsolutePathExists(filename);
File archive = new File(filename);
unzip(archive);
assertPathExists("Habits.csv");
assertPathExists("001 Wake up early");
assertPathExists("001 Wake up early/Checkmarks.csv");
assertPathExists("001 Wake up early/Scores.csv");
assertPathExists("002 Meditate/Checkmarks.csv");
assertPathExists("002 Meditate/Scores.csv");
}
private void assertPathExists(String s)
{
assertAbsolutePathExists(String.format("%s/%s", baseDir.getAbsolutePath(), s));
}
private void assertAbsolutePathExists(String s)
{
File file = new File(s);
assertTrue(String.format("File %s should exist", file.getAbsolutePath()), file.exists());
}
}

@ -0,0 +1,173 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.io;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper;
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;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.GregorianCalendar;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ImportTest extends BaseTest
{
private File baseDir;
private Context context;
@Before
public void setup()
{
super.setup();
DateHelper.setFixedLocalTime(null);
HabitFixtures.purgeHabits();
context = InstrumentationRegistry.getInstrumentation().getContext();
baseDir = DatabaseHelper.getFilesDir("Backups");
if(baseDir == null) fail("baseDir should not be null");
}
private void copyAssetToFile(String assetPath, File dst) throws IOException
{
InputStream in = context.getAssets().open(assetPath);
DatabaseHelper.copy(in, dst);
}
private void importFromFile(String assetFilename) throws IOException
{
File file = new File(String.format("%s/%s", baseDir.getPath(), assetFilename));
copyAssetToFile(assetFilename, file);
assertTrue(file.exists());
assertTrue(file.canRead());
GenericImporter importer = new GenericImporter();
assertThat(importer.canHandle(file), is(true));
importer.importHabitsFromFile(file);
}
private boolean containsRepetition(Habit h, int year, int month, int day)
{
GregorianCalendar date = DateHelper.getStartOfTodayCalendar();
date.set(year, month - 1, day);
return h.repetitions.contains(date.getTimeInMillis());
}
@Test
public void testTickmateDB() throws IOException
{
importFromFile("tickmate.db");
List<Habit> habits = Habit.getAll(true);
assertThat(habits.size(), equalTo(3));
Habit h = habits.get(0);
assertThat(h.name, equalTo("Vegan"));
assertTrue(containsRepetition(h, 2016, 1, 24));
assertTrue(containsRepetition(h, 2016, 2, 5));
assertTrue(containsRepetition(h, 2016, 3, 18));
assertFalse(containsRepetition(h, 2016, 3, 14));
}
@Test
public void testRewireDB() throws IOException
{
importFromFile("rewire.db");
List<Habit> habits = Habit.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));
assertFalse(habit.hasReminder());
assertFalse(containsRepetition(habit, 2015, 12, 31));
assertTrue(containsRepetition(habit, 2016, 1, 18));
assertTrue(containsRepetition(habit, 2016, 1, 28));
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));
boolean[] reminderDays = {false, true, true, true, true, true, false};
assertThat(habit.reminderDays, equalTo(DateHelper.packWeekdayList(reminderDays)));
}
@Test
public void testHabitBullCSV() throws IOException
{
importFromFile("habitbull.csv");
List<Habit> habits = Habit.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));
assertTrue(containsRepetition(habit, 2016, 3, 18));
assertTrue(containsRepetition(habit, 2016, 3, 19));
assertFalse(containsRepetition(habit, 2016, 3, 20));
}
@Test
public void testLoopDB() throws IOException
{
importFromFile("loop.db");
List<Habit> habits = Habit.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));
assertTrue(containsRepetition(habit, 2016, 3, 14));
assertTrue(containsRepetition(habit, 2016, 3, 16));
assertFalse(containsRepetition(habit, 2016, 3, 17));
}
}

@ -0,0 +1,175 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DateHelper;
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 BaseTest
{
Habit nonDailyHabit;
private Habit emptyHabit;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
nonDailyHabit = HabitFixtures.createShortHabit();
emptyHabit = HabitFixtures.createEmptyHabit();
}
@After
public void tearDown()
{
DateHelper.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 = DateHelper.getStartOfToday() - 15 * DateHelper.millisecondsInOneDay;
long to = DateHelper.getStartOfToday() - 5 * DateHelper.millisecondsInOneDay;
int[] expectedValues = { CHECKED_EXPLICITLY, UNCHECKED, CHECKED_IMPLICITLY,
CHECKED_EXPLICITLY, CHECKED_EXPLICITLY, UNCHECKED, UNCHECKED, UNCHECKED, UNCHECKED,
UNCHECKED, UNCHECKED };
int[] actualValues = nonDailyHabit.checkmarks.getValues(from, to);
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void 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)
{
DateHelper.setFixedLocalTime(FIXED_LOCAL_TIME +
days * DateHelper.millisecondsInOneDay);
}
}

@ -0,0 +1,379 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.models;
import android.graphics.Color;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.hamcrest.MatcherAssert;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DateHelper;
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 BaseTest
{
@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 = Color.BLACK;
model.freqNum = 10;
model.freqDen = 20;
model.reminderDays = 1;
model.reminderHour = 8;
model.reminderMin = 30;
model.position = 0;
Habit habit = new Habit(model);
assertThat(habit.archived, is(model.archived));
assertThat(habit.highlight, is(model.highlight));
assertThat(habit.color, is(model.color));
assertThat(habit.freqNum, is(model.freqNum));
assertThat(habit.freqDen, is(model.freqDen));
assertThat(habit.reminderDays, is(model.reminderDays));
assertThat(habit.reminderHour, is(model.reminderHour));
assertThat(habit.reminderMin, is(model.reminderMin));
assertThat(habit.position, is(model.position));
}
@Test
public void 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<Habit> habits = new LinkedList<>();
List<Habit> 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<Habit> 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<Long> 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<Long> ids = new LinkedList<>();
int originalPositions[] = { 0, 1, 1, 4, 6, 8, 10, 10, 13};
for (int p : originalPositions)
{
Habit h = new Habit();
h.position = p;
h.save();
ids.add(h.getId());
}
Habit.rebuildOrder();
for (int i = 0; i < originalPositions.length; i++)
{
Habit h = Habit.get(ids.get(i));
if(h == null) fail();
assertThat(h.position, is(i));
}
}
@Test
public void test_getHabitsWithReminder()
{
List<Habit> habitsWithReminder = new LinkedList<>();
for(int i = 0; i < 10; i++)
{
Habit habit = new Habit();
if(i % 2 == 0)
{
habit.reminderDays = DateHelper.ALL_WEEK_DAYS;
habit.reminderHour = 8;
habit.reminderMin = 30;
habitsWithReminder.add(habit);
}
habit.save();
}
assertThat(habitsWithReminder, equalTo(Habit.getHabitsWithReminder()));
}
@Test
public void test_archive_unarchive()
{
List<Habit> allHabits = new LinkedList<>();
List<Habit> archivedHabits = new LinkedList<>();
List<Habit> 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<Habit> 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 = DateHelper.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 =
"Name,Description,NumRepetitions,Interval,Color\n" +
"Meditate,Did you meditate this morning?,1,1,#AFB42B\n" +
"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));
}
}

@ -0,0 +1,191 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DateHelper;
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 BaseTest
{
private Habit habit;
private Habit emptyHabit;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createShortHabit();
emptyHabit = HabitFixtures.createEmptyHabit();
}
@After
public void tearDown()
{
DateHelper.setFixedLocalTime(null);
}
@Test
public void test_contains()
{
long current = DateHelper.getStartOfToday();
for(boolean b : HabitFixtures.NON_DAILY_HABIT_CHECKS)
{
assertThat(habit.repetitions.contains(current), equalTo(b));
current -= DateHelper.millisecondsInOneDay;
}
for(int i = 0; i < 3; i++)
{
assertThat(habit.repetitions.contains(current), equalTo(false));
current -= DateHelper.millisecondsInOneDay;
}
}
@Test
public void test_delete()
{
long timestamp = DateHelper.getStartOfToday();
assertThat(habit.repetitions.contains(timestamp), equalTo(true));
habit.repetitions.delete(timestamp);
assertThat(habit.repetitions.contains(timestamp), equalTo(false));
}
@Test
public void test_toggle()
{
long timestamp = DateHelper.getStartOfToday();
assertThat(habit.repetitions.contains(timestamp), equalTo(true));
habit.repetitions.toggle(timestamp);
assertThat(habit.repetitions.contains(timestamp), equalTo(false));
habit.repetitions.toggle(timestamp);
assertThat(habit.repetitions.contains(timestamp), equalTo(true));
}
@Test
public void 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 = DateHelper.getStartOfTodayCalendar();
// Sets the current date to the end of November
day.set(2015, 10, 30);
DateHelper.setFixedLocalTime(day.getTimeInMillis());
// Add repetitions randomly from January to December
// Leaves the month of March empty, to check that it returns null
day.set(2015, 0, 1);
for(int i = 0; i < 365; i ++)
{
if(random.nextBoolean())
{
int month = day.get(Calendar.MONTH);
int week = day.get(Calendar.DAY_OF_WEEK) % 7;
if(month != 2)
{
if (month <= 10)
{
weekdayCount[month][week]++;
monthCount[month]++;
}
emptyHabit.repetitions.toggle(day.getTimeInMillis());
}
}
day.add(Calendar.DAY_OF_YEAR, 1);
}
HashMap<Long, Integer[]> freq = emptyHabit.repetitions.getWeekdayFrequency();
// Repetitions until November should be counted correctly
for(int month = 0; month < 11; month++)
{
day.set(2015, month, 1);
Integer actualCount[] = freq.get(day.getTimeInMillis());
if(monthCount[month] == 0)
assertThat(actualCount, equalTo(null));
else
assertThat(actualCount, equalTo(weekdayCount[month]));
}
// Repetitions in December should be discarded
day.set(2015, 11, 1);
assertThat(freq.get(day.getTimeInMillis()), equalTo(null));
}
@Test
public void test_count()
{
long to = DateHelper.getStartOfToday();
long from = to - 9 * DateHelper.millisecondsInOneDay;
assertThat(habit.repetitions.count(from, to), equalTo(6));
to = DateHelper.getStartOfToday() - DateHelper.millisecondsInOneDay;
from = to - 5 * DateHelper.millisecondsInOneDay;
assertThat(habit.repetitions.count(from, to), equalTo(3));
}
@Test
public void test_getOldest()
{
long expectedOldestTimestamp = DateHelper.getStartOfToday() - 9 * DateHelper.millisecondsInOneDay;
assertThat(habit.repetitions.getOldestTimestamp(), equalTo(expectedOldestTimestamp));
Repetition oldest = habit.repetitions.getOldest();
assertFalse(oldest == null);
assertThat(oldest.timestamp, equalTo(expectedOldestTimestamp));
}
}

@ -0,0 +1,176 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
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;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ScoreListTest extends BaseTest
{
private Habit habit;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createEmptyHabit();
}
@After
public void tearDown()
{
DateHelper.setFixedLocalTime(null);
}
@Test
public void test_invalidateNewerThan()
{
assertThat(habit.scores.getTodayValue(), equalTo(0));
toggleRepetitions(0, 2);
assertThat(habit.scores.getTodayValue(), equalTo(1948077));
habit.freqNum = 1;
habit.freqDen = 2;
habit.scores.invalidateNewerThan(0);
assertThat(habit.scores.getTodayValue(), equalTo(1974654));
}
@Test
public void test_getTodayStarValue()
{
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.EMPTY_STAR));
int k = 0;
while(habit.scores.getTodayValue() < Score.HALF_STAR_CUTOFF) toggleRepetitions(k, ++k);
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.HALF_STAR));
while(habit.scores.getTodayValue() < Score.FULL_STAR_CUTOFF) toggleRepetitions(k, ++k);
assertThat(habit.scores.getTodayStarStatus(), equalTo(Score.FULL_STAR));
}
@Test
public void test_getTodayValue()
{
toggleRepetitions(0, 20);
assertThat(habit.scores.getTodayValue(), equalTo(12629351));
}
@Test
public void test_getValue()
{
toggleRepetitions(0, 20);
int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773,
10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023,
4507040, 3699107, 2846927, 1948077, 1000000 };
long current = DateHelper.getStartOfToday();
for(int expectedValue : expectedValues)
{
assertThat(habit.scores.getValue(current), equalTo(expectedValue));
current -= DateHelper.millisecondsInOneDay;
}
}
@Test
public void test_getAllValues_withoutGroups()
{
toggleRepetitions(0, 20);
int expectedValues[] = { 12629351, 12266245, 11883254, 11479288, 11053198, 10603773,
10129735, 9629735, 9102352, 8546087, 7959357, 7340494, 6687738, 5999234, 5273023,
4507040, 3699107, 2846927, 1948077, 1000000 };
int actualValues[] = habit.scores.getAllValues(1);
assertThat(actualValues, equalTo(expectedValues));
}
@Test
public void test_getAllValues_withGroups()
{
toggleRepetitions(0, 20);
int expectedValues[] = { 11434978, 7894999, 3212362 };
int actualValues[] = habit.scores.getAllValues(7);
assertThat(actualValues, equalTo(expectedValues));
}
@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";
StringWriter writer = new StringWriter();
habit.scores.writeCSV(writer);
assertThat(writer.toString(), equalTo(expectedCSV));
}
private void toggleRepetitions(final int from, final int to)
{
DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command()
{
@Override
public void execute()
{
long today = DateHelper.getStartOfToday();
for (int i = from; i < to; i++)
habit.repetitions.toggle(today - i * DateHelper.millisecondsInOneDay);
}
});
}
}

@ -0,0 +1,108 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Score;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ScoreTest extends BaseTest
{
@Before
public void setup()
{
super.setup();
}
@Test
public void test_compute_withDailyHabit()
{
int checkmark = Checkmark.UNCHECKED;
assertThat(Score.compute(1, 0, checkmark), equalTo(0));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775));
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478));
checkmark = Checkmark.CHECKED_IMPLICITLY;
assertThat(Score.compute(1, 0, checkmark), equalTo(0));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(4740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(9480775));
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(18259478));
checkmark = Checkmark.CHECKED_EXPLICITLY;
assertThat(Score.compute(1, 0, checkmark), equalTo(1000000));
assertThat(Score.compute(1, 5000000, checkmark), equalTo(5740387));
assertThat(Score.compute(1, 10000000, checkmark), equalTo(10480775));
assertThat(Score.compute(1, Score.MAX_VALUE, checkmark), equalTo(Score.MAX_VALUE));
}
@Test
public void 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));
}
}

@ -0,0 +1,77 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.tasks;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.widget.ProgressBar;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.ExportCSVTask;
import org.isoron.uhabits.unit.HabitFixtures;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.List;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.IsNot.not;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ExportCSVTaskTest extends BaseTest
{
@Before
public void setup()
{
super.setup();
}
@Test
public void testExportCSV() throws Throwable
{
HabitFixtures.createShortHabit();
List<Habit> habits = Habit.getAll(true);
ProgressBar bar = new ProgressBar(targetContext);
ExportCSVTask task = new ExportCSVTask(habits, bar);
task.setListener(new ExportCSVTask.Listener()
{
@Override
public void onExportCSVFinished(String archiveFilename)
{
assertThat(archiveFilename, is(not(nullValue())));
File f = new File(archiveFilename);
assertTrue(f.exists());
assertTrue(f.canRead());
}
});
task.execute();
waitForAsyncTasks();
}
}

@ -0,0 +1,75 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.tasks;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.widget.ProgressBar;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.tasks.ExportDBTask;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.IsNot.not;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ExportDBTaskTest extends BaseTest
{
@Before
public void setup()
{
super.setup();
}
@Test
public void testExportCSV() throws Throwable
{
Context context = InstrumentationRegistry.getContext();
ProgressBar bar = new ProgressBar(context);
ExportDBTask task = new ExportDBTask(bar);
task.setListener(new ExportDBTask.Listener()
{
@Override
public void onExportDBFinished(String filename)
{
assertThat(filename, is(not(nullValue())));
File f = new File(filename);
assertTrue(f.exists());
assertTrue(f.canRead());
}
});
task.execute();
waitForAsyncTasks();
}
}

@ -0,0 +1,101 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.tasks;
import android.support.annotation.NonNull;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.widget.ProgressBar;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.tasks.ImportDataTask;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.fail;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ImportDataTaskTest extends BaseTest
{
private File baseDir;
@Before
public void setup()
{
super.setup();
baseDir = DatabaseHelper.getFilesDir("Backups");
if(baseDir == null) fail("baseDir should not be null");
}
private void copyAssetToFile(String assetPath, File dst) throws IOException
{
InputStream in = testContext.getAssets().open(assetPath);
DatabaseHelper.copy(in, dst);
}
private void assertTaskResult(final int expectedResult, String assetFilename) throws Throwable
{
ImportDataTask task = createTask(assetFilename);
task.setListener(new ImportDataTask.Listener()
{
@Override
public void onImportFinished(int result)
{
assertThat(result, equalTo(expectedResult));
}
});
task.execute();
waitForAsyncTasks();
}
@NonNull
private ImportDataTask createTask(String assetFilename) throws IOException
{
ProgressBar bar = new ProgressBar(targetContext);
File file = new File(String.format("%s/%s", baseDir.getPath(), assetFilename));
copyAssetToFile(assetFilename, file);
return new ImportDataTask(file, bar);
}
@Test
public void testImportInvalidData() throws Throwable
{
assertTaskResult(ImportDataTask.NOT_RECOGNIZED, "icon.png");
}
@Test
public void testImportValidData() throws Throwable
{
assertTaskResult(ImportDataTask.SUCCESS, "loop.db");
}
}

@ -0,0 +1,88 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.views.CheckmarkView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CheckmarkViewTest extends ViewTest
{
private CheckmarkView view;
private Habit habit;
@Before
public void setup()
{
super.setup();
habit = HabitFixtures.createShortHabit();
view = new CheckmarkView(targetContext);
view.setHabit(habit);
refreshData(view);
measureView(dpToPixels(100), dpToPixels(200), view);
}
@Test
public void testRender_checked() throws IOException
{
assertRenders(view, "CheckmarkView/checked.png");
}
@Test
public void testRender_unchecked() throws IOException
{
habit.repetitions.toggle(DateHelper.getStartOfToday());
view.refreshData();
assertRenders(view, "CheckmarkView/unchecked.png");
}
@Test
public void testRender_implicitlyChecked() throws IOException
{
long today = DateHelper.getStartOfToday();
long day = DateHelper.millisecondsInOneDay;
habit.repetitions.toggle(today);
habit.repetitions.toggle(today - day);
habit.repetitions.toggle(today - 2 * day);
view.refreshData();
assertRenders(view, "CheckmarkView/implicitly_checked.png");
}
@Test
public void testRender_largeSize() throws IOException
{
measureView(dpToPixels(300), dpToPixels(300), view);
assertRenders(view, "CheckmarkView/large_size.png");
}
}

@ -0,0 +1,80 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
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.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitFrequencyViewTest extends ViewTest
{
private HabitFrequencyView view;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();
view = new HabitFrequencyView(targetContext);
view.setHabit(habit);
refreshData(view);
measureView(dpToPixels(300), dpToPixels(100), view);
}
@Test
public void testRender() throws Throwable
{
assertRenders(view, "HabitFrequencyView/render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitFrequencyView/renderTransparent.png");
}
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "HabitFrequencyView/renderDifferentSize.png");
}
@Test
public void testRender_withDataOffset() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, "HabitFrequencyView/renderDataOffset.png");
}
}

@ -0,0 +1,125 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.views.HabitHistoryView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitHistoryViewTest extends ViewTest
{
private Habit habit;
private HabitHistoryView view;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createLongHabit();
view = new HabitHistoryView(targetContext);
view.setHabit(habit);
measureView(dpToPixels(300), dpToPixels(100), view);
refreshData(view);
}
@Test
public void testRender() throws Throwable
{
assertRenders(view, "HabitHistoryView/render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitHistoryView/renderTransparent.png");
}
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "HabitHistoryView/renderDifferentSize.png");
}
@Test
public void testRender_withDataOffset() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, "HabitHistoryView/renderDataOffset.png");
}
@Test
public void tapDate_withEditableView() throws Throwable
{
view.setIsEditable(true);
tap(view, 270, 30);
waitForAsyncTasks();
long today = DateHelper.getStartOfToday();
assertFalse(habit.repetitions.contains(today));
}
@Test
public void tapDate_atInvalidLocations() throws Throwable
{
int expectedCheckmarkValues[] = habit.checkmarks.getAllValues();
view.setIsEditable(true);
tap(view, 45, 5); // header
tap(view, 270, 43); // tomorrow's square
tap(view, 280, 30); // right axis
waitForAsyncTasks();
int actualCheckmarkValues[] = habit.checkmarks.getAllValues();
assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues));
}
@Test
public void tapDate_withReadOnlyView() throws Throwable
{
view.setIsEditable(false);
tap(view, 270, 30);
waitForAsyncTasks();
long today = DateHelper.getStartOfToday();
assertTrue(habit.repetitions.contains(today));
}
}

@ -0,0 +1,104 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
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.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitScoreViewTest extends ViewTest
{
private Habit habit;
private HabitScoreView view;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createLongHabit();
view = new HabitScoreView(targetContext);
view.setHabit(habit);
view.setBucketSize(7);
refreshData(view);
measureView(dpToPixels(300), dpToPixels(100), view);
}
@Test
public void testRender() throws Throwable
{
Log.d("HabitScoreViewTest", String.format("height=%d", dpToPixels(100)));
assertRenders(view, "HabitScoreView/render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitScoreView/renderTransparent.png");
}
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(dpToPixels(200), dpToPixels(200), view);
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
{
view.setBucketSize(30);
view.refreshData();
view.invalidate();
assertRenders(view, "HabitScoreView/renderMonthly.png");
}
@Test
public void testRender_withYearlyBucket() throws Throwable
{
view.setBucketSize(365);
view.refreshData();
view.invalidate();
assertRenders(view, "HabitScoreView/renderYearly.png");
}
}

@ -0,0 +1,74 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
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.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitStreakViewTest extends ViewTest
{
private HabitStreakView view;
@Before
public void setup()
{
super.setup();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();
view = new HabitStreakView(targetContext);
measureView(dpToPixels(300), dpToPixels(100), view);
view.setHabit(habit);
refreshData(view);
}
@Test
public void testRender() throws Throwable
{
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
{
measureView(dpToPixels(100), dpToPixels(100), view);
refreshData(view);
assertRenders(view, "HabitStreakView/renderSmallSize.png");
}
}

@ -0,0 +1,77 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.ColorHelper;
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(ColorHelper.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(ColorHelper.palette[5]);
view.setTextSize(targetContext.getResources().getDimension(R.dimen.tinyTextSize));
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "NumberView/renderDifferentParams.png");
}
}

@ -0,0 +1,78 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.views.RingView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RingViewTest extends ViewTest
{
private RingView view;
@Before
public void setup()
{
super.setup();
view = new RingView(targetContext);
view.setLabel("Hello world");
view.setPercentage(0.6f);
view.setColor(ColorHelper.palette[0]);
view.setMaxDiameter(dpToPixels(100));
}
@Test
public void testRender_base() throws IOException
{
measureView(dpToPixels(100), dpToPixels(100), view);
assertRenders(view, "RingView/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, "RingView/renderLongLabel.png");
}
@Test
public void testRender_withDifferentParams() throws IOException
{
view.setLabel("Habit Strength");
view.setPercentage(0.25f);
view.setMaxDiameter(dpToPixels(50));
view.setColor(ColorHelper.palette[5]);
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "RingView/renderDifferentParams.png");
}
}

@ -0,0 +1,226 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.SystemClock;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import org.isoron.uhabits.BaseTest;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.views.HabitDataView;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import static junit.framework.Assert.fail;
public class ViewTest extends BaseTest
{
protected static final double SIMILARITY_CUTOFF = 0.09;
public static final int HISTOGRAM_BIN_SIZE = 8;
protected void measureView(int width, int height, View view)
{
int specWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int specHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
view.measure(specWidth, specHeight);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}
protected void assertRenders(View view, String expectedImagePath) throws IOException
{
StringBuilder errorMessage = new StringBuilder();
expectedImagePath = getVersionedViewAssetPath(expectedImagePath);
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap actual = view.getDrawingCache();
Bitmap expected = getBitmapFromAssets(expectedImagePath);
int width = actual.getWidth();
int height = actual.getHeight();
Bitmap scaledExpected = Bitmap.createScaledBitmap(expected, width, height, true);
double distance;
boolean similarEnough = true;
if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) > SIMILARITY_CUTOFF)
{
similarEnough = false;
errorMessage.append(String.format(
"Rendered image has wrong histogram (distance=%f). ",
distance));
}
if(!similarEnough)
{
saveBitmap(expectedImagePath, ".scaledExpected", scaledExpected);
String path = saveBitmap(expectedImagePath, ".actual", actual);
errorMessage.append(String.format("Actual rendered image " + "saved to %s", path));
fail(errorMessage.toString());
}
actual.recycle();
expected.recycle();
scaledExpected.recycle();
}
private Bitmap getBitmapFromAssets(String path) throws IOException
{
InputStream stream = testContext.getAssets().open(path);
return BitmapFactory.decodeStream(stream);
}
private String getVersionedViewAssetPath(String path)
{
String result = null;
if (android.os.Build.VERSION.SDK_INT >= 21)
{
try
{
String vpath = "views-v21/" + path;
testContext.getAssets().open(vpath);
result = vpath;
}
catch (IOException e)
{
// ignored
}
}
if(result == null)
result = "views/" + path;
return result;
}
private String saveBitmap(String filename, String suffix, Bitmap bitmap)
throws IOException
{
File dir = DatabaseHelper.getSDCardDir("test-screenshots");
if(dir == null) dir = DatabaseHelper.getFilesDir("test-screenshots");
if(dir == null) throw new RuntimeException("Could not find suitable dir for screenshots");
filename = filename.replaceAll("\\.png$", suffix + ".png");
String absolutePath = String.format("%s/%s", dir.getAbsolutePath(), filename);
File parent = new File(absolutePath).getParentFile();
if(!parent.exists() && !parent.mkdirs())
throw new RuntimeException(String.format("Could not create dir: %s",
parent.getAbsolutePath()));
FileOutputStream out = new FileOutputStream(absolutePath);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
return absolutePath;
}
private int[][] getHistogram(Bitmap bitmap)
{
int histogram[][] = new int[4][256 / HISTOGRAM_BIN_SIZE];
for(int x = 0; x < bitmap.getWidth(); x++)
{
for(int y = 0; y < bitmap.getHeight(); y++)
{
int color = bitmap.getPixel(x, y);
int[] argb = new int[]{
(color >> 24) & 0xff, //alpha
(color >> 16) & 0xff, //red
(color >> 8) & 0xff, //green
(color ) & 0xff //blue
};
histogram[0][argb[0] / HISTOGRAM_BIN_SIZE]++;
histogram[1][argb[1] / HISTOGRAM_BIN_SIZE]++;
histogram[2][argb[2] / HISTOGRAM_BIN_SIZE]++;
histogram[3][argb[3] / HISTOGRAM_BIN_SIZE]++;
}
}
return histogram;
}
private double compareHistograms(int[][] actualHistogram, int[][] expectedHistogram)
{
long diff = 0;
long total = 0;
for(int i = 0; i < 256 / HISTOGRAM_BIN_SIZE; i ++)
{
diff += Math.abs(actualHistogram[0][i] - expectedHistogram[0][i]);
diff += Math.abs(actualHistogram[1][i] - expectedHistogram[1][i]);
diff += Math.abs(actualHistogram[2][i] - expectedHistogram[2][i]);
diff += Math.abs(actualHistogram[3][i] - expectedHistogram[3][i]);
total += actualHistogram[0][i];
total += actualHistogram[1][i];
total += actualHistogram[2][i];
total += actualHistogram[3][i];
}
return (double) diff / total / 2;
}
protected int dpToPixels(int dp)
{
return (int) UIHelper.dpToPixels(targetContext, dp);
}
protected void tap(GestureDetector.OnGestureListener view, int x, int y) throws InterruptedException
{
long now = SystemClock.uptimeMillis();
MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, dpToPixels(x),
dpToPixels(y), 0);
view.onSingleTapUp(e);
e.recycle();
}
protected void refreshData(final HabitDataView view)
{
new BaseTask()
{
@Override
protected void doInBackground()
{
view.refreshData();
}
}.execute();
try
{
waitForAsyncTasks();
}
catch (Exception e)
{
throw new RuntimeException("Time out");
}
}
}

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
~
~ This file is part of Loop Habit Tracker.
~
~ Loop Habit Tracker is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by the
~ Free Software Foundation, either version 3 of the License, or (at your
~ option) any later version.
~
~ Loop Habit Tracker is distributed in the hope that it will be useful, but
~ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
~ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
~ more details.
~
~ You should have received a copy of the GNU General Public License along
~ with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<manifest
package="org.isoron.uhabits"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SET_ANIMATION_SCALE"/>
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:replace="maxSdkVersion"
android:maxSdkVersion="99" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:replace="maxSdkVersion"
android:maxSdkVersion="99" />
</manifest>

@ -21,21 +21,21 @@
<manifest <manifest
package="org.isoron.uhabits" package="org.isoron.uhabits"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="13" android:versionCode="14"
android:versionName="1.3.3"> android:versionName="1.4.0">
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission <uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE" android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/> android:maxSdkVersion="18" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/> android:maxSdkVersion="18" />
<application <application
android:name="com.activeandroid.app.Application" android:name="HabitsApplication"
android:allowBackup="true" android:allowBackup="true"
android:backupAgent=".HabitsBackupAgent" android:backupAgent=".HabitsBackupAgent"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@ -43,12 +43,6 @@
android:theme="@style/AppBaseTheme" android:theme="@style/AppBaseTheme"
android:supportsRtl="true"> android:supportsRtl="true">
<meta-data
android:name="AA_DB_NAME"
android:value="uhabits.db"/>
<meta-data
android:name="AA_DB_VERSION"
android:value="12"/>
<meta-data <meta-data
android:name="com.google.android.backup.api_key" android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw"/> android:value="AEdPqrEAAAAI6aeWncbnMNo8E5GWeZ44dlc5cQ7tCROwFhOtiw"/>

@ -0,0 +1,4 @@
create index idx_score_habit_timestamp on score(habit, timestamp);
create index idx_checkmark_habit_timestamp on checkmarks(habit, timestamp);
create index idx_repetitions_habit_timestamp on repetitions(habit, timestamp);
create index idx_streak_habit_end on streak(habit, end);

@ -1,95 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.helpers;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Typeface;
import android.os.Vibrator;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import org.isoron.uhabits.BuildConfig;
public abstract class DialogHelper
{
public static final String ISORON_NAMESPACE = "http://isoron.org/android";
private static Typeface fontawesome;
public interface OnSavedListener
{
void onSaved(Command command, Object savedObject);
}
public static void showSoftKeyboard(View view)
{
InputMethodManager imm = (InputMethodManager) view.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
public static void vibrate(Context context, int duration)
{
Vibrator vb = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
vb.vibrate(duration);
}
public static void incrementLaunchCount(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
int count = prefs.getInt("launch_count", 0);
prefs.edit().putInt("launch_count", count + 1).apply();
}
public static void updateLastAppVersion(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putInt("last_version", BuildConfig.VERSION_CODE).apply();
}
public static int getLaunchCount(Context context)
{
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getInt("launch_count", 0);
}
public static String getAttribute(Context context, AttributeSet attrs, String name)
{
int resId = attrs.getAttributeResourceValue(ISORON_NAMESPACE, name, 0);
if(resId != 0)
return context.getResources().getString(resId);
else
return attrs.getAttributeValue(ISORON_NAMESPACE, name);
}
public static float dpToPixels(Context context, float dp)
{
Resources resources = context.getResources();
DisplayMetrics metrics = resources.getDisplayMetrics();
return dp * (metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
}
}

@ -28,7 +28,7 @@ import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
public class AboutActivity extends Activity implements View.OnClickListener public class AboutActivity extends Activity implements View.OnClickListener
{ {
@ -68,7 +68,7 @@ public class AboutActivity extends Activity implements View.OnClickListener
{ {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW); intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse("market://details?id=org.isoron.uhabits")); intent.setData(Uri.parse(getString(R.string.playStoreURL)));
startActivity(intent); startActivity(intent);
break; break;
} }
@ -77,8 +77,7 @@ public class AboutActivity extends Activity implements View.OnClickListener
{ {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(Intent.ACTION_SENDTO); intent.setAction(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("mailto:isoron+habits@gmail.com?" + intent.setData(Uri.parse(getString(R.string.feedbackURL)));
"subject=Feedback%20about%20Loop%20Habit%20Tracker"));
startActivity(intent); startActivity(intent);
break; break;
} }
@ -87,7 +86,7 @@ public class AboutActivity extends Activity implements View.OnClickListener
{ {
Intent intent = new Intent(); Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW); intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse("https://github.com/iSoron/uhabits")); intent.setData(Uri.parse(getString(R.string.sourceCodeURL)));
startActivity(intent); startActivity(intent);
break; break;
} }

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.helpers; package org.isoron.uhabits;
import android.app.Activity; import android.app.Activity;
import android.app.backup.BackupManager; import android.app.backup.BackupManager;
@ -25,11 +25,11 @@ import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast; import android.widget.Toast;
import org.isoron.uhabits.R; import org.isoron.uhabits.commands.Command;
import java.util.LinkedList; import java.util.LinkedList;
abstract public class ReplayableActivity extends Activity abstract public class BaseActivity extends Activity implements Thread.UncaughtExceptionHandler
{ {
private static int MAX_UNDO_LEVEL = 15; private static int MAX_UNDO_LEVEL = 15;
@ -37,11 +37,16 @@ abstract public class ReplayableActivity extends Activity
private LinkedList<Command> redoList; private LinkedList<Command> redoList;
private Toast toast; private Toast toast;
Thread.UncaughtExceptionHandler androidExceptionHandler;
@Override @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
{ {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
undoList = new LinkedList<>(); undoList = new LinkedList<>();
redoList = new LinkedList<>(); redoList = new LinkedList<>();
} }
@ -103,7 +108,7 @@ abstract public class ReplayableActivity extends Activity
@Override @Override
protected void onPostExecute(Void aVoid) protected void onPostExecute(Void aVoid)
{ {
ReplayableActivity.this.onPostExecuteCommand(refreshKey); BaseActivity.this.onPostExecuteCommand(refreshKey);
BackupManager.dataChanged("org.isoron.uhabits"); BackupManager.dataChanged("org.isoron.uhabits");
} }
}.execute(); }.execute();
@ -115,4 +120,23 @@ abstract public class ReplayableActivity extends Activity
public void onPostExecuteCommand(Long refreshKey) public void onPostExecuteCommand(Long refreshKey)
{ {
} }
@Override
public void uncaughtException(Thread thread, Throwable ex)
{
try
{
ex.printStackTrace();
HabitsApplication.generateLogFile();
}
catch(Exception e)
{
// ignored
}
if(androidExceptionHandler != null)
androidExceptionHandler.uncaughtException(thread, ex);
else
System.exit(1);
}
} }

@ -36,9 +36,11 @@ import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import java.util.Date; import java.util.Date;
@ -124,11 +126,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
private void dismissAllHabits() private void dismissAllHabits()
{ {
for (Habit h : Habit.getHighlightedHabits())
{
h.highlight = 0;
h.save();
}
} }
private void dismissNotification(Context context, Long habitId) private void dismissNotification(Context context, Long habitId)
@ -141,19 +139,29 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
} }
private void createNotification(Context context, Intent intent) private void createNotification(final Context context, final Intent intent)
{ {
Uri data = intent.getData(); final Uri data = intent.getData();
Habit habit = Habit.get(ContentUris.parseId(data)); final Habit habit = Habit.get(ContentUris.parseId(data));
Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday()); final Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());
Long reminderTime = intent.getLongExtra("reminderTime", DateHelper.getStartOfToday()); final Long reminderTime = intent.getLongExtra("reminderTime", DateHelper.getStartOfToday());
if (habit == null) return; if (habit == null) return;
if (habit.repetitions.hasImplicitRepToday()) return;
habit.highlight = 1; new BaseTask()
habit.save(); {
int todayValue;
@Override
protected void doInBackground()
{
todayValue = habit.checkmarks.getTodayValue();
}
@Override
protected void onPostExecute(Void aVoid)
{
if (todayValue != Checkmark.UNCHECKED) return;
if (!checkWeekday(intent, habit)) return; if (!checkWeekday(intent, habit)) return;
// Check if reminder has been turned off after alarm was scheduled // Check if reminder has been turned off after alarm was scheduled
@ -197,6 +205,10 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE); int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
notificationManager.notify(notificationId, notification); notificationManager.notify(notificationId, notification);
super.onPostExecute(aVoid);
}
}.execute();
} }
public static PendingIntent buildSnoozeIntent(Context context, Habit habit) public static PendingIntent buildSnoozeIntent(Context context, Habit habit)
@ -225,6 +237,13 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
return PendingIntent.getBroadcast(context, 0, deleteIntent, 0); return PendingIntent.getBroadcast(context, 0, deleteIntent, 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 PendingIntent.getActivity(context, 0, intent, 0);
}
private boolean checkWeekday(Intent intent, Habit habit) private boolean checkWeekday(Intent intent, Habit habit)
{ {
Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday()); Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());

@ -0,0 +1,151 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits;
import android.app.Application;
import android.content.Context;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.WindowManager;
import com.activeandroid.ActiveAndroid;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
public class HabitsApplication extends Application
{
@Nullable
private static Context context;
public static boolean isTestMode()
{
try
{
if(context != null)
context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest");
return true;
}
catch (final Exception e)
{
return false;
}
}
@Nullable
public static Context getContext()
{
return context;
}
@Override
public void onCreate()
{
super.onCreate();
HabitsApplication.context = this;
if (isTestMode())
{
File db = DatabaseHelper.getDatabaseFile();
if(db.exists()) db.delete();
}
DatabaseHelper.initializeActiveAndroid();
}
@Override
public void onTerminate()
{
HabitsApplication.context = null;
ActiveAndroid.dispose();
super.onTerminate();
}
public static String getLogcat() throws IOException
{
StringBuilder builder = new StringBuilder();
String[] command = new String[] { "logcat", "-d"};
Process process = Runtime.getRuntime().exec(command);
InputStreamReader in = new InputStreamReader(process.getInputStream());
BufferedReader bufferedReader = new BufferedReader(in);
String line;
while ((line = bufferedReader.readLine()) != null)
{
builder.append(line);
builder.append('\n');
}
return builder.toString();
}
public static String getDeviceInfo()
{
if(context == null) return "";
StringBuilder b = new StringBuilder();
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
b.append(String.format("App Version Name: %s\n", BuildConfig.VERSION_NAME));
b.append(String.format("App Version Code: %s\n", BuildConfig.VERSION_CODE));
b.append(String.format("OS Version: %s (%s)\n", System.getProperty("os.version"),
android.os.Build.VERSION.INCREMENTAL));
b.append(String.format("OS API Level: %s\n", android.os.Build.VERSION.SDK));
b.append(String.format("Device: %s\n", android.os.Build.DEVICE));
b.append(String.format("Model (Product): %s (%s)\n", android.os.Build.MODEL,
android.os.Build.PRODUCT));
b.append(String.format("Manufacturer: %s\n", android.os.Build.MANUFACTURER));
b.append(String.format("Other tags: %s\n", android.os.Build.TAGS));
b.append(String.format("Screen Width: %s\n", wm.getDefaultDisplay().getWidth()));
b.append(String.format("Screen Height: %s\n", wm.getDefaultDisplay().getHeight()));
b.append(String.format("SD Card state: %s\n\n", Environment.getExternalStorageState()));
return b.toString();
}
@NonNull
public static File generateLogFile() throws IOException
{
String logcat = getLogcat();
String deviceInfo = getDeviceInfo();
String date = DateHelper.getBackupDateFormat().format(DateHelper.getLocalTime());
if(context == null) throw new RuntimeException("application context should not be null");
File dir = DatabaseHelper.getFilesDir("Logs");
if (dir == null) throw new IOException("log dir should not be null");
File logFile = new File(String.format("%s/Log %s.txt", dir.getPath(), date));
FileWriter output = new FileWriter(logFile);
output.write(deviceInfo);
output.write(logcat);
output.close();
return logFile;
}
}

@ -26,27 +26,32 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.helpers.DialogHelper; import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.fragments.ListHabitsFragment; import org.isoron.uhabits.fragments.ListHabitsFragment;
import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.widgets.CheckmarkWidgetProvider; import org.isoron.uhabits.widgets.CheckmarkWidgetProvider;
import org.isoron.uhabits.widgets.FrequencyWidgetProvider; import org.isoron.uhabits.widgets.FrequencyWidgetProvider;
import org.isoron.uhabits.widgets.HistoryWidgetProvider; import org.isoron.uhabits.widgets.HistoryWidgetProvider;
import org.isoron.uhabits.widgets.ScoreWidgetProvider; import org.isoron.uhabits.widgets.ScoreWidgetProvider;
import org.isoron.uhabits.widgets.StreakWidgetProvider; import org.isoron.uhabits.widgets.StreakWidgetProvider;
public class MainActivity extends ReplayableActivity import java.io.File;
import java.io.IOException;
public class MainActivity extends BaseActivity
implements ListHabitsFragment.OnHabitClickListener implements ListHabitsFragment.OnHabitClickListener
{ {
private ListHabitsFragment listHabitsFragment; private ListHabitsFragment listHabitsFragment;
@ -56,6 +61,11 @@ public class MainActivity extends ReplayableActivity
public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH"; public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH";
public static final int RESULT_IMPORT_DATA = 1;
public static final int RESULT_EXPORT_CSV = 2;
public static final int RESULT_EXPORT_DB = 3;
public static final int RESULT_BUG_REPORT = 4;
@Override @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
{ {
@ -76,8 +86,8 @@ public class MainActivity extends ReplayableActivity
private void onStartup() private void onStartup()
{ {
PreferenceManager.setDefaultValues(this, R.xml.preferences, false); PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
DialogHelper.incrementLaunchCount(this); UIHelper.incrementLaunchCount(this);
DialogHelper.updateLastAppVersion(this); UIHelper.updateLastAppVersion(this);
showTutorial(); showTutorial();
new AsyncTask<Void, Void, Void>() { new AsyncTask<Void, Void, Void>() {
@ -123,7 +133,7 @@ public class MainActivity extends ReplayableActivity
case R.id.action_settings: case R.id.action_settings:
{ {
Intent intent = new Intent(this, SettingsActivity.class); Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent); startActivityForResult(intent, 0);
return true; return true;
} }
@ -134,11 +144,62 @@ public class MainActivity extends ReplayableActivity
return true; return true;
} }
case R.id.action_faq:
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.helpURL)));
startActivity(intent);
return true;
}
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
} }
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (resultCode)
{
case RESULT_IMPORT_DATA:
listHabitsFragment.showImportDialog();
break;
case RESULT_EXPORT_CSV:
listHabitsFragment.exportAllHabits();
break;
case RESULT_EXPORT_DB:
listHabitsFragment.exportDB();
break;
case RESULT_BUG_REPORT:
generateBugReport();
break;
}
}
private void generateBugReport()
{
try
{
File logFile = HabitsApplication.generateLogFile();
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SENDTO);
intent.setData(Uri.parse(getString(R.string.bugReportURL)));
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(logFile));
startActivity(intent);
}
catch (IOException e)
{
e.printStackTrace();
showToast(R.string.bug_report_failed);
}
}
@Override @Override
public void onHabitClicked(Habit habit) public void onHabitClicked(Habit habit)
{ {
@ -152,15 +213,14 @@ public class MainActivity extends ReplayableActivity
{ {
listHabitsFragment.onPostExecuteCommand(refreshKey); listHabitsFragment.onPostExecuteCommand(refreshKey);
new AsyncTask<Void, Void, Void>() new BaseTask()
{ {
@Override @Override
protected Void doInBackground(Void... params) protected void doInBackground()
{ {
updateWidgets(MainActivity.this); updateWidgets(MainActivity.this);
return null;
} }
}; }.execute();
} }
public static void updateWidgets(Context context) public static void updateWidgets(Context context)
@ -197,4 +257,14 @@ public class MainActivity extends ReplayableActivity
listHabitsFragment.onPostExecuteCommand(null); listHabitsFragment.onPostExecuteCommand(null);
} }
} }
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults)
{
if (grantResults.length <= 0) return;
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) return;
listHabitsFragment.showImportDialog();
}
} }

@ -20,28 +20,16 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.app.ActionBar; import android.app.ActionBar;
import android.content.BroadcastReceiver;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.fragments.ShowHabitFragment;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
public class ShowHabitActivity extends ReplayableActivity public class ShowHabitActivity extends BaseActivity
{ {
private Habit habit;
public Habit habit;
private Receiver receiver;
private LocalBroadcastManager localBroadcastManager;
private ShowHabitFragment fragment;
@Override @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
@ -52,37 +40,18 @@ public class ShowHabitActivity extends ReplayableActivity
habit = Habit.get(ContentUris.parseId(data)); habit = Habit.get(ContentUris.parseId(data));
ActionBar actionBar = getActionBar(); ActionBar actionBar = getActionBar();
if(actionBar != null) if(actionBar != null && getHabit() != null)
{ {
actionBar.setTitle(habit.name); actionBar.setTitle(getHabit().name);
if (android.os.Build.VERSION.SDK_INT >= 21) if (android.os.Build.VERSION.SDK_INT >= 21)
actionBar.setBackgroundDrawable(new ColorDrawable(habit.color)); actionBar.setBackgroundDrawable(new ColorDrawable(getHabit().color));
} }
setContentView(R.layout.show_habit_activity); setContentView(R.layout.show_habit_activity);
fragment = (ShowHabitFragment) getFragmentManager().findFragmentById(R.id.fragment2);
receiver = new Receiver();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
localBroadcastManager.registerReceiver(receiver,
new IntentFilter(MainActivity.ACTION_REFRESH));
}
class Receiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
fragment.refreshData();
}
} }
@Override public Habit getHabit()
protected void onDestroy()
{ {
localBroadcastManager.unregisterReceiver(receiver); return habit;
super.onDestroy();
} }
} }

@ -19,11 +19,9 @@
package org.isoron.uhabits.commands; package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.util.LinkedList;
import java.util.List; import java.util.List;
public class ArchiveHabitsCommand extends Command public class ArchiveHabitsCommand extends Command
@ -31,12 +29,6 @@ public class ArchiveHabitsCommand extends Command
private List<Habit> habits; private List<Habit> habits;
public ArchiveHabitsCommand(Habit habit)
{
habits = new LinkedList<>();
habits.add(habit);
}
public ArchiveHabitsCommand(List<Habit> habits) public ArchiveHabitsCommand(List<Habit> habits)
{ {
this.habits = habits; this.habits = habits;
@ -45,15 +37,13 @@ public class ArchiveHabitsCommand extends Command
@Override @Override
public void execute() public void execute()
{ {
for(Habit h : habits) Habit.archive(habits);
h.archive();
} }
@Override @Override
public void undo() public void undo()
{ {
for(Habit h : habits) Habit.unarchive(habits);
h.unarchive();
} }
public Integer getExecuteStringId() public Integer getExecuteStringId()

@ -21,8 +21,8 @@ package org.isoron.uhabits.commands;
import com.activeandroid.ActiveAndroid; import com.activeandroid.ActiveAndroid;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.util.ArrayList; import java.util.ArrayList;
@ -47,30 +47,16 @@ public class ChangeHabitColorCommand extends Command
@Override @Override
public void execute() public void execute()
{ {
ActiveAndroid.beginTransaction(); Habit.setColor(habits, newColor);
try
{
for(Habit h : habits)
{
h.color = newColor;
h.save();
}
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
} }
@Override @Override
public void undo() public void undo()
{ {
ActiveAndroid.beginTransaction(); DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command()
{
try @Override
public void execute()
{ {
int k = 0; int k = 0;
for(Habit h : habits) for(Habit h : habits)
@ -78,13 +64,8 @@ public class ChangeHabitColorCommand extends Command
h.color = originalColors.get(k++); h.color = originalColors.get(k++);
h.save(); h.save();
} }
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
} }
});
} }
public Integer getExecuteStringId() public Integer getExecuteStringId()

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.helpers; package org.isoron.uhabits.commands;
public abstract class Command public abstract class Command
{ {

@ -19,7 +19,6 @@
package org.isoron.uhabits.commands; package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
@ -51,7 +50,10 @@ public class CreateHabitCommand extends Command
@Override @Override
public void undo() public void undo()
{ {
Habit.get(savedId).delete(); Habit habit = Habit.get(savedId);
if(habit == null) throw new RuntimeException("Habit not found");
habit.cascadeDelete();
} }
@Override @Override

@ -19,7 +19,6 @@
package org.isoron.uhabits.commands; package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;

@ -19,7 +19,6 @@
package org.isoron.uhabits.commands; package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
@ -40,29 +39,36 @@ public class EditHabitCommand extends Command
!this.original.freqNum.equals(this.modified.freqNum)); !this.original.freqNum.equals(this.modified.freqNum));
} }
@Override
public void execute() public void execute()
{ {
Habit habit = Habit.get(savedId); copyAttributes(this.modified);
habit.copyAttributes(modified);
habit.save();
if (hasIntervalChanged)
{
habit.checkmarks.deleteNewerThan(0);
habit.streaks.deleteNewerThan(0);
habit.scores.deleteNewerThan(0);
}
} }
@Override
public void undo() public void undo()
{
copyAttributes(this.original);
}
private void copyAttributes(Habit model)
{ {
Habit habit = Habit.get(savedId); Habit habit = Habit.get(savedId);
habit.copyAttributes(original); if(habit == null) throw new RuntimeException("Habit not found");
habit.copyAttributes(model);
habit.save(); habit.save();
invalidateIfNeeded(habit);
}
private void invalidateIfNeeded(Habit habit)
{
if (hasIntervalChanged) if (hasIntervalChanged)
{ {
habit.checkmarks.deleteNewerThan(0); habit.checkmarks.deleteNewerThan(0);
habit.streaks.deleteNewerThan(0); habit.streaks.deleteNewerThan(0);
habit.scores.deleteNewerThan(0); habit.scores.invalidateNewerThan(0);
} }
} }

@ -19,7 +19,6 @@
package org.isoron.uhabits.commands; package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
public class ToggleRepetitionCommand extends Command public class ToggleRepetitionCommand extends Command

@ -19,11 +19,9 @@
package org.isoron.uhabits.commands; package org.isoron.uhabits.commands;
import org.isoron.helpers.Command;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.util.LinkedList;
import java.util.List; import java.util.List;
public class UnarchiveHabitsCommand extends Command public class UnarchiveHabitsCommand extends Command
@ -31,12 +29,6 @@ public class UnarchiveHabitsCommand extends Command
private List<Habit> habits; private List<Habit> habits;
public UnarchiveHabitsCommand(Habit habit)
{
habits = new LinkedList<>();
habits.add(habit);
}
public UnarchiveHabitsCommand(List<Habit> habits) public UnarchiveHabitsCommand(List<Habit> habits)
{ {
this.habits = habits; this.habits = habits;
@ -45,15 +37,13 @@ public class UnarchiveHabitsCommand extends Command
@Override @Override
public void execute() public void execute()
{ {
for(Habit h : habits) Habit.unarchive(habits);
h.unarchive();
} }
@Override @Override
public void undo() public void undo()
{ {
for(Habit h : habits) Habit.archive(habits);
h.archive();
} }
public Integer getExecuteStringId() public Integer getExecuteStringId()

@ -0,0 +1,175 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.support.annotation.NonNull;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager.LayoutParams;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.io.File;
import java.io.FileFilter;
import java.util.Arrays;
public class FilePickerDialog implements AdapterView.OnItemClickListener
{
private static final String PARENT_DIR = "..";
private final Activity activity;
private ListView list;
private Dialog dialog;
private File currentPath;
public interface OnFileSelectedListener
{
void onFileSelected(File file);
}
private OnFileSelectedListener listener;
public FilePickerDialog(Activity activity, File initialDirectory)
{
this.activity = activity;
list = new ListView(activity);
list.setOnItemClickListener(this);
dialog = new Dialog(activity);
dialog.setContentView(list);
dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
navigateTo(initialDirectory);
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int which, long id)
{
String filename = (String) list.getItemAtPosition(which);
File file;
if (filename.equals(PARENT_DIR))
file = currentPath.getParentFile();
else
file = new File(currentPath, filename);
if (file.isDirectory())
{
navigateTo(file);
}
else
{
if (listener != null) listener.onFileSelected(file);
dialog.dismiss();
}
}
public void show()
{
dialog.show();
}
public void setListener(OnFileSelectedListener listener)
{
this.listener = listener;
}
private void navigateTo(File path)
{
if (!path.exists()) return;
File[] dirs = path.listFiles(new ReadableDirFilter());
File[] files = path.listFiles(new RegularReadableFileFilter());
if(dirs == null || files == null) return;
this.currentPath = path;
dialog.setTitle(currentPath.getPath());
list.setAdapter(new FilePickerAdapter(getFileList(path, dirs, files)));
}
@NonNull
private String[] getFileList(File path, File[] dirs, File[] files)
{
int count = 0;
int length = dirs.length + files.length;
String[] fileList;
if (path.getParentFile() == null || !path.getParentFile().canRead())
{
fileList = new String[length];
}
else
{
fileList = new String[length + 1];
fileList[count++] = PARENT_DIR;
}
Arrays.sort(dirs);
Arrays.sort(files);
for (File dir : dirs)
fileList[count++] = dir.getName();
for (File file : files)
fileList[count++] = file.getName();
return fileList;
}
private class FilePickerAdapter extends ArrayAdapter<String>
{
public FilePickerAdapter(@NonNull String[] fileList)
{
super(FilePickerDialog.this.activity, android.R.layout.simple_list_item_1, fileList);
}
@Override
public View getView(int pos, View view, ViewGroup parent)
{
view = super.getView(pos, view, parent);
TextView tv = (TextView) view;
tv.setSingleLine(true);
return view;
}
}
private static class ReadableDirFilter implements FileFilter
{
@Override
public boolean accept(File file)
{
return (file.isDirectory() && file.canRead());
}
}
private class RegularReadableFileFilter implements FileFilter
{
@Override
public boolean accept(File file)
{
return !file.isDirectory() && file.canRead();
}
}
}

@ -30,6 +30,7 @@ import android.util.Log;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.views.HabitHistoryView; import org.isoron.uhabits.views.HabitHistoryView;
public class HistoryEditorDialog extends DialogFragment public class HistoryEditorDialog extends DialogFragment
@ -44,7 +45,6 @@ public class HistoryEditorDialog extends DialogFragment
{ {
Context context = getActivity(); Context context = getActivity();
historyView = new HabitHistoryView(context, null); historyView = new HabitHistoryView(context, null);
int p = (int) getResources().getDimension(R.dimen.history_editor_padding);
if(savedInstanceState != null) if(savedInstanceState != null)
{ {
@ -52,7 +52,8 @@ public class HistoryEditorDialog extends DialogFragment
if(id > 0) this.habit = Habit.get(id); if(id > 0) this.habit = Habit.get(id);
} }
historyView.setPadding(p, 0, p, 0); int padding = (int) getResources().getDimension(R.dimen.history_editor_padding);
historyView.setPadding(padding, 0, padding, 0);
historyView.setHabit(habit); historyView.setHabit(habit);
historyView.setIsEditable(true); historyView.setIsEditable(true);
@ -61,9 +62,23 @@ public class HistoryEditorDialog extends DialogFragment
.setView(historyView) .setView(historyView)
.setPositiveButton(android.R.string.ok, this); .setPositiveButton(android.R.string.ok, this);
refreshData();
return builder.create(); return builder.create();
} }
private void refreshData()
{
new BaseTask()
{
@Override
protected void doInBackground()
{
historyView.refreshData();
}
}.execute();
}
@Override @Override
public void onResume() public void onResume()
{ {

@ -25,7 +25,7 @@ import android.app.DialogFragment;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.os.Bundle; import android.os.Bundle;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
public class WeekdayPickerDialog extends DialogFragment public class WeekdayPickerDialog extends DialogFragment

@ -19,9 +19,9 @@
package org.isoron.uhabits.fragments; package org.isoron.uhabits.fragments;
import android.annotation.SuppressLint;
import android.app.DialogFragment; import android.app.DialogFragment;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.format.DateFormat; import android.text.format.DateFormat;
@ -29,8 +29,10 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import com.android.colorpicker.ColorPickerDialog; import com.android.colorpicker.ColorPickerDialog;
@ -38,11 +40,11 @@ import com.android.colorpicker.ColorPickerSwatch;
import com.android.datetimepicker.time.RadialPickerLayout; import com.android.datetimepicker.time.RadialPickerLayout;
import com.android.datetimepicker.time.TimePickerDialog; import com.android.datetimepicker.time.TimePickerDialog;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.Command; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.UIHelper.OnSavedListener;
import org.isoron.helpers.DialogHelper.OnSavedListener;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CreateHabitCommand; import org.isoron.uhabits.commands.CreateHabitCommand;
import org.isoron.uhabits.commands.EditHabitCommand; import org.isoron.uhabits.commands.EditHabitCommand;
import org.isoron.uhabits.dialogs.WeekdayPickerDialog; import org.isoron.uhabits.dialogs.WeekdayPickerDialog;
@ -52,7 +54,7 @@ import java.util.Arrays;
public class EditHabitFragment extends DialogFragment public class EditHabitFragment extends DialogFragment
implements OnClickListener, WeekdayPickerDialog.OnWeekdaysPickedListener, implements OnClickListener, WeekdayPickerDialog.OnWeekdaysPickedListener,
TimePickerDialog.OnTimeSetListener TimePickerDialog.OnTimeSetListener, Spinner.OnItemSelectedListener
{ {
private Integer mode; private Integer mode;
static final int EDIT_MODE = 0; static final int EDIT_MODE = 0;
@ -70,6 +72,10 @@ public class EditHabitFragment extends DialogFragment
private TextView tvReminderTime; private TextView tvReminderTime;
private TextView tvReminderDays; private TextView tvReminderDays;
private Spinner sFrequency;
private ViewGroup llCustomFrequency;
private ViewGroup llReminderDays;
private SharedPreferences prefs; private SharedPreferences prefs;
private boolean is24HourMode; private boolean is24HourMode;
@ -104,6 +110,10 @@ public class EditHabitFragment extends DialogFragment
tvReminderTime = (TextView) view.findViewById(R.id.inputReminderTime); tvReminderTime = (TextView) view.findViewById(R.id.inputReminderTime);
tvReminderDays = (TextView) view.findViewById(R.id.inputReminderDays); tvReminderDays = (TextView) view.findViewById(R.id.inputReminderDays);
sFrequency = (Spinner) view.findViewById(R.id.sFrequency);
llCustomFrequency = (ViewGroup) view.findViewById(R.id.llCustomFrequency);
llReminderDays = (ViewGroup) view.findViewById(R.id.llReminderDays);
Button buttonSave = (Button) view.findViewById(R.id.buttonSave); Button buttonSave = (Button) view.findViewById(R.id.buttonSave);
Button buttonDiscard = (Button) view.findViewById(R.id.buttonDiscard); Button buttonDiscard = (Button) view.findViewById(R.id.buttonDiscard);
ImageButton buttonPickColor = (ImageButton) view.findViewById(R.id.buttonPickColor); ImageButton buttonPickColor = (ImageButton) view.findViewById(R.id.buttonPickColor);
@ -113,6 +123,7 @@ public class EditHabitFragment extends DialogFragment
tvReminderTime.setOnClickListener(this); tvReminderTime.setOnClickListener(this);
tvReminderDays.setOnClickListener(this); tvReminderDays.setOnClickListener(this);
buttonPickColor.setOnClickListener(this); buttonPickColor.setOnClickListener(this);
sFrequency.setOnItemSelectedListener(this);
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
@ -125,18 +136,16 @@ public class EditHabitFragment extends DialogFragment
{ {
getDialog().setTitle(R.string.create_habit); getDialog().setTitle(R.string.create_habit);
modifiedHabit = new Habit(); modifiedHabit = new Habit();
modifiedHabit.freqNum = 1;
int defaultNum = prefs.getInt("pref_default_habit_freq_num", modifiedHabit.freqNum); modifiedHabit.freqDen = 1;
int defaultDen = prefs.getInt("pref_default_habit_freq_den", modifiedHabit.freqDen); modifiedHabit.color = prefs.getInt("pref_default_habit_color", modifiedHabit.color);
int defaultColor = prefs.getInt("pref_default_habit_color", modifiedHabit.color);
modifiedHabit.color = defaultColor;
modifiedHabit.freqNum = defaultNum;
modifiedHabit.freqDen = defaultDen;
} }
else if (mode == EDIT_MODE) else if (mode == EDIT_MODE)
{ {
originalHabit = Habit.get((Long) args.get("habitId")); Long habitId = (Long) args.get("habitId");
if(habitId == null) throw new IllegalArgumentException("habitId must be specified");
originalHabit = Habit.get(habitId);
modifiedHabit = new Habit(originalHabit); modifiedHabit = new Habit(originalHabit);
getDialog().setTitle(R.string.edit_habit); getDialog().setTitle(R.string.edit_habit);
@ -152,17 +161,14 @@ public class EditHabitFragment extends DialogFragment
modifiedHabit.reminderDays = savedInstanceState.getInt("reminderDays", -1); modifiedHabit.reminderDays = savedInstanceState.getInt("reminderDays", -1);
if(modifiedHabit.reminderMin < 0) if(modifiedHabit.reminderMin < 0)
{ modifiedHabit.clearReminder();
modifiedHabit.reminderMin = null;
modifiedHabit.reminderHour = null;
modifiedHabit.reminderDays = 127;
}
} }
tvFreqNum.append(modifiedHabit.freqNum.toString()); tvFreqNum.append(modifiedHabit.freqNum.toString());
tvFreqDen.append(modifiedHabit.freqDen.toString()); tvFreqDen.append(modifiedHabit.freqDen.toString());
changeColor(modifiedHabit.color); changeColor(modifiedHabit.color);
updateFrequency();
updateReminder(); updateReminder();
return view; return view;
@ -178,24 +184,23 @@ public class EditHabitFragment extends DialogFragment
editor.apply(); editor.apply();
} }
@SuppressWarnings("ConstantConditions")
private void updateReminder() private void updateReminder()
{ {
if (modifiedHabit.reminderHour != null) if (modifiedHabit.hasReminder())
{ {
tvReminderTime.setTextColor(Color.BLACK);
tvReminderTime.setText(DateHelper.formatTime(getActivity(), modifiedHabit.reminderHour, tvReminderTime.setText(DateHelper.formatTime(getActivity(), modifiedHabit.reminderHour,
modifiedHabit.reminderMin)); modifiedHabit.reminderMin));
tvReminderDays.setVisibility(View.VISIBLE); llReminderDays.setVisibility(View.VISIBLE);
boolean weekdays[] = DateHelper.unpackWeekdayList(modifiedHabit.reminderDays);
tvReminderDays.setText(DateHelper.formatWeekdayList(getActivity(), weekdays));
} }
else else
{ {
tvReminderTime.setTextColor(Color.GRAY);
tvReminderTime.setText(R.string.reminder_off); tvReminderTime.setText(R.string.reminder_off);
tvReminderDays.setVisibility(View.GONE); llReminderDays.setVisibility(View.GONE);
} }
boolean weekdays[] = DateHelper.unpackWeekdayList(modifiedHabit.reminderDays);
tvReminderDays.setText(DateHelper.formatWeekdayList(getActivity(), weekdays));
} }
public void setOnSavedListener(OnSavedListener onSavedListener) public void setOnSavedListener(OnSavedListener onSavedListener)
@ -257,11 +262,6 @@ public class EditHabitFragment extends DialogFragment
if (!validate()) return; if (!validate()) return;
SharedPreferences.Editor editor = prefs.edit();
editor.putInt("pref_default_habit_freq_num", modifiedHabit.freqNum);
editor.putInt("pref_default_habit_freq_den", modifiedHabit.freqDen);
editor.apply();
Command command = null; Command command = null;
Habit savedHabit = null; Habit savedHabit = null;
@ -305,12 +305,13 @@ public class EditHabitFragment extends DialogFragment
return valid; return valid;
} }
@SuppressWarnings("ConstantConditions")
private void onDateSpinnerClick() private void onDateSpinnerClick()
{ {
int defaultHour = 8; int defaultHour = 8;
int defaultMin = 0; int defaultMin = 0;
if (modifiedHabit.reminderHour != null) if (modifiedHabit.hasReminder())
{ {
defaultHour = modifiedHabit.reminderHour; defaultHour = modifiedHabit.reminderHour;
defaultMin = modifiedHabit.reminderMin; defaultMin = modifiedHabit.reminderMin;
@ -321,8 +322,11 @@ public class EditHabitFragment extends DialogFragment
timePicker.show(getFragmentManager(), "timePicker"); timePicker.show(getFragmentManager(), "timePicker");
} }
@SuppressWarnings("ConstantConditions")
private void onWeekdayClick() private void onWeekdayClick()
{ {
if(!modifiedHabit.hasReminder()) return;
WeekdayPickerDialog dialog = new WeekdayPickerDialog(); WeekdayPickerDialog dialog = new WeekdayPickerDialog();
dialog.setListener(this); dialog.setListener(this);
dialog.setSelectedDays(DateHelper.unpackWeekdayList(modifiedHabit.reminderDays)); dialog.setSelectedDays(DateHelper.unpackWeekdayList(modifiedHabit.reminderDays));
@ -334,14 +338,14 @@ public class EditHabitFragment extends DialogFragment
{ {
modifiedHabit.reminderHour = hour; modifiedHabit.reminderHour = hour;
modifiedHabit.reminderMin = minute; modifiedHabit.reminderMin = minute;
modifiedHabit.reminderDays = DateHelper.ALL_WEEK_DAYS;
updateReminder(); updateReminder();
} }
@Override @Override
public void onTimeCleared(RadialPickerLayout view) public void onTimeCleared(RadialPickerLayout view)
{ {
modifiedHabit.reminderHour = null; modifiedHabit.clearReminder();
modifiedHabit.reminderMin = null;
updateReminder(); updateReminder();
} }
@ -359,16 +363,93 @@ public class EditHabitFragment extends DialogFragment
} }
@Override @Override
@SuppressWarnings("ConstantConditions")
public void onSaveInstanceState(Bundle outState) public void onSaveInstanceState(Bundle outState)
{ {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putInt("color", modifiedHabit.color); outState.putInt("color", modifiedHabit.color);
if(modifiedHabit.reminderHour != null)
if(modifiedHabit.hasReminder())
{ {
outState.putInt("reminderMin", modifiedHabit.reminderMin); outState.putInt("reminderMin", modifiedHabit.reminderMin);
outState.putInt("reminderHour", modifiedHabit.reminderHour); outState.putInt("reminderHour", modifiedHabit.reminderHour);
outState.putInt("reminderDays", modifiedHabit.reminderDays); outState.putInt("reminderDays", modifiedHabit.reminderDays);
} }
} }
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
{
if(parent.getId() == R.id.sFrequency)
{
switch (position)
{
case 0:
modifiedHabit.freqNum = 1;
modifiedHabit.freqDen = 1;
break;
case 1:
modifiedHabit.freqNum = 1;
modifiedHabit.freqDen = 7;
break;
case 2:
modifiedHabit.freqNum = 2;
modifiedHabit.freqDen = 7;
break;
case 3:
modifiedHabit.freqNum = 5;
modifiedHabit.freqDen = 7;
break;
case 4:
modifiedHabit.freqNum = 3;
modifiedHabit.freqDen = 7;
break;
}
}
updateFrequency();
}
@SuppressLint("SetTextI18n")
private void updateFrequency()
{
int quickSelectPosition = -1;
if(modifiedHabit.freqNum.equals(modifiedHabit.freqDen))
quickSelectPosition = 0;
else if(modifiedHabit.freqNum == 1 && modifiedHabit.freqDen == 7)
quickSelectPosition = 1;
else if(modifiedHabit.freqNum == 2 && modifiedHabit.freqDen == 7)
quickSelectPosition = 2;
else if(modifiedHabit.freqNum == 5 && modifiedHabit.freqDen == 7)
quickSelectPosition = 3;
if(quickSelectPosition >= 0)
{
sFrequency.setVisibility(View.VISIBLE);
sFrequency.setSelection(quickSelectPosition);
llCustomFrequency.setVisibility(View.GONE);
tvFreqNum.setText(modifiedHabit.freqNum.toString());
tvFreqDen.setText(modifiedHabit.freqDen.toString());
}
else
{
sFrequency.setVisibility(View.GONE);
llCustomFrequency.setVisibility(View.VISIBLE);
}
}
@Override
public void onNothingSelected(AdapterView<?> parent)
{
}
} }

@ -27,7 +27,7 @@ import android.widget.BaseAdapter;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.ListHabitsHelper; import org.isoron.uhabits.helpers.ListHabitsHelper;
import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.loaders.HabitListLoader;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save