Merge branch 'feature/refactoring-mvc' into dev

pull/145/head
Alinson S. Xavier 9 years ago
commit 31fdae1c8b

@ -1,4 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'me.tatarka.retrolambda'
android {
compileSdkVersion 23
@ -13,7 +15,7 @@ android {
buildConfigField "String", "databaseFilename", "\"uhabits.db\""
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//testInstrumentationRunnerArgument "size", "small"
testInstrumentationRunnerArgument "size", "medium"
}
buildTypes {
@ -29,6 +31,22 @@ android {
lintOptions {
checkReleaseBuilds false
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
testOptions {
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen { false }
showStandardStreams = true
}
}
}
}
dependencies {
@ -40,12 +58,29 @@ dependencies {
compile 'org.apmem.tools:layouts:1.10@aar'
compile 'com.opencsv:opencsv:3.7'
compile 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT'
compile 'org.jetbrains:annotations-java5:15.0'
compile 'com.jakewharton:butterknife:8.0.1'
apt 'com.jakewharton:butterknife-compiler:8.0.1'
compile 'com.google.dagger:dagger:2.2'
apt 'com.google.dagger:dagger-compiler:2.2'
testApt 'com.google.dagger:dagger-compiler:2.2'
androidTestApt 'com.google.dagger:dagger-compiler:2.2'
provided 'javax.annotation:jsr250-api:1.0'
compile project(':libs:drag-sort-listview:library')
testCompile 'junit:junit:4.12'
testCompile 'org.hamcrest:hamcrest-library:1.3'
testCompile 'org.mockito:mockito-core:1.10.19'
androidTestCompile 'com.android.support:support-annotations:23.3.0'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
androidTestCompile 'org.mockito:mockito-core:1.10.19'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.1') {
exclude group: 'com.android.support'
@ -60,13 +95,6 @@ dependencies {
}
}
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
}
retrolambda {
defaultMethods true
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@ -0,0 +1,32 @@
/*
* 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 javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = {AndroidModule.class})
public interface AndroidTestComponent extends BaseComponent
{
void inject(BaseAndroidTest baseAndroidTest);
}

@ -0,0 +1,135 @@
/*
* 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.appwidget.*;
import android.content.*;
import android.os.*;
import android.support.annotation.*;
import android.support.test.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import java.util.*;
import java.util.concurrent.*;
import javax.inject.*;
import static junit.framework.Assert.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
public class BaseAndroidTest
{
// 8:00am, January 25th, 2015 (UTC)
public static final long FIXED_LOCAL_TIME = 1422172800000L;
private static boolean isLooperPrepared;
protected Context testContext;
protected Context targetContext;
@Inject
protected Preferences prefs;
@Inject
protected HabitList habitList;
@Inject
protected CommandRunner commandRunner;
protected AndroidTestComponent androidTestComponent;
protected HabitFixtures fixtures;
protected CountDownLatch latch;
@Before
public void setUp()
{
if (!isLooperPrepared)
{
Looper.prepare();
isLooperPrepared = true;
}
targetContext = InstrumentationRegistry.getTargetContext();
testContext = InstrumentationRegistry.getContext();
InterfaceUtils.setFixedTheme(R.style.AppBaseTheme);
DateUtils.setFixedLocalTime(FIXED_LOCAL_TIME);
androidTestComponent = DaggerAndroidTestComponent.builder().build();
HabitsApplication.setComponent(androidTestComponent);
androidTestComponent.inject(this);
fixtures = new HabitFixtures(habitList);
latch = new CountDownLatch(1);
}
protected void assertWidgetProviderIsInstalled(Class componentClass)
{
ComponentName provider =
new ComponentName(targetContext, componentClass);
AppWidgetManager manager = AppWidgetManager.getInstance(targetContext);
List<ComponentName> installedProviders = new LinkedList<>();
for (AppWidgetProviderInfo info : manager.getInstalledProviders())
installedProviders.add(info.provider);
assertThat(installedProviders, hasItems(provider));
}
protected void setTheme(@StyleRes int themeId)
{
InterfaceUtils.setFixedTheme(themeId);
targetContext.setTheme(themeId);
}
protected void sleep(int time)
{
try
{
Thread.sleep(time);
}
catch (InterruptedException e)
{
fail();
}
}
protected void waitForAsyncTasks()
throws InterruptedException, TimeoutException
{
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN)
{
Thread.sleep(1000);
return;
}
BaseTask.waitForTasks(10000);
}
}

@ -1,68 +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.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.helpers.UIHelper;
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();
UIHelper.setFixedTheme(R.style.AppBaseTheme);
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);
}
}

@ -17,47 +17,46 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
package org.isoron.uhabits;
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 android.graphics.*;
import android.os.*;
import android.support.annotation.*;
import android.view.*;
import android.widget.*;
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 org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.utils.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import static junit.framework.Assert.fail;
import static android.view.View.MeasureSpec.*;
import static junit.framework.Assert.*;
public class ViewTest extends BaseTest
public class BaseViewTest extends BaseAndroidTest
{
protected static final double SIMILARITY_CUTOFF = 0.09;
protected static final double DEFAULT_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);
private double similarityCutoff;
view.measure(specWidth, specHeight);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
@Override
public void setUp()
{
super.setUp();
similarityCutoff = DEFAULT_SIMILARITY_CUTOFF;
}
protected void assertRenders(View view, String expectedImagePath) throws IOException
protected void assertRenders(View view, String expectedImagePath)
throws IOException
{
StringBuilder errorMessage = new StringBuilder();
expectedImagePath = getVersionedViewAssetPath(expectedImagePath);
if (view.isLayoutRequested()) measureView(view, view.getMeasuredWidth(),
view.getMeasuredHeight());
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap actual = view.getDrawingCache();
@ -65,97 +64,127 @@ public class ViewTest extends BaseTest
int width = actual.getWidth();
int height = actual.getHeight();
Bitmap scaledExpected = Bitmap.createScaledBitmap(expected, width, height, true);
Bitmap scaledExpected =
Bitmap.createScaledBitmap(expected, width, height, true);
double distance;
boolean similarEnough = true;
if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) > SIMILARITY_CUTOFF)
if ((distance = compareHistograms(getHistogram(actual),
getHistogram(scaledExpected))) > similarityCutoff)
{
similarEnough = false;
errorMessage.append(String.format(
"Rendered image has wrong histogram (distance=%f). ",
distance));
"Rendered image has wrong histogram (distance=%f). ",
distance));
}
if(!similarEnough)
if (!similarEnough)
{
saveBitmap(expectedImagePath, ".expected", scaledExpected);
String path = saveBitmap(expectedImagePath, "", actual);
errorMessage.append(String.format("Actual rendered image " + "saved to %s", path));
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
@NonNull
protected FrameLayout convertToView(BaseWidget widget,
int width,
int height)
{
InputStream stream = testContext.getAssets().open(path);
return BitmapFactory.decodeStream(stream);
widget.setDimensions(
new WidgetDimensions(width, height, width, height));
FrameLayout view = new FrameLayout(targetContext);
RemoteViews remoteViews = widget.getPortraitRemoteViews();
view.addView(remoteViews.apply(targetContext, view));
measureView(view, width, height);
return view;
}
private String getVersionedViewAssetPath(String path)
protected int dpToPixels(int dp)
{
String result = null;
return (int) InterfaceUtils.dpToPixels(targetContext, dp);
}
if (android.os.Build.VERSION.SDK_INT >= 21)
{
try
{
String vpath = "views-v21/" + path;
testContext.getAssets().open(vpath);
result = vpath;
}
catch (IOException e)
{
// ignored
}
}
protected void measureView(View view, int width, int height)
{
int specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
int specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
if(result == null)
result = "views/" + path;
view.measure(specWidth, specHeight);
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}
return result;
protected void setSimilarityCutoff(double similarityCutoff)
{
this.similarityCutoff = similarityCutoff;
}
private String saveBitmap(String filename, String suffix, Bitmap bitmap)
throws IOException
protected void skipAnimation(View view)
{
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");
ViewPropertyAnimator animator = view.animate();
animator.setDuration(0);
animator.start();
}
filename = filename.replaceAll("\\.png$", suffix + ".png");
String absolutePath = String.format("%s/%s", dir.getAbsolutePath(), filename);
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();
}
File parent = new File(absolutePath).getParentFile();
if(!parent.exists() && !parent.mkdirs())
throw new RuntimeException(String.format("Could not create dir: %s",
parent.getAbsolutePath()));
private double compareHistograms(int[][] actualHistogram,
int[][] expectedHistogram)
{
long diff = 0;
long total = 0;
FileOutputStream out = new FileOutputStream(absolutePath);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
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]);
return absolutePath;
total += actualHistogram[0][i];
total += actualHistogram[1][i];
total += actualHistogram[2][i];
total += actualHistogram[3][i];
}
return (double) diff / total / 2;
}
private Bitmap getBitmapFromAssets(String path) throws IOException
{
InputStream stream = testContext.getAssets().open(path);
return BitmapFactory.decodeStream(stream);
}
private int[][] getHistogram(Bitmap bitmap)
{
int histogram[][] = new int[4][256 / HISTOGRAM_BIN_SIZE];
for(int x = 0; x < bitmap.getWidth(); x++)
for (int x = 0; x < bitmap.getWidth(); x++)
{
for(int y = 0; y < bitmap.getHeight(); y++)
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
(color >> 24) & 0xff, //alpha
(color >> 16) & 0xff, //red
(color >> 8) & 0xff, //green
(color) & 0xff //blue
};
histogram[0][argb[0] / HISTOGRAM_BIN_SIZE]++;
@ -168,59 +197,49 @@ public class ViewTest extends BaseTest
return histogram;
}
private double compareHistograms(int[][] actualHistogram, int[][] expectedHistogram)
private String getVersionedViewAssetPath(String path)
{
long diff = 0;
long total = 0;
String result = null;
for(int i = 0; i < 256 / HISTOGRAM_BIN_SIZE; i ++)
if (android.os.Build.VERSION.SDK_INT >= 21)
{
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];
try
{
String vpath = "views-v21/" + path;
testContext.getAssets().open(vpath);
result = vpath;
}
catch (IOException e)
{
// ignored
}
}
return (double) diff / total / 2;
}
if (result == null) result = "views/" + path;
protected int dpToPixels(int dp)
{
return (int) UIHelper.dpToPixels(targetContext, dp);
return result;
}
protected void tap(GestureDetector.OnGestureListener view, int x, int y) throws InterruptedException
private String saveBitmap(String filename, String suffix, Bitmap bitmap)
throws IOException
{
long now = SystemClock.uptimeMillis();
MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, dpToPixels(x),
dpToPixels(y), 0);
view.onSingleTapUp(e);
e.recycle();
}
File dir = FileUtils.getSDCardDir("test-screenshots");
if (dir == null) dir = FileUtils.getFilesDir("test-screenshots");
if (dir == null) throw new RuntimeException(
"Could not find suitable dir for screenshots");
protected void refreshData(final HabitDataView view)
{
new BaseTask()
{
@Override
protected void doInBackground()
{
view.refreshData();
}
}.execute();
filename = filename.replaceAll("\\.png$", suffix + ".png");
String absolutePath =
String.format("%s/%s", dir.getAbsolutePath(), filename);
try
{
waitForAsyncTasks();
}
catch (Exception e)
{
throw new RuntimeException("Time out");
}
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;
}
}

@ -0,0 +1,90 @@
/*
* 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 org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.DateUtils;
public class HabitFixtures
{
public boolean NON_DAILY_HABIT_CHECKS[] = {
true, false, false, true, true, true, false, false, true, true
};
private final HabitList habitList;
public HabitFixtures(HabitList habitList)
{
this.habitList = habitList;
}
public Habit createEmptyHabit()
{
Habit habit = new Habit();
habit.setName("Meditate");
habit.setDescription("Did you meditate this morning?");
habit.setColor(3);
habit.setFrequency(Frequency.DAILY);
habitList.add(habit);
return habit;
}
public Habit createLongHabit()
{
Habit habit = createEmptyHabit();
habit.setFrequency(new Frequency(3, 7));
habit.setColor(4);
long day = DateUtils.millisecondsInOneDay;
long today = DateUtils.getStartOfToday();
int marks[] = { 0, 1, 3, 5, 7, 8, 9, 10, 12, 14, 15, 17, 19, 20, 26, 27,
28, 50, 51, 52, 53, 54, 58, 60, 63, 65, 70, 71, 72, 73, 74, 75, 80,
81, 83, 89, 90, 91, 95, 102, 103, 108, 109, 120};
for (int mark : marks)
habit.getRepetitions().toggleTimestamp(today - mark * day);
return habit;
}
public Habit createShortHabit()
{
Habit habit = new Habit();
habit.setName("Wake up early");
habit.setDescription("Did you wake up before 6am?");
habit.setFrequency(new Frequency(2, 3));
habitList.add(habit);
long timestamp = DateUtils.getStartOfToday();
for (boolean c : NON_DAILY_HABIT_CHECKS)
{
if (c) habit.getRepetitions().toggleTimestamp(timestamp);
timestamp -= DateUtils.millisecondsInOneDay;
}
return habit;
}
public void purgeHabits(HabitList habitList)
{
for (Habit h : habitList.getAll(true))
habitList.remove(h);
}
}

@ -17,24 +17,24 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit;
package org.isoron.uhabits;
import android.os.Build;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.os.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.HabitsApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.isoron.uhabits.ui.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.IOException;
import java.io.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitsApplicationTest
public class HabitsApplicationTest extends BaseAndroidTest
{
@Test
public void test_getLogcat() throws IOException
@ -45,7 +45,11 @@ public class HabitsApplicationTest
String msg = "LOGCAT TEST";
new RuntimeException(msg).printStackTrace();
String log = HabitsApplication.getLogcat();
HabitsApplication app = HabitsApplication.getInstance();
assert(app != null);
BaseSystem system = new BaseSystem(targetContext);
String log = system.getLogcat();
assertThat(log, containsString(msg));
}
}

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
package org.isoron.uhabits.espresso;
import android.preference.Preference;
import android.view.View;
@ -39,7 +39,7 @@ public class HabitMatchers
@Override
public boolean matchesSafely(Habit habit)
{
return habit.name.equals(name);
return habit.getName().equals(name);
}
@Override
@ -51,7 +51,7 @@ public class HabitMatchers
@Override
public void describeMismatchSafely(Habit habit, Description description)
{
description.appendText("was ").appendText(habit.name);
description.appendText("was ").appendText(habit.getName());
}
};
}

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.ui;
package org.isoron.uhabits.espresso;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
@ -61,7 +61,7 @@ public class HabitViewActions
@Override
public void perform(UiController uiController, View view)
{
if (view.getId() != R.id.llButtons)
if (view.getId() != R.id.checkmarkPanel)
throw new InvalidParameterException("View must have id llButtons");
LinearLayout llButtons = (LinearLayout) view;

@ -0,0 +1,199 @@
/*
* 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.espresso;
import android.support.test.espresso.*;
import android.support.test.espresso.contrib.*;
import org.hamcrest.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.*;
import java.util.*;
import static android.support.test.espresso.Espresso.*;
import static android.support.test.espresso.Espresso.pressBack;
import static android.support.test.espresso.action.ViewActions.*;
import static android.support.test.espresso.assertion.ViewAssertions.*;
import static android.support.test.espresso.matcher.RootMatchers.*;
import static android.support.test.espresso.matcher.ViewMatchers.Visibility.*;
import static android.support.test.espresso.matcher.ViewMatchers.*;
import static org.hamcrest.Matchers.*;
public class MainActivityActions
{
public static String addHabit()
{
return addHabit(false);
}
public static String addHabit(boolean openDialogs)
{
String name = "New Habit " + new Random().nextInt(1000000);
String description = "Did you perform your new habit today?";
String num = "4";
String den = "8";
onView(withId(R.id.action_add)).perform(click());
typeHabitData(name, description, num, den);
if (openDialogs)
{
onView(withId(R.id.buttonPickColor)).perform(click());
pressBack();
onView(withId(R.id.tvReminderTime)).perform(click());
onView(withText("Done")).perform(click());
onView(withId(R.id.tvReminderDays)).perform(click());
onView(withText("OK")).perform(click());
}
onView(withId(R.id.buttonSave)).perform(click());
onData(Matchers.allOf(is(instanceOf(Habit.class)),
HabitMatchers.withName(name))).onChildView(withId(R.id.label));
return name;
}
public static void assertHabitExists(String name)
{
List<String> names = new LinkedList<>();
names.add(name);
assertHabitsExist(names);
}
public static void assertHabitsDontExist(List<String> names)
{
for (String name : names)
onView(withId(R.id.listView)).check(matches(Matchers.not(
HabitMatchers.containsHabit(HabitMatchers.withName(name)))));
}
public static void assertHabitsExist(List<String> names)
{
for (String name : names)
onData(Matchers.allOf(is(instanceOf(Habit.class)),
HabitMatchers.withName(name))).check(matches(isDisplayed()));
}
private static void clickHiddenMenuItem(int stringId)
{
try
{
// Try the ActionMode overflow menu first
onView(allOf(withContentDescription("More options"), withParent(
withParent(withClassName(containsString("Action")))))).perform(
click());
}
catch (Exception e1)
{
// Try the toolbar overflow menu
onView(allOf(withContentDescription("More options"), withParent(
withParent(withClassName(containsString("Toolbar")))))).perform(
click());
}
onView(withText(stringId)).perform(click());
}
public static void clickMenuItem(int stringId)
{
try
{
onView(withText(stringId)).perform(click());
}
catch (Exception e1)
{
try
{
onView(withContentDescription(stringId)).perform(click());
}
catch (Exception e2)
{
clickHiddenMenuItem(stringId);
}
}
}
public static void clickSettingsItem(String text)
{
onView(withClassName(containsString("RecyclerView"))).perform(
RecyclerViewActions.actionOnItem(
hasDescendant(withText(containsString(text))), click()));
}
public static void deleteHabit(String name)
{
deleteHabits(Collections.singletonList(name));
}
public static void deleteHabits(List<String> names)
{
selectHabits(names);
clickMenuItem(R.string.delete);
onView(withText("OK")).perform(click());
assertHabitsDontExist(names);
}
public static void selectHabit(String name)
{
selectHabits(Collections.singletonList(name));
}
public static void selectHabits(List<String> names)
{
boolean first = true;
for (String name : names)
{
onData(Matchers.allOf(is(instanceOf(Habit.class)),
HabitMatchers.withName(name)))
.onChildView(withId(R.id.label))
.perform(first ? longClick() : click());
first = false;
}
}
public static void typeHabitData(String name,
String description,
String num,
String den)
{
onView(withId(R.id.tvName)).perform(replaceText(name));
onView(withId(R.id.tvDescription)).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.tvFreqNum)).perform(replaceText(num));
onView(withId(R.id.tvFreqDen)).perform(replaceText(den));
}
}

@ -0,0 +1,317 @@
/*
* 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.espresso;
import android.app.*;
import android.content.*;
import android.support.test.*;
import android.support.test.espresso.*;
import android.support.test.espresso.intent.rule.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.hamcrest.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import static android.support.test.espresso.Espresso.*;
import static android.support.test.espresso.Espresso.pressBack;
import static android.support.test.espresso.action.ViewActions.*;
import static android.support.test.espresso.assertion.ViewAssertions.*;
import static android.support.test.espresso.intent.Intents.*;
import static android.support.test.espresso.intent.matcher.IntentMatchers.*;
import static android.support.test.espresso.matcher.ViewMatchers.*;
import static org.hamcrest.Matchers.*;
import static org.isoron.uhabits.espresso.HabitViewActions.*;
import static org.isoron.uhabits.espresso.MainActivityActions.*;
import static org.isoron.uhabits.espresso.ShowHabitActivityActions.*;
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainTest
{
private SystemHelper sys;
@Rule
public IntentsTestRule<MainActivity> activityRule =
new IntentsTestRule<>(MainActivity.class);
@Before
public void setup()
{
Context context =
InstrumentationRegistry.getInstrumentation().getContext();
sys = new SystemHelper(context);
sys.disableAllAnimations();
sys.acquireWakeLock();
sys.unlockScreen();
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();
}
public void skipTutorial()
{
try
{
for (int i = 0; i < 10; i++)
onView(allOf(withClassName(endsWith("AppCompatImageButton")),
isDisplayed())).perform(click());
}
catch (NoMatchingViewException e)
{
// ignored
}
}
@After
public void tearDown()
{
sys.releaseWakeLock();
}
/**
* User opens menu, clicks about, sees about screen.
*/
@Test
public void testAbout()
{
clickMenuItem(R.string.about);
onView(isRoot()).perform(swipeUp());
}
/**
* 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(Matchers.allOf(is(instanceOf(Habit.class)),
HabitMatchers.withName(name)))
.onChildView(withId(R.id.checkmarkPanel))
.perform(toggleAllCheckmarks());
Thread.sleep(1200);
onData(Matchers.allOf(is(instanceOf(Habit.class)),
HabitMatchers.withName(name)))
.onChildView(withId(R.id.label))
.perform(click());
onView(withId(R.id.scoreView)).perform(scrollTo(), swipeRight());
onView(withId(R.id.frequencyChart)).perform(scrollTo(), swipeRight());
}
/**
* 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.tvName)).check(matches(isDisplayed()));
}
/**
* 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);
clickMenuItem(R.string.archive);
assertHabitsDontExist(names);
clickMenuItem(R.string.show_archived);
assertHabitsExist(names);
selectHabits(names);
clickMenuItem(R.string.unarchive);
clickMenuItem(R.string.show_archived);
assertHabitsExist(names);
deleteHabits(names);
}
/**
* 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(Matchers.allOf(is(instanceOf(Habit.class)),
HabitMatchers.withName(name)))
.onChildView(withId(R.id.label))
.perform(longClick());
clickMenuItem(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);
clickMenuItem(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(Matchers.allOf(is(instanceOf(Habit.class)),
HabitMatchers.withName(name)))
.onChildView(withId(R.id.label))
.perform(click());
openHistoryEditor();
onView(withClassName(endsWith("HabitHistoryView"))).perform(
clickAtRandomLocations(20));
pressBack();
onView(withId(R.id.historyChart)).perform(scrollTo(), swipeRight(),
swipeLeft());
}
/**
* User creates a habit, opens settings, clicks export as CSV, is asked what
* activity should handle the file.
*/
@Test
public void testExportCSV()
{
addHabit();
clickMenuItem(R.string.settings);
clickSettingsItem("Export as CSV");
intended(hasAction(Intent.ACTION_SEND));
}
/**
* 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();
clickMenuItem(R.string.settings);
String date =
DateUtils.getBackupDateFormat().format(DateUtils.getLocalTime());
date = date.substring(0, date.length() - 2);
clickSettingsItem("Export full backup");
intended(hasAction(Intent.ACTION_SEND));
deleteHabit(name);
clickMenuItem(R.string.settings);
clickSettingsItem("Import data");
onData(
allOf(is(instanceOf(String.class)), startsWith("Backups"))).perform(
click());
onData(
allOf(is(instanceOf(String.class)), containsString(date))).perform(
click());
selectHabit(name);
}
/**
* User opens the settings and generates a bug report.
*/
@Test
public void testGenerateBugReport()
{
clickMenuItem(R.string.settings);
clickSettingsItem("Generate bug report");
intended(hasAction(Intent.ACTION_SEND));
}
/**
* User opens menu, clicks Help, sees website.
*/
@Test
public void testHelp()
{
clickMenuItem(R.string.help);
intended(hasAction(Intent.ACTION_VIEW));
}
/**
* User opens menu, clicks settings, sees settings screen.
*/
@Test
public void testSettings()
{
clickMenuItem(R.string.settings);
}
}

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

@ -1,4 +1,23 @@
package org.isoron.uhabits.ui;
/*
* 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.espresso;
import android.app.KeyguardManager;
import android.content.Context;

@ -17,18 +17,16 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.io;
package org.isoron.uhabits.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.BaseAndroidTest;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.utils.FileUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -45,50 +43,27 @@ import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitsCSVExporterTest extends BaseTest
public class HabitsCSVExporterTest extends BaseAndroidTest
{
private File baseDir;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
HabitFixtures.createShortHabit();
HabitFixtures.createEmptyHabit();
fixtures.purgeHabits(habitList);
fixtures.createShortHabit();
fixtures.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);
List<Habit> habits = habitList.getAll(true);
HabitsCSVExporter exporter = new HabitsCSVExporter(habits, baseDir);
String filename = exporter.writeArchive();
@ -105,14 +80,41 @@ public class HabitsCSVExporterTest extends BaseTest
assertPathExists("002 Meditate/Scores.csv");
}
private void assertAbsolutePathExists(String s)
{
File file = new File(s);
assertTrue(
String.format("File %s should exist", file.getAbsolutePath()),
file.exists());
}
private void assertPathExists(String s)
{
assertAbsolutePathExists(String.format("%s/%s", baseDir.getAbsolutePath(), s));
assertAbsolutePathExists(
String.format("%s/%s", baseDir.getAbsolutePath(), s));
}
private void assertAbsolutePathExists(String s)
private void unzip(File file) throws IOException
{
File file = new File(s);
assertTrue(String.format("File %s should exist", file.getAbsolutePath()), file.exists());
ZipFile zip = new ZipFile(file);
Enumeration<? 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();
FileUtils.copy(stream, outputFile);
}
zip.close();
}
}

@ -17,95 +17,76 @@
* 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;
package org.isoron.uhabits.io;
import android.content.*;
import android.support.test.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.*;
import java.util.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ImportTest extends BaseTest
public class ImportTest extends BaseAndroidTest
{
private File baseDir;
private Context context;
@Before
public void setup()
public void setUp()
{
super.setup();
DateHelper.setFixedLocalTime(null);
super.setUp();
DateUtils.setFixedLocalTime(null);
HabitFixtures.purgeHabits();
fixtures.purgeHabits(habitList);
context = InstrumentationRegistry.getInstrumentation().getContext();
baseDir = DatabaseHelper.getFilesDir("Backups");
if(baseDir == null) fail("baseDir should not be null");
baseDir = FileUtils.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
@Test
public void testHabitBullCSV() 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));
importFromFile("habitbull.csv");
importer.importHabitsFromFile(file);
}
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(4));
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());
Habit habit = habits.get(0);
assertThat(habit.getName(), equalTo("Breed dragons"));
assertThat(habit.getDescription(), equalTo("with love and fire"));
assertThat(habit.getFrequency(), equalTo(Frequency.DAILY));
assertTrue(containsRepetition(habit, 2016, 3, 18));
assertTrue(containsRepetition(habit, 2016, 3, 19));
assertFalse(containsRepetition(habit, 2016, 3, 20));
}
@Test
public void testTickmateDB() throws IOException
public void testLoopDB() throws IOException
{
importFromFile("tickmate.db");
importFromFile("loop.db");
List<Habit> habits = Habit.getAll(true);
assertThat(habits.size(), equalTo(3));
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(9));
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));
Habit habit = habits.get(0);
assertThat(habit.getName(), equalTo("Wake up early"));
assertThat(habit.getFrequency(), equalTo(Frequency.THREE_TIMES_PER_WEEK));
assertTrue(containsRepetition(habit, 2016, 3, 14));
assertTrue(containsRepetition(habit, 2016, 3, 16));
assertFalse(containsRepetition(habit, 2016, 3, 17));
}
@Test
@ -113,13 +94,13 @@ public class ImportTest extends BaseTest
{
importFromFile("rewire.db");
List<Habit> habits = Habit.getAll(true);
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(3));
Habit habit = habits.get(0);
assertThat(habit.name, equalTo("Wake up early"));
assertThat(habit.freqNum, equalTo(3));
assertThat(habit.freqDen, equalTo(7));
assertThat(habit.getName(), equalTo("Wake up early"));
assertThat(habit.getFrequency(),
equalTo(Frequency.THREE_TIMES_PER_WEEK));
assertFalse(habit.hasReminder());
assertFalse(containsRepetition(habit, 2015, 12, 31));
assertTrue(containsRepetition(habit, 2016, 1, 18));
@ -127,47 +108,59 @@ public class ImportTest extends BaseTest
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)));
assertThat(habit.getName(), equalTo("brush teeth"));
assertThat(habit.getFrequency(),
equalTo(Frequency.THREE_TIMES_PER_WEEK));
assertThat(habit.hasReminder(), equalTo(true));
Reminder reminder = habit.getReminder();
assertThat(reminder.getHour(), equalTo(8));
assertThat(reminder.getMinute(), equalTo(0));
boolean[] reminderDays = { false, true, true, true, true, true, false };
assertThat(reminder.getDays(),
equalTo(DateUtils.packWeekdayList(reminderDays)));
}
@Test
public void testHabitBullCSV() throws IOException
public void testTickmateDB() throws IOException
{
importFromFile("habitbull.csv");
importFromFile("tickmate.db");
List<Habit> habits = Habit.getAll(true);
assertThat(habits.size(), equalTo(4));
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(3));
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));
Habit h = habits.get(0);
assertThat(h.getName(), 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 testLoopDB() throws IOException
private boolean containsRepetition(Habit h, int year, int month, int day)
{
importFromFile("loop.db");
GregorianCalendar date = DateUtils.getStartOfTodayCalendar();
date.set(year, month - 1, day);
return h.getRepetitions().containsTimestamp(date.getTimeInMillis());
}
List<Habit> habits = Habit.getAll(true);
assertThat(habits.size(), equalTo(9));
private void copyAssetToFile(String assetPath, File dst) throws IOException
{
InputStream in = context.getAssets().open(assetPath);
FileUtils.copy(in, dst);
}
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));
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);
}
}

@ -0,0 +1,117 @@
/*
* 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.models.sqlite;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteCheckmarkListTest extends BaseAndroidTest
{
private Habit habit;
private CheckmarkList checkmarks;
private long today;
private long day;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
checkmarks = habit.getCheckmarks();
checkmarks.getToday(); // compute checkmarks
today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testAdd()
{
checkmarks.invalidateNewerThan(0);
List<Checkmark> list = new LinkedList<>();
list.add(new Checkmark(0, 0));
list.add(new Checkmark(1, 1));
list.add(new Checkmark(2, 2));
checkmarks.add(list);
List<CheckmarkRecord> records = getAllRecords();
assertThat(records.size(), equalTo(3));
assertThat(records.get(0).timestamp, equalTo(2L));
}
@Test
public void testGetByInterval()
{
long from = today - 10 * day;
long to = today - 3 * day;
List<Checkmark> list = checkmarks.getByInterval(from, to);
assertThat(list.size(), equalTo(8));
assertThat(list.get(0).getTimestamp(), equalTo(today - 3 * day));
assertThat(list.get(3).getTimestamp(), equalTo(today - 6 * day));
assertThat(list.get(7).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testInvalidateNewerThan()
{
List<CheckmarkRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
checkmarks.invalidateNewerThan(today - 20 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(100));
assertThat(records.get(0).timestamp, equalTo(today - 21 * day));
}
private List<CheckmarkRecord> getAllRecords()
{
return new Select()
.from(CheckmarkRecord.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc")
.execute();
}
}

@ -0,0 +1,229 @@
/*
* 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.models.sqlite;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.junit.*;
import org.junit.rules.*;
import org.junit.runner.*;
import java.util.*;
import static junit.framework.Assert.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.core.IsEqual.*;
@SuppressWarnings("JavaDoc")
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteHabitListTest extends BaseAndroidTest
{
@Rule
public ExpectedException exception = ExpectedException.none();
@Override
public void setUp()
{
super.setUp();
fixtures.purgeHabits(habitList);
for (int i = 0; i < 10; i++)
{
Habit h = new Habit();
h.setName("habit " + i);
h.setId((long) i);
if (i % 2 == 0) h.setArchived(true);
HabitRecord record = new HabitRecord();
record.copyFrom(h);
record.position = i;
record.save(i);
}
}
@Test
public void testAdd_withDuplicate()
{
Habit habit = new Habit();
habitList.add(habit);
exception.expect(IllegalArgumentException.class);
habitList.add(habit);
}
@Test
public void testAdd_withId()
{
Habit habit = new Habit();
habit.setName("Hello world with id");
habit.setId(12300L);
habitList.add(habit);
assertThat(habit.getId(), equalTo(12300L));
HabitRecord record = getRecord(12300L);
assertNotNull(record);
assertThat(record.name, equalTo(habit.getName()));
}
@Test
public void testAdd_withoutId()
{
Habit habit = new Habit();
habit.setName("Hello world");
assertNull(habit.getId());
habitList.add(habit);
assertNotNull(habit.getId());
HabitRecord record = getRecord(habit.getId());
assertNotNull(record);
assertThat(record.name, equalTo(habit.getName()));
}
@Test
public void testCountActive()
{
assertThat(habitList.countActive(), equalTo(5));
}
@Test
public void testCountWithArchived()
{
assertThat(habitList.countWithArchived(), equalTo(10));
}
@Test
public void testGetAll_withArchived()
{
List<Habit> habits = habitList.getAll(true);
assertThat(habits.size(), equalTo(10));
assertThat(habits.get(3).getName(), equalTo("habit 3"));
}
@Test
public void testGetAll_withoutArchived()
{
List<Habit> habits = habitList.getAll(false);
assertThat(habits.size(), equalTo(5));
assertThat(habits.get(3).getName(), equalTo("habit 7"));
List<Habit> another = habitList.getAll(false);
assertThat(habits, equalTo(another));
}
@Test
public void testGetById()
{
Habit h1 = habitList.getById(0);
assertNotNull(h1);
assertThat(h1.getName(), equalTo("habit 0"));
Habit h2 = habitList.getById(0);
assertNotNull(h2);
assertThat(h1, equalTo(h2));
}
@Test
public void testGetById_withInvalid()
{
long invalidId = 9183792001L;
Habit h1 = habitList.getById(invalidId);
assertNull(h1);
}
@Test
public void testGetByPosition()
{
Habit h = habitList.getByPosition(5);
assertNotNull(h);
assertThat(h.getName(), equalTo("habit 5"));
h = habitList.getByPosition(5000);
assertNull(h);
}
@Test
public void testIndexOf()
{
Habit h1 = habitList.getByPosition(5);
assertNotNull(h1);
assertThat(habitList.indexOf(h1), equalTo(5));
Habit h2 = new Habit();
assertThat(habitList.indexOf(h2), equalTo(-1));
h2.setId(1000L);
assertThat(habitList.indexOf(h2), equalTo(-1));
}
@Test
public void test_reorder()
{
// Same as HabitListTest.java
// TODO: remove duplication
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 = habitList.getByPosition(from);
Habit toHabit = habitList.getByPosition(to);
habitList.reorder(fromHabit, toHabit);
int actualPositions[] = new int[10];
for (int j = 0; j < 10; j++)
{
Habit h = habitList.getById(j);
assertNotNull(h);
actualPositions[j] = habitList.indexOf(h);
}
assertThat(actualPositions, equalTo(expectedPosition[i]));
}
}
private HabitRecord getRecord(long id)
{
return new Select()
.from(HabitRecord.class)
.where("id = ?", id)
.executeSingle();
}
}

@ -0,0 +1,143 @@
/*
* 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.models.sqlite;
import android.support.annotation.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.IsNot.not;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteRepetitionListTest extends BaseAndroidTest
{
private Habit habit;
private long today;
private RepetitionList repetitions;
private long day;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
repetitions = habit.getRepetitions();
today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testAdd()
{
RepetitionRecord record = getByTimestamp(today + day);
assertThat(record, is(nullValue()));
Repetition rep = new Repetition(today + day);
habit.getRepetitions().add(rep);
record = getByTimestamp(today + day);
assertThat(record, is(not(nullValue())));
}
@Test
public void testGetByInterval()
{
List<Repetition> reps =
repetitions.getByInterval(today - 10 * day, today);
assertThat(reps.size(), equalTo(8));
assertThat(reps.get(0).getTimestamp(), equalTo(today - 10 * day));
assertThat(reps.get(4).getTimestamp(), equalTo(today - 5 * day));
assertThat(reps.get(5).getTimestamp(), equalTo(today - 3 * day));
}
@Test
public void testGetByTimestamp()
{
Repetition rep = repetitions.getByTimestamp(today);
assertThat(rep, is(not(nullValue())));
assertThat(rep.getTimestamp(), equalTo(today));
rep = repetitions.getByTimestamp(today - 2 * day);
assertThat(rep, is(nullValue()));
}
@Test
public void testGetOldest()
{
Repetition rep = repetitions.getOldest();
assertThat(rep, is(not(nullValue())));
assertThat(rep.getTimestamp(), equalTo(today - 120 * day));
}
@Test
public void testGetOldest_withEmptyHabit()
{
Habit empty = fixtures.createEmptyHabit();
Repetition rep = empty.getRepetitions().getOldest();
assertThat(rep, is(nullValue()));
}
@Test
public void testRemove()
{
RepetitionRecord record = getByTimestamp(today);
assertThat(record, is(not(nullValue())));
Repetition rep = record.toRepetition();
repetitions.remove(rep);
record = getByTimestamp(today);
assertThat(record, is(nullValue()));
}
@Nullable
private RepetitionRecord getByTimestamp(long timestamp)
{
return selectByTimestamp(timestamp).executeSingle();
}
@NonNull
private From selectByTimestamp(long timestamp)
{
return new Select()
.from(RepetitionRecord.class)
.where("habit = ?", habit.getId())
.and("timestamp = ?", timestamp);
}
}

@ -0,0 +1,126 @@
/*
* 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.models.sqlite;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import com.activeandroid.query.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.records.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.util.*;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
@SuppressWarnings("JavaDoc")
@RunWith(AndroidJUnit4.class)
@MediumTest
public class SQLiteScoreListTest extends BaseAndroidTest
{
private Habit habit;
private ScoreList scores;
private long today;
private long day;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createLongHabit();
scores = habit.getScores();
today = DateUtils.getStartOfToday();
day = DateUtils.millisecondsInOneDay;
}
@Test
public void testGetAll()
{
List<Score> list = scores.getAll();
assertThat(list.size(), equalTo(121));
assertThat(list.get(0).getTimestamp(), equalTo(today));
assertThat(list.get(10).getTimestamp(), equalTo(today - 10 * day));
}
@Test
public void testInvalidateNewerThan()
{
scores.getTodayValue(); // force recompute
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(121));
scores.invalidateNewerThan(today - 10 * day);
records = getAllRecords();
assertThat(records.size(), equalTo(110));
assertThat(records.get(0).timestamp, equalTo(today - 11 * day));
}
@Test
public void testAdd()
{
new Delete().from(ScoreRecord.class).execute();
List<Score> list = new LinkedList<>();
list.add(new Score(today, 0));
list.add(new Score(today - day, 0));
list.add(new Score(today - 2 * day, 0));
scores.add(list);
List<ScoreRecord> records = getAllRecords();
assertThat(records.size(), equalTo(3));
assertThat(records.get(0).timestamp, equalTo(today));
}
@Test
public void testGetByTimestamp()
{
Score s = scores.getByTimestamp(today);
assertNotNull(s);
assertThat(s.getTimestamp(), equalTo(today));
s = scores.getByTimestamp(today - 200 * day);
assertNull(s);
}
private List<ScoreRecord> getAllRecords()
{
return new Select()
.from(ScoreRecord.class)
.where("habit = ?", habit.getId())
.orderBy("timestamp desc")
.execute();
}
}

@ -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.tasks;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.*;
import java.util.*;
import static junit.framework.Assert.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.IsNot.not;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ExportCSVTaskTest extends BaseAndroidTest
{
@Before
public void setUp()
{
super.setUp();
}
@Test
public void testExportCSV() throws Throwable
{
fixtures.createShortHabit();
List<Habit> habits = habitList.getAll(true);
ExportCSVTask task = new ExportCSVTask(habits, null);
task.setListener(archiveFilename -> {
assertThat(archiveFilename, is(not(nullValue())));
File f = new File(archiveFilename);
assertTrue(f.exists());
assertTrue(f.canRead());
});
task.execute();
waitForAsyncTasks();
}
}

@ -17,16 +17,12 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.tasks;
package org.isoron.uhabits.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.isoron.uhabits.BaseAndroidTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -41,21 +37,18 @@ import static org.hamcrest.core.IsNot.not;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ExportDBTaskTest extends BaseTest
public class ExportDBTaskTest extends BaseAndroidTest
{
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
}
@Test
public void testExportCSV() throws Throwable
{
Context context = InstrumentationRegistry.getContext();
ProgressBar bar = new ProgressBar(context);
ExportDBTask task = new ExportDBTask(bar);
ExportDBTask task = new ExportDBTask(null);
task.setListener(new ExportDBTask.Listener()
{
@Override

@ -17,16 +17,14 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.tasks;
package org.isoron.uhabits.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.isoron.uhabits.BaseAndroidTest;
import org.isoron.uhabits.utils.FileUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -41,23 +39,23 @@ import static org.junit.Assert.fail;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ImportDataTaskTest extends BaseTest
public class ImportDataTaskTest extends BaseAndroidTest
{
private File baseDir;
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
baseDir = DatabaseHelper.getFilesDir("Backups");
baseDir = FileUtils.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);
FileUtils.copy(in, dst);
}
private void assertTaskResult(final int expectedResult, String assetFilename) throws Throwable
@ -67,7 +65,7 @@ public class ImportDataTaskTest extends BaseTest
task.setListener(new ImportDataTask.Listener()
{
@Override
public void onImportFinished(int result)
public void onImportDataFinished(int result)
{
assertThat(result, equalTo(expectedResult));
}
@ -80,11 +78,9 @@ public class ImportDataTaskTest extends BaseTest
@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);
return new ImportDataTask(file, null);
}
@Test

@ -1,225 +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.uhabits.ui;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.contrib.RecyclerViewActions;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import java.util.Collections;
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.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.replaceText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.RootMatchers.isPlatformPopup;
import static android.support.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE;
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
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.withParent;
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.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.isoron.uhabits.ui.HabitMatchers.containsHabit;
import static org.isoron.uhabits.ui.HabitMatchers.withName;
public class MainActivityActions
{
public static String addHabit()
{
return addHabit(false);
}
public static String addHabit(boolean openDialogs)
{
String name = "New Habit " + new Random().nextInt(1000000);
String description = "Did you perform your new habit today?";
String num = "4";
String den = "8";
onView(withId(R.id.action_add))
.perform(click());
typeHabitData(name, description, num, den);
if(openDialogs)
{
onView(withId(R.id.buttonPickColor))
.perform(click());
pressBack();
onView(withId(R.id.inputReminderTime))
.perform(click());
onView(withText("Done"))
.perform(click());
onView(withId(R.id.inputReminderDays))
.perform(click());
onView(withText("OK"))
.perform(click());
}
onView(withId(R.id.buttonSave))
.perform(click());
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label));
return name;
}
public static void typeHabitData(String name, String description, String num, String den)
{
onView(withId(R.id.input_name))
.perform(replaceText(name));
onView(withId(R.id.input_description))
.perform(replaceText(description));
try
{
onView(allOf(withId(R.id.sFrequency), withEffectiveVisibility(VISIBLE)))
.perform(click());
onData(allOf(instanceOf(String.class), startsWith("Custom")))
.inRoot(isPlatformPopup())
.perform(click());
}
catch(NoMatchingViewException e)
{
// ignored
}
onView(withId(R.id.input_freq_num))
.perform(replaceText(num));
onView(withId(R.id.input_freq_den))
.perform(replaceText(den));
}
public static void selectHabit(String name)
{
selectHabits(Collections.singletonList(name));
}
public static void selectHabits(List<String> names)
{
boolean first = true;
for(String name : names)
{
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.onChildView(withId(R.id.label))
.perform(first ? longClick() : click());
first = false;
}
}
public static void assertHabitsDontExist(List<String> names)
{
for(String name : names)
onView(withId(R.id.listView))
.check(matches(not(containsHabit(withName(name)))));
}
public static void assertHabitExists(String name)
{
List<String> names = new LinkedList<>();
names.add(name);
assertHabitsExist(names);
}
public static void assertHabitsExist(List<String> names)
{
for(String name : names)
onData(allOf(is(instanceOf(Habit.class)), withName(name)))
.check(matches(isDisplayed()));
}
public static void deleteHabit(String name)
{
deleteHabits(Collections.singletonList(name));
}
public static void deleteHabits(List<String> names)
{
selectHabits(names);
clickMenuItem(R.string.delete);
onView(withText("OK"))
.perform(click());
assertHabitsDontExist(names);
}
public static void clickMenuItem(int stringId)
{
try
{
onView(withText(stringId)).perform(click());
}
catch (Exception e1)
{
try
{
onView(withContentDescription(stringId)).perform(click());
}
catch(Exception e2)
{
clickHiddenMenuItem(stringId);
}
}
}
private static void clickHiddenMenuItem(int stringId)
{
try
{
// Try the ActionMode overflow menu first
onView(allOf(withContentDescription("More options"), withParent(withParent(
withClassName(containsString("Action")))))).perform(click());
}
catch (Exception e1)
{
// Try the toolbar overflow menu
onView(allOf(withContentDescription("More options"), withParent(withParent(
withClassName(containsString("Toolbar")))))).perform(click());
}
onView(withText(stringId)).perform(click());
}
public static void clickSettingsItem(String text)
{
onView(withClassName(containsString("RecyclerView")))
.perform(RecyclerViewActions.actionOnItem(
hasDescendant(withText(containsString(text))),
click()));
}
}

@ -1,346 +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.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.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 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.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.clickMenuItem;
import static org.isoron.uhabits.ui.MainActivityActions.clickSettingsItem;
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);
clickMenuItem(R.string.archive);
assertHabitsDontExist(names);
clickMenuItem(R.string.show_archived);
assertHabitsExist(names);
selectHabits(names);
clickMenuItem(R.string.unarchive);
clickMenuItem(R.string.show_archived);
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());
clickMenuItem(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);
clickMenuItem(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()
{
clickMenuItem(R.string.settings);
}
/**
* User opens menu, clicks about, sees about screen.
*/
@Test
public void testAbout()
{
clickMenuItem(R.string.about);
onView(isRoot()).perform(swipeUp());
}
/**
* User opens menu, clicks Help, sees website.
*/
@Test
public void testHelp()
{
clickMenuItem(R.string.help);
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();
clickMenuItem(R.string.settings);
String date = DateHelper.getBackupDateFormat().format(DateHelper.getLocalTime());
date = date.substring(0, date.length() - 2);
clickSettingsItem("Export full backup");
intended(hasAction(Intent.ACTION_SEND));
deleteHabit(name);
clickMenuItem(R.string.settings);
clickSettingsItem("Import data");
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();
clickMenuItem(R.string.settings);
clickSettingsItem("Export as CSV");
intended(hasAction(Intent.ACTION_SEND));
}
/**
* User opens the settings and generates a bug report.
*/
@Test
public void testGenerateBugReport()
{
clickMenuItem(R.string.settings);
clickSettingsItem("Generate bug report");
intended(hasAction(Intent.ACTION_SEND));
}
}

@ -17,64 +17,66 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
package org.isoron.uhabits.ui.common.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
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;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitFrequencyViewTest extends ViewTest
@MediumTest
public class FrequencyChartTest extends BaseViewTest
{
private HabitFrequencyView view;
public static final String BASE_PATH = "common/FrequencyChart/";
private FrequencyChart view;
@Override
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();
fixtures.purgeHabits(habitList);
Habit habit = fixtures.createLongHabit();
view = new HabitFrequencyView(targetContext);
view.setHabit(habit);
refreshData(view);
measureView(dpToPixels(300), dpToPixels(100), view);
view = new FrequencyChart(targetContext);
view.setFrequency(habit.getRepetitions().getWeekdayFrequency());
view.setColor(ColorUtils.getAndroidTestColor(habit.getColor()));
measureView(view, dpToPixels(300), dpToPixels(100));
}
@Test
public void testRender() throws Throwable
{
assertRenders(view, "HabitFrequencyView/render.png");
assertRenders(view, BASE_PATH + "render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
public void testRender_withDataOffset() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitFrequencyView/renderTransparent.png");
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, BASE_PATH + "renderDataOffset.png");
}
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "HabitFrequencyView/renderDifferentSize.png");
measureView(view, dpToPixels(200), dpToPixels(200));
assertRenders(view, BASE_PATH + "renderDifferentSize.png");
}
@Test
public void testRender_withDataOffset() throws Throwable
public void testRender_withTransparentBackground() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, "HabitFrequencyView/renderDataOffset.png");
view.setIsBackgroundTransparent(true);
assertRenders(view, BASE_PATH + "renderTransparent.png");
}
}

@ -0,0 +1,119 @@
/*
* 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.common.views;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class HistoryChartTest extends BaseViewTest
{
private static final String BASE_PATH = "common/HistoryChart/";
private HistoryChart chart;
@Override
@Before
public void setUp()
{
super.setUp();
fixtures.purgeHabits(habitList);
Habit habit = fixtures.createLongHabit();
chart = new HistoryChart(targetContext);
chart.setCheckmarks(habit.getCheckmarks().getAllValues());
chart.setColor(ColorUtils.getAndroidTestColor(habit.getColor()));
measureView(chart, dpToPixels(400), dpToPixels(200));
}
// @Test
// public void tapDate_atInvalidLocations() throws Throwable
// {
// int expectedCheckmarkValues[] = habit.getCheckmarks().getAllValues();
//
// chart.setIsEditable(true);
// tap(chart, 118, 13); // header
// tap(chart, 336, 60); // tomorrow's square
// tap(chart, 370, 60); // right axis
// waitForAsyncTasks();
//
// int actualCheckmarkValues[] = habit.getCheckmarks().getAllValues();
// assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues));
// }
//
// @Test
// public void tapDate_withEditableView() throws Throwable
// {
// chart.setIsEditable(true);
// tap(chart, 340, 40); // today's square
// waitForAsyncTasks();
//
// long today = DateUtils.getStartOfToday();
// assertFalse(habit.getRepetitions().containsTimestamp(today));
// }
//
// @Test
// public void tapDate_withReadOnlyView() throws Throwable
// {
// chart.setIsEditable(false);
// tap(chart, 340, 40); // today's square
// waitForAsyncTasks();
//
// long today = DateUtils.getStartOfToday();
// assertTrue(habit.getRepetitions().containsTimestamp(today));
// }
@Test
public void testRender() throws Throwable
{
assertRenders(chart, BASE_PATH + "render.png");
}
@Test
public void testRender_withDataOffset() throws Throwable
{
chart.onScroll(null, null, -dpToPixels(150), 0);
chart.invalidate();
assertRenders(chart, BASE_PATH + "renderDataOffset.png");
}
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(chart, dpToPixels(200), dpToPixels(200));
assertRenders(chart, BASE_PATH + "renderDifferentSize.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
{
chart.setIsBackgroundTransparent(true);
assertRenders(chart, BASE_PATH + "renderTransparent.png");
}
}

@ -17,35 +17,37 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
package org.isoron.uhabits.ui.common.views;
import android.graphics.Color;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.graphics.*;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
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 org.isoron.uhabits.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.IOException;
import java.io.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class RingViewTest extends ViewTest
@MediumTest
public class RingViewTest extends BaseViewTest
{
private static final String BASE_PATH = "common/RingView/";
private RingView view;
@Override
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
view = new RingView(targetContext);
view.setPercentage(0.6f);
view.setText("60%");
view.setColor(ColorHelper.CSV_PALETTE[0]);
view.setColor(ColorUtils.getAndroidTestColor(0));
view.setBackgroundColor(Color.WHITE);
view.setThickness(dpToPixels(3));
}
@ -53,17 +55,17 @@ public class RingViewTest extends ViewTest
@Test
public void testRender_base() throws IOException
{
measureView(dpToPixels(100), dpToPixels(100), view);
assertRenders(view, "RingView/render.png");
measureView(view, dpToPixels(100), dpToPixels(100));
assertRenders(view, BASE_PATH + "render.png");
}
@Test
public void testRender_withDifferentParams() throws IOException
{
view.setPercentage(0.25f);
view.setColor(ColorHelper.CSV_PALETTE[5]);
view.setColor(ColorUtils.getAndroidTestColor(5));
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "RingView/renderDifferentParams.png");
measureView(view, dpToPixels(200), dpToPixels(200));
assertRenders(view, BASE_PATH + "renderDifferentParams.png");
}
}

@ -17,88 +17,92 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
package org.isoron.uhabits.ui.common.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import android.util.*;
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;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitScoreViewTest extends ViewTest
@MediumTest
public class ScoreChartTest extends BaseViewTest
{
private static final String BASE_PATH = "common/ScoreChart/";
private Habit habit;
private HabitScoreView view;
private ScoreChart view;
@Override
@Before
public void setup()
public void setUp()
{
super.setup();
super.setUp();
HabitFixtures.purgeHabits();
habit = HabitFixtures.createLongHabit();
fixtures.purgeHabits(habitList);
habit = fixtures.createLongHabit();
view = new HabitScoreView(targetContext);
view.setHabit(habit);
view = new ScoreChart(targetContext);
view.setScores(habit.getScores().getAll());
view.setColor(ColorUtils.getColor(targetContext, habit.getColor()));
view.setBucketSize(7);
refreshData(view);
measureView(dpToPixels(300), dpToPixels(200), view);
measureView(view, dpToPixels(300), dpToPixels(200));
}
@Test
public void testRender() throws Throwable
{
Log.d("HabitScoreViewTest", String.format("height=%d", dpToPixels(100)));
assertRenders(view, "HabitScoreView/render.png");
Log.d("HabitScoreViewTest",
String.format("height=%d", dpToPixels(100)));
assertRenders(view, BASE_PATH + "render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
public void testRender_withDataOffset() throws Throwable
{
view.setIsTransparencyEnabled(true);
assertRenders(view, "HabitScoreView/renderTransparent.png");
view.onScroll(null, null, -dpToPixels(150), 0);
view.invalidate();
assertRenders(view, BASE_PATH + "renderDataOffset.png");
}
@Test
public void testRender_withDifferentSize() throws Throwable
{
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "HabitScoreView/renderDifferentSize.png");
measureView(view, dpToPixels(200), dpToPixels(200));
assertRenders(view, BASE_PATH + "renderDifferentSize.png");
}
@Test
public void testRender_withDataOffset() throws Throwable
public void testRender_withMonthlyBucket() throws Throwable
{
view.onScroll(null, null, -dpToPixels(150), 0);
view.setScores(habit.getScores().groupBy(DateUtils.TruncateField.MONTH));
view.setBucketSize(30);
view.invalidate();
assertRenders(view, "HabitScoreView/renderDataOffset.png");
assertRenders(view, BASE_PATH + "renderMonthly.png");
}
@Test
public void testRender_withMonthlyBucket() throws Throwable
public void testRender_withTransparentBackground() throws Throwable
{
view.setBucketSize(30);
view.refreshData();
view.invalidate();
assertRenders(view, "HabitScoreView/renderMonthly.png");
view.setIsTransparencyEnabled(true);
assertRenders(view, BASE_PATH + "renderTransparent.png");
}
@Test
public void testRender_withYearlyBucket() throws Throwable
{
view.setScores(habit.getScores().groupBy(DateUtils.TruncateField.YEAR));
view.setBucketSize(365);
view.refreshData();
view.invalidate();
assertRenders(view, "HabitScoreView/renderYearly.png");
assertRenders(view, BASE_PATH + "renderYearly.png");
}
}

@ -17,58 +17,57 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.views;
package org.isoron.uhabits.ui.common.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
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;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class HabitStreakViewTest extends ViewTest
@MediumTest
public class StreakChartTest extends BaseViewTest
{
private HabitStreakView view;
private static final String BASE_PATH = "common/StreakChart/";
private StreakChart view;
@Override
@Before
public void setup()
public void setUp()
{
super.setup();
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createLongHabit();
super.setUp();
view = new HabitStreakView(targetContext);
measureView(dpToPixels(300), dpToPixels(100), view);
fixtures.purgeHabits(habitList);
Habit habit = fixtures.createLongHabit();
view.setHabit(habit);
refreshData(view);
view = new StreakChart(targetContext);
view.setColor(ColorUtils.getAndroidTestColor(habit.getColor()));
view.setStreaks(habit.getStreaks().getBest(5));
measureView(view, dpToPixels(300), dpToPixels(100));
}
@Test
public void testRender() throws Throwable
{
assertRenders(view, "HabitStreakView/render.png");
assertRenders(view, BASE_PATH + "render.png");
}
@Test
public void testRender_withTransparentBackground() throws Throwable
public void testRender_withSmallSize() throws Throwable
{
view.setIsBackgroundTransparent(true);
assertRenders(view, "HabitStreakView/renderTransparent.png");
measureView(view, dpToPixels(100), dpToPixels(100));
assertRenders(view, BASE_PATH + "renderSmallSize.png");
}
@Test
public void testRender_withSmallSize() throws Throwable
public void testRender_withTransparentBackground() throws Throwable
{
measureView(dpToPixels(100), dpToPixels(100), view);
refreshData(view);
assertRenders(view, "HabitStreakView/renderSmallSize.png");
view.setIsBackgroundTransparent(true);
assertRenders(view, BASE_PATH + "renderTransparent.png");
}
}

@ -0,0 +1,188 @@
/*
* 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.habits.list.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.BaseViewTest;
import org.isoron.uhabits.utils.ColorUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class CheckmarkButtonViewTest extends BaseViewTest
{
public static final String PATH = "habits/list/CheckmarkButtonView/";
private CountDownLatch latch;
private CheckmarkButtonView view;
@Override
@Before
public void setUp()
{
super.setUp();
setSimilarityCutoff(0.03f);
latch = new CountDownLatch(1);
view = new CheckmarkButtonView(targetContext);
view.setValue(Checkmark.UNCHECKED);
view.setColor(ColorUtils.getAndroidTestColor(7));
measureView(view, dpToPixels(40), dpToPixels(40));
}
@Test
public void testRender_explicitCheck() throws Exception
{
view.setValue(Checkmark.CHECKED_EXPLICITLY);
assertRendersCheckedExplicitly();
}
@Test
public void testRender_implicitCheck() throws Exception
{
view.setValue(Checkmark.CHECKED_IMPLICITLY);
assertRendersCheckedImplicitly();
}
@Test
public void testRender_unchecked() throws Exception
{
view.setValue(Checkmark.UNCHECKED);
assertRendersUnchecked();
}
protected void assertRendersCheckedExplicitly() throws IOException
{
assertRenders(view, PATH + "render_explicit_check.png");
}
protected void assertRendersCheckedImplicitly() throws IOException
{
assertRenders(view, PATH + "render_implicit_check.png");
}
protected void assertRendersUnchecked() throws IOException
{
assertRenders(view, PATH + "render_unchecked.png");
}
// @Test
// public void testLongClick() throws Exception
// {
// setOnToggleListener();
// view.performLongClick();
// waitForLatch();
// assertRendersCheckedExplicitly();
// }
//
// @Test
// public void testClick_withShortToggle_fromUnchecked() throws Exception
// {
// Preferences.getInstance().setShortToggleEnabled(true);
// view.setValue(Checkmark.UNCHECKED);
// setOnToggleListenerAndPerformClick();
// assertRendersCheckedExplicitly();
// }
//
// @Test
// public void testClick_withShortToggle_fromChecked() throws Exception
// {
// Preferences.getInstance().setShortToggleEnabled(true);
// view.setValue(Checkmark.CHECKED_EXPLICITLY);
// setOnToggleListenerAndPerformClick();
// assertRendersUnchecked();
// }
//
// @Test
// public void testClick_withShortToggle_withoutListener() throws Exception
// {
// Preferences.getInstance().setShortToggleEnabled(true);
// view.setValue(Checkmark.CHECKED_EXPLICITLY);
// view.setController(null);
// view.performClick();
// assertRendersUnchecked();
// }
//
// protected void setOnToggleListenerAndPerformClick() throws InterruptedException
// {
// setOnToggleListener();
// view.performClick();
// waitForLatch();
// }
//
// @Test
// public void testClick_withoutShortToggle() throws Exception
// {
// Preferences.getInstance().setShortToggleEnabled(false);
// setOnInvalidToggleListener();
// view.performClick();
// waitForLatch();
// assertRendersUnchecked();
// }
// protected void setOnInvalidToggleListener()
// {
// view.setController(new CheckmarkButtonView.Controller()
// {
// @Override
// public void onToggleCheckmark(CheckmarkButtonView view, long timestamp)
// {
// fail();
// }
//
// @Override
// public void onInvalidToggle(CheckmarkButtonView v)
// {
// assertThat(v, equalTo(view));
// latch.countDown();
// }
// });
// }
// protected void setOnToggleListener()
// {
// view.setController(new CheckmarkButtonView.Controller()
// {
// @Override
// public void onToggleCheckmark(CheckmarkButtonView v, long t)
// {
// assertThat(v, equalTo(view));
// assertThat(t, equalTo(DateUtils.getStartOfToday()));
// latch.countDown();
// }
//
// @Override
// public void onInvalidToggle(CheckmarkButtonView view)
// {
// fail();
// }
// });
// }
}

@ -0,0 +1,97 @@
/*
* 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.habits.list.views;
import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.BaseViewTest;
import org.isoron.uhabits.utils.ColorUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class CheckmarkPanelViewTest extends BaseViewTest
{
public static final String PATH = "habits/list/CheckmarkPanelView/";
private CountDownLatch latch;
private CheckmarkPanelView view;
private int checkmarks[];
@Override
@Before
public void setUp()
{
super.setUp();
setSimilarityCutoff(0.03f);
prefs.setShouldReverseCheckmarks(false);
Habit habit = new Habit();
latch = new CountDownLatch(1);
checkmarks = new int[]{
Checkmark.CHECKED_EXPLICITLY, Checkmark.UNCHECKED,
Checkmark.CHECKED_IMPLICITLY, Checkmark.CHECKED_EXPLICITLY};
view = new CheckmarkPanelView(targetContext);
view.setHabit(habit);
view.setCheckmarkValues(checkmarks);
view.setColor(ColorUtils.getAndroidTestColor(7));
measureView(view, dpToPixels(200), dpToPixels(200));
}
// protected void waitForLatch() throws InterruptedException
// {
// assertTrue("Latch timeout", latch.await(1, TimeUnit.SECONDS));
// }
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
// @Test
// public void testToggleCheckmark_withLeftToRight() throws Exception
// {
// setToggleListener();
// view.getButton(1).performToggle();
// waitForLatch();
// }
//
// @Test
// public void testToggleCheckmark_withReverseCheckmarks() throws Exception
// {
// prefs.setShouldReverseCheckmarks(true);
// view.setCheckmarkValues(checkmarks); // refresh after preference change
//
// setToggleListener();
// view.getButton(2).performToggle();
// waitForLatch();
// }
}

@ -0,0 +1,79 @@
/*
* 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.habits.list.views;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.ui.habits.list.model.*;
import org.junit.*;
import org.junit.runner.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class HintViewTest extends BaseViewTest
{
public static final String PATH = "habits/list/HintView/";
private HintView view;
private HintList list;
@Before
@Override
public void setUp()
{
super.setUp();
view = new HintView(targetContext);
list = mock(HintList.class);
view.setHints(list);
measureView(view, 400, 200);
String text =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
when(list.shouldShow()).thenReturn(true);
when(list.pop()).thenReturn(text);
view.showNext();
skipAnimation(view);
}
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
@Test
public void testClick() throws Exception
{
assertThat(view.getAlpha(), equalTo(1f));
view.performClick();
skipAnimation(view);
assertThat(view.getAlpha(), equalTo(0f));
}
}

@ -0,0 +1,86 @@
/*
* 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.widgets;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.widgets.*;
import org.junit.*;
import org.junit.runner.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.isoron.uhabits.models.Checkmark.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class CheckmarkWidgetTest extends BaseViewTest
{
private static final String PATH = "widgets/CheckmarkWidgetView/";
private Habit habit;
private CheckmarkList checkmarks;
private FrameLayout view;
@Override
public void setUp()
{
super.setUp();
habit = fixtures.createShortHabit();
checkmarks = habit.getCheckmarks();
CheckmarkWidget widget = new CheckmarkWidget(targetContext, 0, habit);
view = convertToView(widget, 200, 250);
assertThat(checkmarks.getTodayValue(), equalTo(CHECKED_EXPLICITLY));
}
@Test
public void testClick() throws Exception
{
Button button = (Button) view.findViewById(R.id.button);
assertThat(button, is(not(nullValue())));
// A better test would be to capture the intent, but it doesn't seem
// possible to capture intents sent to BroadcastReceivers.
button.performClick();
sleep(1000);
assertThat(checkmarks.getTodayValue(), equalTo(UNCHECKED));
}
@Test
public void testIsInstalled()
{
assertWidgetProviderIsInstalled(CheckmarkWidgetProvider.class);
}
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "checked.png");
}
}

@ -0,0 +1,64 @@
/*
* 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.widgets;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.widgets.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class FrequencyWidgetTest extends BaseViewTest
{
private static final String PATH = "widgets/FrequencyWidget/";
private Habit habit;
private FrameLayout view;
@Override
public void setUp()
{
super.setUp();
setTheme(R.style.TransparentWidgetTheme);
habit = fixtures.createLongHabit();
FrequencyWidget widget = new FrequencyWidget(targetContext, 0, habit);
view = convertToView(widget, 400, 400);
}
@Test
public void testIsInstalled()
{
assertWidgetProviderIsInstalled(FrequencyWidgetProvider.class);
}
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
}

@ -0,0 +1,64 @@
/*
* 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.widgets;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.widgets.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class HistoryWidgetTest extends BaseViewTest
{
private static final String PATH = "widgets/HistoryWidget/";
private Habit habit;
private FrameLayout view;
@Override
public void setUp()
{
super.setUp();
setTheme(R.style.TransparentWidgetTheme);
habit = fixtures.createLongHabit();
HistoryWidget widget = new HistoryWidget(targetContext, 0, habit);
view = convertToView(widget, 400, 400);
}
@Test
public void testIsInstalled()
{
assertWidgetProviderIsInstalled(HistoryWidgetProvider.class);
}
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
}

@ -0,0 +1,64 @@
/*
* 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.widgets;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.widgets.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class ScoreWidgetTest extends BaseViewTest
{
private static final String PATH = "widgets/ScoreWidget/";
private Habit habit;
private FrameLayout view;
@Override
public void setUp()
{
super.setUp();
setTheme(R.style.TransparentWidgetTheme);
habit = fixtures.createLongHabit();
ScoreWidget widget = new ScoreWidget(targetContext, 0, habit);
view = convertToView(widget, 400, 400);
}
@Test
public void testIsInstalled()
{
assertWidgetProviderIsInstalled(ScoreWidgetProvider.class);
}
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
}

@ -0,0 +1,64 @@
/*
* 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.widgets;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import android.widget.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.widgets.*;
import org.junit.*;
import org.junit.runner.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class StreakWidgetTest extends BaseViewTest
{
private static final String PATH = "widgets/StreakWidget/";
private Habit habit;
private FrameLayout view;
@Override
public void setUp()
{
super.setUp();
setTheme(R.style.TransparentWidgetTheme);
habit = fixtures.createLongHabit();
StreakWidget widget = new StreakWidget(targetContext, 0, habit);
view = convertToView(widget, 400, 400);
}
@Test
public void testIsInstalled()
{
assertWidgetProviderIsInstalled(StreakWidgetProvider.class);
}
@Test
public void testRender() throws Exception
{
assertRenders(view, PATH + "render.png");
}
}

@ -0,0 +1,92 @@
/*
* 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.widgets.views;
import android.support.test.runner.*;
import android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.*;
import org.junit.*;
import org.junit.runner.*;
import java.io.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class CheckmarkWidgetViewTest extends BaseViewTest
{
private static final String PATH = "widgets/CheckmarkWidgetView/";
private CheckmarkWidgetView view;
private Habit habit;
@Override
@Before
public void setUp()
{
super.setUp();
InterfaceUtils.setFixedTheme(R.style.TransparentWidgetTheme);
habit = fixtures.createShortHabit();
view = new CheckmarkWidgetView(targetContext);
int color = ColorUtils.getAndroidTestColor(habit.getColor());
int score = habit.getScores().getTodayValue();
float percentage = (float) score / Score.MAX_VALUE;
view.setActiveColor(color);
view.setCheckmarkValue(habit.getCheckmarks().getTodayValue());
view.setPercentage(percentage);
view.setName(habit.getName());
view.refresh();
measureView(view, dpToPixels(100), dpToPixels(200));
}
@Test
public void testRender_checked() throws IOException
{
assertRenders(view, PATH + "checked.png");
}
@Test
public void testRender_implicitlyChecked() throws IOException
{
view.setCheckmarkValue(Checkmark.CHECKED_IMPLICITLY);
view.refresh();
assertRenders(view, PATH + "implicitly_checked.png");
}
@Test
public void testRender_largeSize() throws IOException
{
measureView(view, dpToPixels(300), dpToPixels(300));
assertRenders(view, PATH + "large_size.png");
}
@Test
public void testRender_unchecked() throws IOException
{
view.setCheckmarkValue(Checkmark.UNCHECKED);
view.refresh();
assertRenders(view, PATH + "unchecked.png");
}
}

@ -1,168 +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.uhabits.unit;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.Log;
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 = 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 = 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();
}
}

@ -1,120 +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.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);
}
}

@ -1,175 +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.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);
}
}

@ -1,378 +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.uhabits.unit.models;
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 = 0;
model.freqNum = 10;
model.freqDen = 20;
model.reminderDays = 1;
model.reminderHour = 8;
model.reminderMin = 30;
model.position = 0;
Habit habit = new Habit(model);
assertThat(habit.archived, is(model.archived));
assertThat(habit.highlight, is(model.highlight));
assertThat(habit.color, is(model.color));
assertThat(habit.freqNum, is(model.freqNum));
assertThat(habit.freqDen, is(model.freqDen));
assertThat(habit.reminderDays, is(model.reminderDays));
assertThat(habit.reminderHour, is(model.reminderHour));
assertThat(habit.reminderMin, is(model.reminderMin));
assertThat(habit.position, is(model.position));
}
@Test
public void test_get_withValidId()
{
Habit habit = new Habit();
habit.save();
Habit habit2 = Habit.get(habit.getId());
assertThat(habit, equalTo(habit2));
}
@Test
public void test_get_withInvalidId()
{
Habit habit = Habit.get(123456L);
assertThat(habit, is(nullValue()));
}
@Test
public void test_getAll_withoutArchived()
{
List<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 =
"Position,Name,Description,NumRepetitions,Interval,Color\n" +
"001,Meditate,Did you meditate this morning?,1,1,#AFB42B\n" +
"002,Wake up early,Did you wake up before 6am?,2,3,#00897B\n";
StringWriter writer = new StringWriter();
Habit.writeCSV(Habit.getAll(true), writer);
MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV));
}
}

@ -1,191 +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.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));
}
}

@ -1,176 +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.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);
}
});
}
}

@ -1,77 +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.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();
}
}

@ -1,91 +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.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.DateHelper;
import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.unit.HabitFixtures;
import org.isoron.uhabits.views.CheckmarkWidgetView;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CheckmarkWidgetViewTest extends ViewTest
{
private CheckmarkWidgetView view;
private Habit habit;
@Before
public void setup()
{
super.setup();
UIHelper.setFixedTheme(R.style.TransparentWidgetTheme);
habit = HabitFixtures.createShortHabit();
view = new CheckmarkWidgetView(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");
}
}

@ -1,125 +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.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(400), dpToPixels(200), 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, 340, 40); // today's square
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, 118, 13); // header
tap(view, 336, 60); // tomorrow's square
tap(view, 370, 60); // right axis
waitForAsyncTasks();
int actualCheckmarkValues[] = habit.checkmarks.getAllValues();
assertThat(actualCheckmarkValues, equalTo(expectedCheckmarkValues));
}
@Test
public void tapDate_withReadOnlyView() throws Throwable
{
view.setIsEditable(false);
tap(view, 340, 40); // today's square
waitForAsyncTasks();
long today = DateHelper.getStartOfToday();
assertTrue(habit.repetitions.contains(today));
}
}

@ -1,77 +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.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.CSV_PALETTE[0]);
measureView(dpToPixels(100), dpToPixels(100), view);
}
@Test
public void testRender_base() throws IOException
{
assertRenders(view, "NumberView/render.png");
}
@Test
public void testRender_withLongLabel() throws IOException
{
view.setLabel("The quick brown fox jumps over the lazy fox");
measureView(dpToPixels(100), dpToPixels(100), view);
assertRenders(view, "NumberView/renderLongLabel.png");
}
@Test
public void testRender_withDifferentParams() throws IOException
{
view.setNumber(500);
view.setColor(ColorHelper.CSV_PALETTE[5]);
view.setTextSize(targetContext.getResources().getDimension(R.dimen.tinyTextSize));
measureView(dpToPixels(200), dpToPixels(200), view);
assertRenders(view, "NumberView/renderDifferentParams.png");
}
}

@ -60,28 +60,28 @@
</activity>
<activity
android:name=".ShowHabitActivity"
android:name=".ui.habits.show.ShowHabitActivity"
android:label="@string/title_activity_show_habit">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.isoron.uhabits.MainActivity"/>
android:value=".MainActivity"/>
</activity>
<activity
android:name=".SettingsActivity"
android:name=".ui.settings.SettingsActivity"
android:label="@string/settings">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.isoron.uhabits.MainActivity"/>
android:value=".MainActivity"/>
</activity>
<activity
android:name=".IntroActivity"
android:name=".ui.intro.IntroActivity"
android:label=""
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
<activity
android:name=".widgets.HabitPickerDialog"
android:name=".ui.widgets.HabitPickerDialog"
android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
@ -89,7 +89,7 @@
</activity>
<activity
android:name=".AboutActivity"
android:name=".ui.about.AboutActivity"
android:label="@string/about">
<meta-data
android:name="android.support.PARENT_ACTIVITY"

@ -0,0 +1,34 @@
/*
* 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 javax.inject.Singleton;
import dagger.Component;
/**
* Dependency injection component for classes that are specific to Android.
*/
@Singleton
@Component(modules = {AndroidModule.class})
public interface AndroidComponent extends BaseComponent
{
}

@ -0,0 +1,73 @@
/*
* 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 org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.sqlite.*;
import org.isoron.uhabits.utils.*;
import javax.inject.*;
import dagger.*;
/**
* Module that provides dependencies when the application is running on
* Android.
* <p>
* This module is also used for instrumented tests.
*/
@Module
public class AndroidModule
{
@Provides
@Singleton
CommandRunner provideCommandRunner()
{
return new CommandRunner();
}
@Provides
@Singleton
HabitList provideHabitList()
{
return SQLiteHabitList.getInstance();
}
@Provides
ModelFactory provideModelFactory()
{
return new SQLModelFactory();
}
@Provides
@Singleton
Preferences providePreferences()
{
return new Preferences();
}
@Provides
@Singleton
WidgetPreferences provideWidgetPreferences()
{
return new WidgetPreferences();
}
}

@ -1,204 +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.uhabits;
import android.app.backup.BackupManager;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.widget.Toast;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.UIHelper;
import java.util.LinkedList;
abstract public class BaseActivity extends AppCompatActivity implements Thread.UncaughtExceptionHandler
{
private static int MAX_UNDO_LEVEL = 15;
private LinkedList<Command> undoList;
private LinkedList<Command> redoList;
private Toast toast;
Thread.UncaughtExceptionHandler androidExceptionHandler;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
UIHelper.applyCurrentTheme(this);
androidExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
undoList = new LinkedList<>();
redoList = new LinkedList<>();
}
public void executeCommand(Command command, Long refreshKey)
{
executeCommand(command, false, refreshKey);
}
protected void undo()
{
if (undoList.isEmpty())
{
showToast(R.string.toast_nothing_to_undo);
return;
}
Command last = undoList.pop();
redoList.push(last);
last.undo();
showToast(last.getUndoStringId());
}
protected void redo()
{
if (redoList.isEmpty())
{
showToast(R.string.toast_nothing_to_redo);
return;
}
Command last = redoList.pop();
executeCommand(last, false, null);
}
public void showToast(Integer stringId)
{
if (stringId == null) return;
if (toast == null) toast = Toast.makeText(this, stringId, Toast.LENGTH_SHORT);
else toast.setText(stringId);
toast.show();
}
public void executeCommand(final Command command, Boolean clearRedoStack, final Long refreshKey)
{
undoList.push(command);
if (undoList.size() > MAX_UNDO_LEVEL) undoList.removeLast();
if (clearRedoStack) redoList.clear();
new AsyncTask<Void, Void, Void>()
{
@Override
protected Void doInBackground(Void... params)
{
command.execute();
return null;
}
@Override
protected void onPostExecute(Void aVoid)
{
BaseActivity.this.onPostExecuteCommand(refreshKey);
BackupManager.dataChanged("org.isoron.uhabits");
}
}.execute();
showToast(command.getExecuteStringId());
}
protected void setupSupportActionBar(boolean homeButtonEnabled)
{
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
if(toolbar == null) return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
toolbar.setElevation(UIHelper.dpToPixels(this, 2));
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if(actionBar == null) return;
if(homeButtonEnabled)
actionBar.setDisplayHomeAsUpEnabled(true);
}
public void onPostExecuteCommand(Long refreshKey)
{
}
@Override
public void uncaughtException(Thread thread, Throwable ex)
{
try
{
ex.printStackTrace();
HabitsApplication.dumpBugReportToFile();
}
catch(Exception e)
{
// ignored
}
if(androidExceptionHandler != null)
androidExceptionHandler.uncaughtException(thread, ex);
else
System.exit(1);
}
protected void setupActionBarColor(int color)
{
ActionBar actionBar = getSupportActionBar();
if(actionBar == null) return;
if (!UIHelper.getStyledBoolean(this, R.attr.useHabitColorAsPrimary)) return;
ColorDrawable drawable = new ColorDrawable(color);
actionBar.setBackgroundDrawable(drawable);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
{
int darkerColor = ColorHelper.mixColors(color, Color.BLACK, 0.75f);
getWindow().setStatusBarColor(darkerColor);
}
}
@Override
protected void onPostResume()
{
super.onPostResume();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
hideFakeToolbarShadow();
}
protected void hideFakeToolbarShadow()
{
View view = findViewById(R.id.toolbarShadow);
if(view != null) view.setVisibility(View.GONE);
view = findViewById(R.id.headerShadow);
if(view != null) view.setVisibility(View.GONE);
}
}

@ -0,0 +1,98 @@
/*
* 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 org.isoron.uhabits.commands.*;
import org.isoron.uhabits.io.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.ui.*;
import org.isoron.uhabits.ui.habits.edit.*;
import org.isoron.uhabits.ui.habits.list.*;
import org.isoron.uhabits.ui.habits.list.controllers.*;
import org.isoron.uhabits.ui.habits.list.model.*;
import org.isoron.uhabits.ui.habits.list.views.*;
import org.isoron.uhabits.ui.habits.show.*;
import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.widgets.*;
/**
* Base component for dependency injection.
*/
public interface BaseComponent
{
void inject(CheckmarkButtonController checkmarkButtonController);
void inject(ListHabitsController listHabitsController);
void inject(CheckmarkPanelView checkmarkPanelView);
void inject(ToggleRepetitionTask toggleRepetitionTask);
void inject(HabitCardListCache habitCardListCache);
void inject(HabitBroadcastReceiver habitBroadcastReceiver);
void inject(ListHabitsSelectionMenu listHabitsSelectionMenu);
void inject(HintList hintList);
void inject(HabitCardListAdapter habitCardListAdapter);
void inject(ArchiveHabitsCommand archiveHabitsCommand);
void inject(ChangeHabitColorCommand changeHabitColorCommand);
void inject(UnarchiveHabitsCommand unarchiveHabitsCommand);
void inject(EditHabitCommand editHabitCommand);
void inject(CreateHabitCommand createHabitCommand);
void inject(HabitPickerDialog habitPickerDialog);
void inject(BaseWidgetProvider baseWidgetProvider);
void inject(ShowHabitActivity showHabitActivity);
void inject(DeleteHabitsCommand deleteHabitsCommand);
void inject(ListHabitsActivity listHabitsActivity);
void inject(BaseSystem baseSystem);
void inject(HistoryEditorDialog historyEditorDialog);
void inject(HabitsApplication application);
void inject(Habit habit);
void inject(AbstractImporter abstractImporter);
void inject(HabitsCSVExporter habitsCSVExporter);
void inject(BaseDialogFragment baseDialogFragment);
void inject(ShowHabitController showHabitController);
void inject(BaseWidget baseWidget);
void inject(WidgetUpdater widgetManager);
}

@ -19,37 +19,62 @@
package org.isoron.uhabits;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.LocalBroadcastManager;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import java.util.Date;
import android.app.*;
import android.content.*;
import android.graphics.*;
import android.net.*;
import android.os.*;
import android.preference.*;
import android.support.v4.app.*;
import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*;
import java.util.*;
import javax.inject.*;
/**
* The Android BroadcastReceiver for Loop Habit Tracker.
* <p/>
* All broadcast messages are received and processed by this class.
*/
public class HabitBroadcastReceiver extends BroadcastReceiver
{
public static final String ACTION_CHECK = "org.isoron.uhabits.ACTION_CHECK";
public static final String ACTION_DISMISS = "org.isoron.uhabits.ACTION_DISMISS";
public static final String ACTION_SHOW_REMINDER = "org.isoron.uhabits.ACTION_SHOW_REMINDER";
public static final String ACTION_SNOOZE = "org.isoron.uhabits.ACTION_SNOOZE";
public static final String ACTION_DISMISS =
"org.isoron.uhabits.ACTION_DISMISS";
public static final String ACTION_SHOW_REMINDER =
"org.isoron.uhabits.ACTION_SHOW_REMINDER";
public static final String ACTION_SNOOZE =
"org.isoron.uhabits.ACTION_SNOOZE";
@Inject
HabitList habitList;
@Inject
CommandRunner commandRunner;
public HabitBroadcastReceiver()
{
super();
HabitsApplication.getComponent().inject(this);
}
public static void dismissNotification(Context context, Habit habit)
{
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
}
@Override
public void onReceive(final Context context, Intent intent)
@ -74,81 +99,52 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
break;
case Intent.ACTION_BOOT_COMPLETED:
ReminderHelper.createReminderAlarms(context);
ReminderUtils.createReminderAlarms(context, habitList);
break;
}
}
private void createReminderAlarmsDelayed(final Context context)
{
new Handler().postDelayed(new Runnable()
{
@Override
public void run()
{
ReminderHelper.createReminderAlarms(context);
}
}, 5000);
}
private void snoozeHabit(Context context, Intent intent)
{
Uri data = intent.getData();
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
long delayMinutes = Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
long habitId = ContentUris.parseId(data);
Habit habit = Habit.get(habitId);
if(habit != null)
ReminderHelper.createReminderAlarm(context, habit,
new Date().getTime() + delayMinutes * 60 * 1000);
dismissNotification(context, habitId);
}
private void checkHabit(Context context, Intent intent)
{
Uri data = intent.getData();
Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());
long today = DateUtils.getStartOfToday();
Long timestamp = intent.getLongExtra("timestamp", today);
long habitId = ContentUris.parseId(data);
Habit habit = Habit.get(habitId);
if(habit != null)
habit.repetitions.toggle(timestamp);
dismissNotification(context, habitId);
sendRefreshBroadcast(context);
}
public static void sendRefreshBroadcast(Context context)
{
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(context);
Intent refreshIntent = new Intent(MainActivity.ACTION_REFRESH);
manager.sendBroadcast(refreshIntent);
Habit habit = habitList.getById(habitId);
if (habit != null)
{
ToggleRepetitionCommand command =
new ToggleRepetitionCommand(habit, timestamp);
commandRunner.execute(command, habitId);
}
MainActivity.updateWidgets(context);
dismissNotification(context, habitId);
}
private void dismissAllHabits()
private boolean checkWeekday(Intent intent, Habit habit)
{
if (!habit.hasReminder()) return false;
Reminder reminder = habit.getReminder();
}
Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
private void dismissNotification(Context context, Long habitId)
{
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE);
boolean reminderDays[] =
DateUtils.unpackWeekdayList(reminder.getDays());
int weekday = DateUtils.getWeekday(timestamp);
int notificationId = (int) (habitId % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
return reminderDays[weekday];
}
private void createNotification(final Context context, final Intent intent)
{
final Uri data = intent.getData();
final Habit habit = Habit.get(ContentUris.parseId(data));
final Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());
final Long reminderTime = intent.getLongExtra("reminderTime", DateHelper.getStartOfToday());
final Habit habit = habitList.getById(ContentUris.parseId(data));
final Long timestamp =
intent.getLongExtra("timestamp", DateUtils.getStartOfToday());
final Long reminderTime =
intent.getLongExtra("reminderTime", DateUtils.getStartOfToday());
if (habit == null) return;
@ -159,7 +155,7 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
@Override
protected void doInBackground()
{
todayValue = habit.checkmarks.getTodayValue();
todayValue = habit.getCheckmarks().getTodayValue();
}
@Override
@ -173,42 +169,48 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
contentIntent.setData(data);
PendingIntent contentPendingIntent =
PendingIntent.getActivity(context, 0, contentIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent.FLAG_CANCEL_CURRENT);
PendingIntent dismissPendingIntent = buildDismissIntent(context);
PendingIntent checkIntentPending = buildCheckIntent(context,
habit, timestamp, 1);
PendingIntent snoozeIntentPending = buildSnoozeIntent(context, habit);
PendingIntent dismissPendingIntent;
dismissPendingIntent =
HabitPendingIntents.dismissNotification(context);
PendingIntent checkIntentPending =
HabitPendingIntents.toggleCheckmark(context, habit,
timestamp, 1);
PendingIntent snoozeIntentPending =
HabitPendingIntents.snoozeNotification(context, habit);
Uri ringtoneUri = ReminderHelper.getRingtoneUri(context);
Uri ringtoneUri = ReminderUtils.getRingtoneUri(context);
NotificationCompat.WearableExtender wearableExtender =
new NotificationCompat.WearableExtender().setBackground(
BitmapFactory.decodeResource(context.getResources(),
R.drawable.stripe));
new NotificationCompat.WearableExtender().setBackground(
BitmapFactory.decodeResource(context.getResources(),
R.drawable.stripe));
Notification notification =
new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.name)
.setContentText(habit.description)
.setContentIntent(contentPendingIntent)
.setDeleteIntent(dismissPendingIntent)
.addAction(R.drawable.ic_action_check,
context.getString(R.string.check), checkIntentPending)
.addAction(R.drawable.ic_action_snooze,
context.getString(R.string.snooze), snoozeIntentPending)
.setSound(ringtoneUri)
.extend(wearableExtender)
.setWhen(reminderTime)
.setShowWhen(true)
.build();
new NotificationCompat.Builder(context)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(habit.getName())
.setContentText(habit.getDescription())
.setContentIntent(contentPendingIntent)
.setDeleteIntent(dismissPendingIntent)
.addAction(R.drawable.ic_action_check,
context.getString(R.string.check),
checkIntentPending)
.addAction(R.drawable.ic_action_snooze,
context.getString(R.string.snooze),
snoozeIntentPending)
.setSound(ringtoneUri)
.extend(wearableExtender)
.setWhen(reminderTime)
.setShowWhen(true)
.build();
notification.flags |= Notification.FLAG_AUTO_CANCEL;
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
notificationManager.notify(notificationId, notification);
@ -218,63 +220,39 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
}.execute();
}
public static PendingIntent buildSnoozeIntent(Context context, Habit habit)
{
Uri data = habit.getUri();
Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class);
snoozeIntent.setData(data);
snoozeIntent.setAction(ACTION_SNOOZE);
return PendingIntent.getBroadcast(context, 0, snoozeIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
public static PendingIntent buildCheckIntent(Context context, Habit
habit, Long timestamp, int requestCode)
{
Uri data = habit.getUri();
Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class);
checkIntent.setData(data);
checkIntent.setAction(ACTION_CHECK);
if(timestamp != null) checkIntent.putExtra("timestamp", timestamp);
return PendingIntent.getBroadcast(context, requestCode, checkIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
public static PendingIntent buildDismissIntent(Context context)
private void createReminderAlarmsDelayed(final Context context)
{
Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class);
deleteIntent.setAction(ACTION_DISMISS);
return PendingIntent.getBroadcast(context, 0, deleteIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
new Handler().postDelayed(
() -> ReminderUtils.createReminderAlarms(context, habitList), 5000);
}
public static PendingIntent buildViewHabitIntent(Context context, Habit habit)
private void dismissAllHabits()
{
Intent intent = new Intent(context, ShowHabitActivity.class);
intent.setData(Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId()));
return TaskStackBuilder.create(context.getApplicationContext())
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
private boolean checkWeekday(Intent intent, Habit habit)
private void dismissNotification(Context context, Long habitId)
{
Long timestamp = intent.getLongExtra("timestamp", DateHelper.getStartOfToday());
boolean reminderDays[] = DateHelper.unpackWeekdayList(habit.reminderDays);
int weekday = DateHelper.getWeekday(timestamp);
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
return reminderDays[weekday];
int notificationId = (int) (habitId % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
}
public static void dismissNotification(Context context, Habit habit)
private void snoozeHabit(Context context, Intent intent)
{
NotificationManager notificationManager =
(NotificationManager) context.getSystemService(
Activity.NOTIFICATION_SERVICE);
Uri data = intent.getData();
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(context);
long delayMinutes =
Long.parseLong(prefs.getString("pref_snooze_interval", "15"));
int notificationId = (int) (habit.getId() % Integer.MAX_VALUE);
notificationManager.cancel(notificationId);
long habitId = ContentUris.parseId(data);
Habit habit = habitList.getById(habitId);
if (habit != null) ReminderUtils.createReminderAlarm(context, habit,
new Date().getTime() + delayMinutes * 60 * 1000);
dismissNotification(context, habitId);
}
}

@ -0,0 +1,76 @@
/*
* 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.*;
import android.content.*;
import android.net.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.ui.habits.show.*;
public abstract class HabitPendingIntents
{
private static final String BASE_URL =
"content://org.isoron.uhabits/habit/";
public static PendingIntent dismissNotification(Context context)
{
Intent deleteIntent = new Intent(context, HabitBroadcastReceiver.class);
deleteIntent.setAction(HabitBroadcastReceiver.ACTION_DISMISS);
return PendingIntent.getBroadcast(context, 0, deleteIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
public static PendingIntent snoozeNotification(Context context, Habit habit)
{
Uri data = habit.getUri();
Intent snoozeIntent = new Intent(context, HabitBroadcastReceiver.class);
snoozeIntent.setData(data);
snoozeIntent.setAction(HabitBroadcastReceiver.ACTION_SNOOZE);
return PendingIntent.getBroadcast(context, 0, snoozeIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
public static PendingIntent toggleCheckmark(Context context,
Habit habit,
Long timestamp,
int requestCode)
{
Uri data = habit.getUri();
Intent checkIntent = new Intent(context, HabitBroadcastReceiver.class);
checkIntent.setData(data);
checkIntent.setAction(HabitBroadcastReceiver.ACTION_CHECK);
if (timestamp != null) checkIntent.putExtra("timestamp", timestamp);
return PendingIntent.getBroadcast(context, requestCode, checkIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
public static PendingIntent viewHabit(Context context, Habit habit)
{
Intent intent = new Intent(context, ShowHabitActivity.class);
intent.setData(Uri.parse(BASE_URL + habit.getId()));
return android.support.v4.app.TaskStackBuilder
.create(context.getApplicationContext())
.addNextIntentWithParentStack(intent)
.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
}

@ -19,36 +19,78 @@
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 android.app.*;
import android.content.*;
import android.support.annotation.*;
import com.activeandroid.ActiveAndroid;
import com.activeandroid.*;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.utils.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.io.*;
/**
* The Android application for Loop Habit Tracker.
*/
public class HabitsApplication extends Application
{
public static final int RESULT_BUG_REPORT = 4;
public static final int RESULT_EXPORT_CSV = 2;
public static final int RESULT_EXPORT_DB = 3;
public static final int RESULT_IMPORT_DATA = 1;
@Nullable
private static HabitsApplication application;
private static BaseComponent component;
@Nullable
private static Context context;
private static WidgetUpdater widgetManager;
public static BaseComponent getComponent()
{
return component;
}
public static void setComponent(BaseComponent component)
{
HabitsApplication.component = component;
}
@Nullable
public static Context getContext()
{
return context;
}
@Nullable
public static HabitsApplication getInstance()
{
return application;
}
@NonNull
public static WidgetUpdater getWidgetManager()
{
if (widgetManager == null)
throw new RuntimeException("widgetManager is null");
return widgetManager;
}
public static boolean isTestMode()
{
try
{
if(context != null)
context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest");
if (context != null) context
.getClassLoader()
.loadClass("org.isoron.uhabits.BaseAndroidTest");
return true;
}
catch (final Exception e)
@ -57,25 +99,25 @@ public class HabitsApplication extends Application
}
}
@Nullable
public static Context getContext()
{
return context;
}
@Override
public void onCreate()
{
super.onCreate();
HabitsApplication.context = this;
HabitsApplication.application = this;
component = DaggerAndroidComponent.builder().build();
component.inject(this);
if (isTestMode())
{
File db = DatabaseHelper.getDatabaseFile();
if(db.exists()) db.delete();
File db = DatabaseUtils.getDatabaseFile();
if (db.exists()) db.delete();
}
DatabaseHelper.initializeActiveAndroid();
widgetManager = new WidgetUpdater(this);
widgetManager.startListening();
DatabaseUtils.initializeActiveAndroid();
}
@Override
@ -83,84 +125,7 @@ public class HabitsApplication extends Application
{
HabitsApplication.context = null;
ActiveAndroid.dispose();
widgetManager.stopListening();
super.onTerminate();
}
public static String getLogcat() throws IOException
{
int maxNLines = 250;
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);
LinkedList<String> log = new LinkedList<>();
String line;
while ((line = bufferedReader.readLine()) != null)
{
log.addLast(line);
if(log.size() > maxNLines) log.removeFirst();
}
for(String l : log)
{
builder.append(l);
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 dumpBugReportToFile() throws IOException
{
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(generateBugReport());
output.close();
return logFile;
}
@NonNull
public static String generateBugReport() throws IOException
{
String logcat = getLogcat();
String deviceInfo = getDeviceInfo();
return deviceInfo + "\n" + logcat;
}
}

@ -23,12 +23,17 @@ import android.app.backup.BackupAgentHelper;
import android.app.backup.FileBackupHelper;
import android.app.backup.SharedPreferencesBackupHelper;
/**
* An Android BackupAgentHelper customized for this application.
*/
public class HabitsBackupAgent extends BackupAgentHelper
{
@Override
public void onCreate()
{
addHelper("preferences", new SharedPreferencesBackupHelper(this, "preferences"));
addHelper("database", new FileBackupHelper(this, "../databases/uhabits.db"));
addHelper("preferences",
new SharedPreferencesBackupHelper(this, "preferences"));
addHelper("database",
new FileBackupHelper(this, "../databases/uhabits.db"));
}
}

@ -19,328 +19,16 @@
package org.isoron.uhabits;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.ActionBar;
import android.view.Menu;
import android.view.MenuItem;
import org.isoron.uhabits.ui.habits.list.ListHabitsActivity;
import org.isoron.uhabits.fragments.ListHabitsFragment;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.helpers.UIHelper;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.widgets.CheckmarkWidgetProvider;
import org.isoron.uhabits.widgets.FrequencyWidgetProvider;
import org.isoron.uhabits.widgets.HistoryWidgetProvider;
import org.isoron.uhabits.widgets.ScoreWidgetProvider;
import org.isoron.uhabits.widgets.StreakWidgetProvider;
import java.io.IOException;
public class MainActivity extends BaseActivity
implements ListHabitsFragment.OnHabitClickListener
/**
* Application that starts upon clicking the launcher icon.
*/
public class MainActivity extends ListHabitsActivity
{
private ListHabitsFragment listHabitsFragment;
private SharedPreferences prefs;
private BroadcastReceiver receiver;
private LocalBroadcastManager localBroadcastManager;
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
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.list_habits_activity);
setupSupportActionBar(false);
prefs = PreferenceManager.getDefaultSharedPreferences(this);
listHabitsFragment =
(ListHabitsFragment) getSupportFragmentManager().findFragmentById(R.id.fragment1);
receiver = new Receiver();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
localBroadcastManager.registerReceiver(receiver, new IntentFilter(ACTION_REFRESH));
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
onPreLollipopStartup();
onStartup();
}
private void onPreLollipopStartup()
{
ActionBar actionBar = getSupportActionBar();
if(actionBar == null) return;
if(UIHelper.isNightMode()) return;
int color = getResources().getColor(R.color.grey_900);
actionBar.setBackgroundDrawable(new ColorDrawable(color));
}
private void onStartup()
{
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
UIHelper.incrementLaunchCount(this);
UIHelper.updateLastAppVersion(this);
showTutorial();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params)
{
ReminderHelper.createReminderAlarms(MainActivity.this);
updateWidgets(MainActivity.this);
return null;
}
}.execute();
}
private void showTutorial()
{
Boolean firstRun = prefs.getBoolean("pref_first_run", true);
if (firstRun)
{
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("pref_first_run", false);
editor.putLong("last_hint_timestamp", DateHelper.getStartOfToday()).apply();
editor.apply();
Intent intent = new Intent(this, IntroActivity.class);
this.startActivity(intent);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
menu.clear();
getMenuInflater().inflate(R.menu.list_habits_menu, menu);
MenuItem nightModeItem = menu.findItem(R.id.action_night_mode);
nightModeItem.setChecked(UIHelper.isNightMode());
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
switch (item.getItemId())
{
case R.id.action_night_mode:
{
if(UIHelper.isNightMode())
UIHelper.setCurrentTheme(UIHelper.THEME_LIGHT);
else
UIHelper.setCurrentTheme(UIHelper.THEME_DARK);
refreshTheme();
return true;
}
case R.id.action_settings:
{
Intent intent = new Intent(this, SettingsActivity.class);
startActivityForResult(intent, 0);
return true;
}
case R.id.action_about:
{
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
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:
return super.onOptionsItemSelected(item);
}
}
private void refreshTheme()
{
new Handler().postDelayed(new Runnable()
{
@Override
public void run()
{
Intent intent = new Intent(MainActivity.this, MainActivity.class);
MainActivity.this.finish();
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
startActivity(intent);
}
}, 500); // Let the menu disappear first
}
@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
{
HabitsApplication.dumpBugReportToFile();
}
catch (IOException e)
{
// ignored
}
try
{
String log = "---------- BUG REPORT BEGINS ----------\n";
log += HabitsApplication.generateBugReport();
log += "---------- BUG REPORT ENDS ------------\n";
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("message/rfc822");
intent.putExtra(Intent.EXTRA_EMAIL, new String[] { "dev@loophabits.org" });
intent.putExtra(Intent.EXTRA_SUBJECT, "Bug Report - Loop Habit Tracker");
intent.putExtra(Intent.EXTRA_TEXT, log);
startActivity(intent);
}
catch (IOException e)
{
e.printStackTrace();
showToast(R.string.bug_report_failed);
}
}
@Override
public void onHabitClicked(Habit habit)
{
Intent intent = new Intent(this, ShowHabitActivity.class);
intent.setData(Uri.parse("content://org.isoron.uhabits/habit/" + habit.getId()));
startActivity(intent);
}
@Override
public void onPostExecuteCommand(Long refreshKey)
{
listHabitsFragment.onPostExecuteCommand(refreshKey);
new BaseTask()
{
@Override
protected void doInBackground()
{
dismissNotifications(MainActivity.this);
updateWidgets(MainActivity.this);
}
}.execute();
}
private void dismissNotifications(Context context)
{
for(Habit h : Habit.getHabitsWithReminder())
{
if(h.checkmarks.getTodayValue() != Checkmark.UNCHECKED)
HabitBroadcastReceiver.dismissNotification(context, h);
}
}
public static void updateWidgets(Context context)
{
updateWidgets(context, CheckmarkWidgetProvider.class);
updateWidgets(context, HistoryWidgetProvider.class);
updateWidgets(context, ScoreWidgetProvider.class);
updateWidgets(context, StreakWidgetProvider.class);
updateWidgets(context, FrequencyWidgetProvider.class);
}
private static void updateWidgets(Context context, Class providerClass)
{
ComponentName provider = new ComponentName(context, providerClass);
Intent intent = new Intent(context, providerClass);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
int ids[] = AppWidgetManager.getInstance(context).getAppWidgetIds(provider);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
context.sendBroadcast(intent);
}
@Override
protected void onDestroy()
{
localBroadcastManager.unregisterReceiver(receiver);
super.onDestroy();
}
class Receiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
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();
}
/*
* Since changing the main activity on the manifest file causes things to
* break we always point the launcher icon to this activity instead, and
* redirect to the desired one using inheritance.
*/
}

@ -1,64 +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.uhabits;
import android.content.ContentUris;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.models.Habit;
public class ShowHabitActivity extends BaseActivity
{
private Habit habit;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Uri data = getIntent().getData();
habit = Habit.get(ContentUris.parseId(data));
setContentView(R.layout.show_habit_activity);
setupSupportActionBar(true);
setupHabitActionBar();
}
private void setupHabitActionBar()
{
if(habit == null) return;
ActionBar actionBar = getSupportActionBar();
if(actionBar == null) return;
actionBar.setTitle(habit.name);
setupActionBarColor(ColorHelper.getColor(this, habit.color));
}
public Habit getHabit()
{
return habit;
}
}

@ -19,38 +19,52 @@
package org.isoron.uhabits.commands;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList;
import java.util.List;
import javax.inject.Inject;
/**
* Command to archive a list of habits.
*/
public class ArchiveHabitsCommand extends Command
{
@Inject
HabitList habitList;
private List<Habit> habits;
public ArchiveHabitsCommand(List<Habit> habits)
{
HabitsApplication.getComponent().inject(this);
this.habits = habits;
}
@Override
public void execute()
{
Habit.archive(habits);
for(Habit h : habits) h.setArchived(true);
habitList.update(habits);
}
@Override
public void undo()
{
Habit.unarchive(habits);
for(Habit h : habits) h.setArchived(false);
habitList.update(habits);
}
@Override
public Integer getExecuteStringId()
{
return R.string.toast_habit_archived;
}
@Override
public Integer getUndoStringId()
{
return R.string.toast_habit_unarchived;

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

Loading…
Cancel
Save