Merge branch 'feature/import-data' into dev

pull/77/merge
Alinson S. Xavier 10 years ago
commit 5115379fdd

@ -35,6 +35,7 @@ dependencies {
compile 'com.android.support:support-v4:23.1.1' compile 'com.android.support:support-v4:23.1.1'
compile 'com.github.paolorotolo:appintro:3.4.0' compile 'com.github.paolorotolo:appintro:3.4.0'
compile 'org.apmem.tools:layouts:1.10@aar' compile 'org.apmem.tools:layouts:1.10@aar'
compile 'com.opencsv:opencsv:3.7'
compile project(':libs:drag-sort-listview:library') compile project(':libs:drag-sort-listview:library')
compile files('libs/ActiveAndroid.jar') compile files('libs/ActiveAndroid.jar')

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

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

@ -1,6 +1,28 @@
/*
* 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; package org.isoron.uhabits.ui;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.NoMatchingViewException; import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.intent.rule.IntentsTestRule; import android.support.test.espresso.intent.rule.IntentsTestRule;
@ -9,6 +31,7 @@ import android.test.suitebuilder.annotation.LargeTest;
import org.isoron.uhabits.MainActivity; import org.isoron.uhabits.MainActivity;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
@ -31,15 +54,22 @@ 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.swipeRight;
import static android.support.test.espresso.action.ViewActions.swipeUp; import static android.support.test.espresso.action.ViewActions.swipeUp;
import static android.support.test.espresso.assertion.ViewAssertions.matches; 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.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isRoot; 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.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText; import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.isoron.uhabits.ui.HabitMatchers.isPreferenceWithText;
import static org.isoron.uhabits.ui.HabitMatchers.withName; import static org.isoron.uhabits.ui.HabitMatchers.withName;
import static org.isoron.uhabits.ui.HabitViewActions.clickAtRandomLocations; import static org.isoron.uhabits.ui.HabitViewActions.clickAtRandomLocations;
import static org.isoron.uhabits.ui.HabitViewActions.toggleAllCheckmarks; import static org.isoron.uhabits.ui.HabitViewActions.toggleAllCheckmarks;
@ -65,6 +95,8 @@ public class MainTest
public IntentsTestRule<MainActivity> activityRule = new IntentsTestRule<>( public IntentsTestRule<MainActivity> activityRule = new IntentsTestRule<>(
MainActivity.class); MainActivity.class);
private Context targetContext;
@Before @Before
public void setup() public void setup()
{ {
@ -74,6 +106,14 @@ public class MainTest
sys.acquireWakeLock(); sys.acquireWakeLock();
sys.unlockScreen(); 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_VIEW))).respondWith(okResult);
skipTutorial(); skipTutorial();
} }
@ -97,11 +137,14 @@ public class MainTest
} }
} }
/**
* 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 @Test
public void testArchiveHabits() public void testArchiveHabits()
{ {
List<String> names = new LinkedList<>(); List<String> names = new LinkedList<>();
Context context = InstrumentationRegistry.getTargetContext();
for(int i = 0; i < 3; i++) for(int i = 0; i < 3; i++)
names.add(addHabit()); names.add(addHabit());
@ -111,7 +154,7 @@ public class MainTest
clickActionModeMenuItem(R.string.archive); clickActionModeMenuItem(R.string.archive);
assertHabitsDontExist(names); assertHabitsDontExist(names);
openActionBarOverflowOrOptionsMenu(context); openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.show_archived)) onView(withText(R.string.show_archived))
.perform(click()); .perform(click());
@ -119,7 +162,7 @@ public class MainTest
selectHabits(names); selectHabits(names);
clickActionModeMenuItem(R.string.unarchive); clickActionModeMenuItem(R.string.unarchive);
openActionBarOverflowOrOptionsMenu(context); openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.show_archived)) onView(withText(R.string.show_archived))
.perform(click()); .perform(click());
@ -127,6 +170,10 @@ public class MainTest
deleteHabits(names); deleteHabits(names);
} }
/**
* User opens the app, clicks the add button, types some bogus information, tries to save,
* dialog displays an error.
*/
@Test @Test
public void testAddInvalidHabit() public void testAddInvalidHabit()
{ {
@ -139,6 +186,10 @@ public class MainTest
onView(withId(R.id.input_name)).check(matches(isDisplayed())); 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 @Test
public void testAddHabitAndViewStats() throws InterruptedException public void testAddHabitAndViewStats() throws InterruptedException
{ {
@ -161,6 +212,11 @@ public class MainTest
.perform(scrollTo(), swipeRight()); .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 @Test
public void testEditHabit() public void testEditHabit()
{ {
@ -187,6 +243,10 @@ public class MainTest
deleteHabit(modifiedName); 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 @Test
public void testEditHistory() public void testEditHistory()
{ {
@ -205,20 +265,82 @@ public class MainTest
.perform(scrollTo(), swipeRight(), swipeLeft()); .perform(scrollTo(), swipeRight(), swipeLeft());
} }
/**
* User opens menu, clicks settings, sees settings screen.
*/
@Test @Test
public void testSettings() public void testSettings()
{ {
Context context = InstrumentationRegistry.getContext(); openActionBarOverflowOrOptionsMenu(targetContext);
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.settings)).perform(click()); onView(withText(R.string.settings)).perform(click());
} }
/**
* User opens menu, clicks about, sees about screen.
*/
@Test @Test
public void testAbout() public void testAbout()
{ {
Context context = InstrumentationRegistry.getContext(); openActionBarOverflowOrOptionsMenu(targetContext);
openActionBarOverflowOrOptionsMenu(context);
onView(withText(R.string.about)).perform(click()); onView(withText(R.string.about)).perform(click());
onView(isRoot()).perform(swipeUp()); onView(isRoot()).perform(swipeUp());
} }
/**
* User opens menu, clicks Help, sees website.
*/
@Test
public void testHelp()
{
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.help)).perform(click());
intended(hasAction(Intent.ACTION_VIEW));
}
/**
* User creates a habit, exports full backup, deletes the habit, restores backup, sees that the
* previously created habit has appeared back.
*/
@Test
public void testExportImportDB()
{
String name = addHabit();
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.settings)).perform(click());
String date = DateHelper.getBackupDateFormat().format(DateHelper.getLocalTime());
date = date.substring(0, date.length() - 2);
onData(isPreferenceWithText("Export full backup")).perform(click());
intended(hasAction(Intent.ACTION_SEND));
deleteHabit(name);
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.settings)).perform(click());
onData(isPreferenceWithText("Import data")).perform(click());
onData(allOf(is(instanceOf(String.class)), startsWith("Backups")))
.perform(click());
onData(allOf(is(instanceOf(String.class)), containsString(date)))
.perform(click());
selectHabit(name);
}
/**
* User creates a habit, opens settings, clicks export as CSV, is asked what activity should
* handle the file.
*/
@Test
public void testExportCSV()
{
addHabit();
openActionBarOverflowOrOptionsMenu(targetContext);
onView(withText(R.string.settings)).perform(click());
onData(isPreferenceWithText("Export as CSV")).perform(click());
intended(hasAction(Intent.ACTION_SEND));
}
} }

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

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

@ -22,13 +22,16 @@ package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.StringWriter;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY; import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY;
@ -140,6 +143,27 @@ public class CheckmarkListTest
assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(UNCHECKED)); assertThat(nonDailyHabit.checkmarks.getTodayValue(), equalTo(UNCHECKED));
} }
@Test
public void 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) private void travelInTime(int days)
{ {
DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME + DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME +

@ -19,7 +19,8 @@
package org.isoron.uhabits.unit.models; package org.isoron.uhabits.unit.models;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
public class HabitFixtures public class HabitFixtures
@ -28,9 +29,11 @@ public class HabitFixtures
public static boolean NON_DAILY_HABIT_CHECKS[] = { true, false, false, true, true, true, false, public static boolean NON_DAILY_HABIT_CHECKS[] = { true, false, false, true, true, true, false,
false, true, true }; false, true, true };
static Habit createNonDailyHabit() public static Habit createNonDailyHabit()
{ {
Habit habit = new Habit(); Habit habit = new Habit();
habit.name = "Wake up early";
habit.description = "Did you wake up before 6am?";
habit.freqNum = 2; habit.freqNum = 2;
habit.freqDen = 3; habit.freqDen = 3;
habit.save(); habit.save();
@ -45,16 +48,19 @@ public class HabitFixtures
return habit; return habit;
} }
static Habit createEmptyHabit() public static Habit createEmptyHabit()
{ {
Habit habit = new Habit(); Habit habit = new Habit();
habit.name = "Meditate";
habit.description = "Did you meditate this morning?";
habit.color = ColorHelper.palette[3];
habit.freqNum = 1; habit.freqNum = 1;
habit.freqDen = 1; habit.freqDen = 1;
habit.save(); habit.save();
return habit; return habit;
} }
static void purgeHabits() public static void purgeHabits()
{ {
for(Habit h : Habit.getAll(true)) for(Habit h : Habit.getAll(true))
h.cascadeDelete(); h.cascadeDelete();

@ -23,12 +23,15 @@ import android.graphics.Color;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.helpers.DateHelper; import org.hamcrest.MatcherAssert;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.StringWriter;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -353,4 +356,21 @@ public class HabitTest
h.clearReminder(); h.clearReminder();
assertThat(h.hasReminder(), is(false)); assertThat(h.hasReminder(), is(false));
} }
@Test
public void writeCSV() throws IOException
{
HabitFixtures.createEmptyHabit();
HabitFixtures.createNonDailyHabit();
String expectedCSV =
"Name,Description,NumRepetitions,Interval,Color\n" +
"Meditate,Did you meditate this morning?,1,1,#AFB42B\n" +
"Wake up early,Did you wake up before 6am?,2,3,#00897B\n";
StringWriter writer = new StringWriter();
Habit.writeCSV(Habit.getAll(true), writer);
MatcherAssert.assertThat(writer.toString(), equalTo(expectedCSV));
}
} }

@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;

@ -22,8 +22,8 @@ package org.isoron.uhabits.unit.models;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.helpers.ActiveAndroidHelper; import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.Score;
import org.junit.After; import org.junit.After;
@ -31,6 +31,9 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.io.IOException;
import java.io.StringWriter;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
@ -130,9 +133,33 @@ public class ScoreListTest
assertThat(actualValues, equalTo(expectedValues)); assertThat(actualValues, equalTo(expectedValues));
} }
@Test
public void writeCSV() throws IOException
{
HabitFixtures.purgeHabits();
Habit habit = HabitFixtures.createNonDailyHabit();
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) private void toggleRepetitions(final int from, final int to)
{ {
ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command() DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command()
{ {
@Override @Override
public void execute() public void execute()

@ -19,29 +19,16 @@
package org.isoron.uhabits.unit.models; package org.isoron.uhabits.unit.models;
import android.graphics.Color;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Score;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.util.LinkedList;
import java.util.List;
import static org.hamcrest.Matchers.equalTo; 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.assertThat;
import static org.junit.Assert.fail;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.Checkmark;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@SmallTest @SmallTest

@ -0,0 +1,77 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.unit.tasks;
import android.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.models.Habit;
import org.isoron.uhabits.tasks.ExportCSVTask;
import org.isoron.uhabits.unit.models.HabitFixtures;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
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
{
@Test
public void exportCSV() throws InterruptedException
{
Context context = InstrumentationRegistry.getContext();
final CountDownLatch latch = new CountDownLatch(1);
HabitFixtures.createNonDailyHabit();
List<Habit> habits = Habit.getAll(true);
ProgressBar bar = new ProgressBar(context);
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());
latch.countDown();
}
});
task.execute();
latch.await(30, TimeUnit.SECONDS);
}
}

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

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

@ -28,11 +28,11 @@
<uses-permission <uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE" android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/> android:maxSdkVersion="18" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/> android:maxSdkVersion="18" />
<application <application
android:name="HabitsApplication" android:name="HabitsApplication"

@ -1,29 +0,0 @@
package org.isoron.helpers;
import com.activeandroid.ActiveAndroid;
public class ActiveAndroidHelper
{
public interface Command
{
void execute();
}
public static void executeAsTransaction(Command command)
{
ActiveAndroid.beginTransaction();
try
{
command.execute();
ActiveAndroid.setTransactionSuccessful();
}
catch (RuntimeException e)
{
throw e;
}
finally
{
ActiveAndroid.endTransaction();
}
}
}

@ -28,7 +28,7 @@ import android.os.Bundle;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
public class AboutActivity extends Activity implements View.OnClickListener public class AboutActivity extends Activity implements View.OnClickListener
{ {

@ -36,7 +36,7 @@ import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;

@ -20,19 +20,26 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.app.Application; import android.app.Application;
import android.content.Context;
import android.support.annotation.Nullable;
import com.activeandroid.ActiveAndroid; import com.activeandroid.ActiveAndroid;
import com.activeandroid.Configuration;
import org.isoron.uhabits.helpers.DatabaseHelper;
import java.io.File; import java.io.File;
public class HabitsApplication extends Application public class HabitsApplication extends Application
{ {
private boolean isTestMode() @Nullable
private static Context context;
public static boolean isTestMode()
{ {
try try
{ {
getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest"); if(context != null)
context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest");
return true; return true;
} }
catch (final Exception e) catch (final Exception e)
@ -41,37 +48,31 @@ public class HabitsApplication extends Application
} }
} }
private void deleteDB(String databaseFilename) @Nullable
public static Context getContext()
{ {
File databaseFile = new File(String.format("%s/../databases/%s", return context;
getApplicationContext().getFilesDir().getPath(), databaseFilename));
if(databaseFile.exists()) databaseFile.delete();
} }
@Override @Override
public void onCreate() public void onCreate()
{ {
super.onCreate(); super.onCreate();
String databaseFilename = BuildConfig.databaseFilename; HabitsApplication.context = this;
if (isTestMode()) if (isTestMode())
{ {
databaseFilename = "test.db"; File db = DatabaseHelper.getDatabaseFile();
deleteDB(databaseFilename); if(db.exists()) db.delete();
} }
Configuration dbConfig = new Configuration.Builder(this) DatabaseHelper.initializeActiveAndroid();
.setDatabaseName(databaseFilename)
.setDatabaseVersion(BuildConfig.databaseVersion)
.create();
ActiveAndroid.initialize(dbConfig);
} }
@Override @Override
public void onTerminate() public void onTerminate()
{ {
HabitsApplication.context = null;
ActiveAndroid.dispose(); ActiveAndroid.dispose();
super.onTerminate(); super.onTerminate();
} }

@ -19,6 +19,7 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.Manifest;
import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ComponentName; import android.content.ComponentName;
@ -26,17 +27,20 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.helpers.DialogHelper; import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.fragments.ListHabitsFragment; import org.isoron.uhabits.fragments.ListHabitsFragment;
import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
@ -56,6 +60,10 @@ public class MainActivity extends ReplayableActivity
public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH"; public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH";
public static final int RESULT_IMPORT_DATA = 1;
public static final int RESULT_EXPORT_CSV = 2;
public static final int RESULT_EXPORT_DB = 3;
@Override @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
{ {
@ -123,7 +131,7 @@ public class MainActivity extends ReplayableActivity
case R.id.action_settings: case R.id.action_settings:
{ {
Intent intent = new Intent(this, SettingsActivity.class); Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent); startActivityForResult(intent, 0);
return true; return true;
} }
@ -134,11 +142,39 @@ public class MainActivity extends ReplayableActivity
return true; return true;
} }
case R.id.action_faq:
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.helpURL)));
startActivity(intent);
return true;
}
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
} }
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
switch (resultCode)
{
case RESULT_IMPORT_DATA:
listHabitsFragment.showImportDialog();
break;
case RESULT_EXPORT_CSV:
listHabitsFragment.exportAllHabits();
break;
case RESULT_EXPORT_DB:
listHabitsFragment.exportDB();
break;
}
}
@Override @Override
public void onHabitClicked(Habit habit) public void onHabitClicked(Habit habit)
{ {
@ -197,4 +233,14 @@ public class MainActivity extends ReplayableActivity
listHabitsFragment.onPostExecuteCommand(null); listHabitsFragment.onPostExecuteCommand(null);
} }
} }
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults)
{
if (grantResults.length <= 0) return;
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) return;
listHabitsFragment.showImportDialog();
}
} }

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.helpers; package org.isoron.uhabits;
import android.app.Activity; import android.app.Activity;
import android.app.backup.BackupManager; import android.app.backup.BackupManager;
@ -25,7 +25,6 @@ import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast; import android.widget.Toast;
import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.Command;
import java.util.LinkedList; import java.util.LinkedList;

@ -30,7 +30,6 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.fragments.ShowHabitFragment; import org.isoron.uhabits.fragments.ShowHabitFragment;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;

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

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

@ -32,7 +32,6 @@ import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
@ -41,9 +40,9 @@ import com.android.colorpicker.ColorPickerSwatch;
import com.android.datetimepicker.time.RadialPickerLayout; import com.android.datetimepicker.time.RadialPickerLayout;
import com.android.datetimepicker.time.TimePickerDialog; import com.android.datetimepicker.time.TimePickerDialog;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.helpers.DialogHelper.OnSavedListener; import org.isoron.uhabits.helpers.DialogHelper.OnSavedListener;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.CreateHabitCommand; import org.isoron.uhabits.commands.CreateHabitCommand;

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

@ -17,36 +17,29 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.dialogs; package org.isoron.uhabits.fragments;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.view.ActionMode; import android.view.ActionMode;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import com.android.colorpicker.ColorPickerDialog; import com.android.colorpicker.ColorPickerDialog;
import com.android.colorpicker.ColorPickerSwatch; import com.android.colorpicker.ColorPickerSwatch;
import org.isoron.helpers.ColorHelper;
import org.isoron.helpers.DialogHelper;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.ReplayableActivity;
import org.isoron.uhabits.commands.ArchiveHabitsCommand; import org.isoron.uhabits.commands.ArchiveHabitsCommand;
import org.isoron.uhabits.commands.ChangeHabitColorCommand; import org.isoron.uhabits.commands.ChangeHabitColorCommand;
import org.isoron.uhabits.commands.DeleteHabitsCommand; import org.isoron.uhabits.commands.DeleteHabitsCommand;
import org.isoron.uhabits.commands.UnarchiveHabitsCommand; import org.isoron.uhabits.commands.UnarchiveHabitsCommand;
import org.isoron.uhabits.fragments.EditHabitFragment; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.io.CSVExporter; import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.loaders.HabitListLoader;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -205,12 +198,6 @@ public class HabitSelectionCallback implements ActionMode.Callback
return true; return true;
} }
case R.id.action_export_csv:
{
onExportHabitsClick(selectedHabits);
return true;
}
} }
return false; return false;
@ -221,47 +208,4 @@ public class HabitSelectionCallback implements ActionMode.Callback
{ {
if(listener != null) listener.onActionModeDestroyed(mode); if(listener != null) listener.onActionModeDestroyed(mode);
} }
private void onExportHabitsClick(final LinkedList<Habit> selectedHabits)
{
new AsyncTask<Void, Void, Void>()
{
String filename;
@Override
protected void onPreExecute()
{
if(progressBar != null)
{
progressBar.setIndeterminate(true);
progressBar.setVisibility(View.VISIBLE);
}
}
@Override
protected void onPostExecute(Void aVoid)
{
if(filename != null)
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("application/zip");
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(filename)));
activity.startActivity(intent);
}
if(progressBar != null)
progressBar.setVisibility(View.GONE);
}
@Override
protected Void doInBackground(Void... params)
{
CSVExporter exporter = new CSVExporter(activity, selectedHabits);
filename = exporter.writeArchive();
return null;
}
}.execute();
}
} }

@ -26,6 +26,7 @@ import android.content.SharedPreferences;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.view.ActionMode; import android.view.ActionMode;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
@ -48,20 +49,24 @@ import com.mobeta.android.dslv.DragSortController;
import com.mobeta.android.dslv.DragSortListView; import com.mobeta.android.dslv.DragSortListView;
import com.mobeta.android.dslv.DragSortListView.DropListener; import com.mobeta.android.dslv.DragSortListView.DropListener;
import org.isoron.uhabits.commands.Command;
import org.isoron.helpers.DateHelper;
import org.isoron.helpers.DialogHelper;
import org.isoron.helpers.DialogHelper.OnSavedListener;
import org.isoron.helpers.ReplayableActivity;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.ReplayableActivity;
import org.isoron.uhabits.commands.Command;
import org.isoron.uhabits.commands.ToggleRepetitionCommand; import org.isoron.uhabits.commands.ToggleRepetitionCommand;
import org.isoron.uhabits.dialogs.HabitSelectionCallback; import org.isoron.uhabits.dialogs.FilePickerDialog;
import org.isoron.uhabits.dialogs.HintManager; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.uhabits.helpers.DialogHelper.OnSavedListener;
import org.isoron.uhabits.helpers.HintManager;
import org.isoron.uhabits.helpers.ListHabitsHelper; import org.isoron.uhabits.helpers.ListHabitsHelper;
import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.helpers.ReminderHelper;
import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.loaders.HabitListLoader;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.tasks.ExportCSVTask;
import org.isoron.uhabits.tasks.ExportDBTask;
import org.isoron.uhabits.tasks.ImportDataTask;
import java.io.File;
import java.util.Date; import java.util.Date;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -69,7 +74,8 @@ import java.util.List;
public class ListHabitsFragment extends Fragment public class ListHabitsFragment extends Fragment
implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener, implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener,
OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener, OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener,
HabitSelectionCallback.Listener HabitSelectionCallback.Listener, ImportDataTask.Listener, ExportCSVTask.Listener,
ExportDBTask.Listener
{ {
long lastLongClick = 0; long lastLongClick = 0;
private boolean isShortToggleEnabled; private boolean isShortToggleEnabled;
@ -224,15 +230,6 @@ public class ListHabitsFragment extends Fragment
return true; return true;
} }
case R.id.action_faq:
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(getString(R.string.helpURL)));
startActivity(intent);
return true;
}
default: default:
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -426,4 +423,92 @@ public class ListHabitsFragment extends Fragment
selectItem(position); selectItem(position);
} }
} }
public void showImportDialog()
{
File dir = activity.getExternalFilesDir(null);
if(dir == null) return;
FilePickerDialog picker = new FilePickerDialog(activity, dir);
picker.setListener(new FilePickerDialog.OnFileSelectedListener()
{
@Override
public void onFileSelected(File file)
{
ImportDataTask task = new ImportDataTask(file, progressBar);
task.setListener(ListHabitsFragment.this);
task.execute();
}
});
picker.show();
}
@Override
public void onImportFinished(int result)
{
switch (result)
{
case ImportDataTask.SUCCESS:
loader.updateAllHabits(true);
activity.showToast(R.string.habits_imported);
break;
case ImportDataTask.NOT_RECOGNIZED:
activity.showToast(R.string.file_not_recognized);
break;
default:
activity.showToast(R.string.could_not_import);
break;
}
}
public void exportAllHabits()
{
ExportCSVTask task = new ExportCSVTask(Habit.getAll(true), progressBar);
task.setListener(this);
task.execute();
}
@Override
public void onExportCSVFinished(@Nullable String archiveFilename)
{
if(archiveFilename != null)
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("application/zip");
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(archiveFilename)));
activity.startActivity(intent);
}
else
{
activity.showToast(R.string.could_not_export);
}
}
public void exportDB()
{
ExportDBTask task = new ExportDBTask(progressBar);
task.setListener(this);
task.execute();
}
@Override
public void onExportDBFinished(@Nullable String filename)
{
if(filename != null)
{
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("application/octet-stream");
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(filename)));
activity.startActivity(intent);
}
else
{
activity.showToast(R.string.could_not_export);
}
}
} }

@ -22,8 +22,10 @@ package org.isoron.uhabits.fragments;
import android.app.backup.BackupManager; import android.app.backup.BackupManager;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import org.isoron.uhabits.MainActivity;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
public class SettingsFragment extends PreferenceFragment public class SettingsFragment extends PreferenceFragment
@ -34,6 +36,25 @@ public class SettingsFragment extends PreferenceFragment
{ {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
setResultOnPreferenceClick("importData", MainActivity.RESULT_IMPORT_DATA);
setResultOnPreferenceClick("exportCSV", MainActivity.RESULT_EXPORT_CSV);
setResultOnPreferenceClick("exportDB", MainActivity.RESULT_EXPORT_DB);
}
private void setResultOnPreferenceClick(String key, final int result)
{
Preference exportCSV = findPreference(key);
exportCSV.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener()
{
@Override
public boolean onPreferenceClick(Preference preference)
{
getActivity().setResult(result);
getActivity().finish();
return true;
}
});
} }
@Override @Override

@ -37,8 +37,8 @@ import android.widget.LinearLayout;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.DialogHelper; import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.ShowHabitActivity; import org.isoron.uhabits.ShowHabitActivity;

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.helpers; package org.isoron.uhabits.helpers;
import android.graphics.Color; import android.graphics.Color;
@ -91,4 +91,9 @@ public class ColorHelper
hsv[index] = newValue; hsv[index] = newValue;
return Color.HSVToColor(hsv); return Color.HSVToColor(hsv);
} }
public static String toHTML(int color)
{
return String.format("#%06X", 0xFFFFFF & color);
}
} }

@ -0,0 +1,166 @@
/*
* 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.helpers;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import com.activeandroid.ActiveAndroid;
import com.activeandroid.Configuration;
import org.isoron.uhabits.BuildConfig;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.models.Checkmark;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Repetition;
import org.isoron.uhabits.models.Score;
import org.isoron.uhabits.models.Streak;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
public class DatabaseHelper
{
public static void copy(File src, File dst) throws IOException
{
FileInputStream inStream = new FileInputStream(src);
FileOutputStream outStream = new FileOutputStream(dst);
copy(inStream, outStream);
}
public static void copy(InputStream inStream, File dst) throws IOException
{
FileOutputStream outStream = new FileOutputStream(dst);
copy(inStream, outStream);
}
public static void copy(InputStream in, OutputStream out) throws IOException
{
int numBytes;
byte[] buffer = new byte[1024];
while ((numBytes = in.read(buffer)) != -1)
out.write(buffer, 0, numBytes);
}
public interface Command
{
void execute();
}
public static void executeAsTransaction(Command command)
{
ActiveAndroid.beginTransaction();
try
{
command.execute();
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static String saveDatabaseCopy(File dir) throws IOException
{
File db = getDatabaseFile();
SimpleDateFormat dateFormat = DateHelper.getBackupDateFormat();
String date = dateFormat.format(DateHelper.getLocalTime());
File dbCopy = new File(String.format("%s/Loop Habits Backup %s.db", dir.getAbsolutePath(), date));
copy(db, dbCopy);
return dbCopy.getAbsolutePath();
}
@NonNull
public static File getDatabaseFile()
{
Context context = HabitsApplication.getContext();
if(context == null) throw new RuntimeException("No application context found");
String databaseFilename = getDatabaseFilename();
return new File(String.format("%s/../databases/%s",
context.getApplicationContext().getFilesDir().getPath(), databaseFilename));
}
@NonNull
public static String getDatabaseFilename()
{
String databaseFilename = BuildConfig.databaseFilename;
if (HabitsApplication.isTestMode())
databaseFilename = "test.db";
return databaseFilename;
}
@Nullable
public static File getFilesDir(String prefix)
{
Context context = HabitsApplication.getContext();
if(context == null) return null;
File chosenDir = null;
File externalFilesDirs[] = ContextCompat.getExternalFilesDirs(context, null);
if(externalFilesDirs == null) return null;
for(File dir : externalFilesDirs)
{
if (dir == null || !dir.canWrite()) continue;
chosenDir = dir;
break;
}
if(chosenDir == null) return null;
File dir = new File(String.format("%s/%s/", chosenDir.getAbsolutePath(), prefix));
dir.mkdirs();
return dir;
}
@SuppressWarnings("unchecked")
public static void initializeActiveAndroid()
{
Context context = HabitsApplication.getContext();
if(context == null) throw new RuntimeException("application context should not be null");
Configuration dbConfig = new Configuration.Builder(context)
.setDatabaseName(getDatabaseFilename())
.setDatabaseVersion(BuildConfig.databaseVersion)
.addModelClasses(Checkmark.class, Habit.class, Repetition.class, Score.class,
Streak.class)
.create();
ActiveAndroid.initialize(dbConfig);
}
}

@ -17,13 +17,14 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.helpers; package org.isoron.uhabits.helpers;
import android.content.Context; import android.content.Context;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.Locale; import java.util.Locale;
@ -96,6 +97,22 @@ public class DateHelper
return df.format(date); return df.format(date);
} }
public static SimpleDateFormat getCSVDateFormat()
{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat;
}
public static SimpleDateFormat getBackupDateFormat()
{
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat;
}
public static String formatHeaderDate(GregorianCalendar day) public static String formatHeaderDate(GregorianCalendar day)
{ {
String dayOfMonth = Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH)); String dayOfMonth = Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH));

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.helpers; package org.isoron.uhabits.helpers;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.dialogs; package org.isoron.uhabits.helpers;
import android.animation.Animator; import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
@ -27,7 +27,6 @@ import android.preference.PreferenceManager;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
public class HintManager public class HintManager

@ -30,7 +30,6 @@ import android.widget.Button;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.loaders.HabitListLoader;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;

@ -28,7 +28,6 @@ import android.os.Build;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.HabitBroadcastReceiver;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;

@ -0,0 +1,48 @@
/*
* 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.io;
import android.support.annotation.NonNull;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
public abstract class AbstractImporter
{
public abstract boolean canHandle(@NonNull File file) throws IOException;
public abstract void importHabitsFromFile(@NonNull File file) throws IOException;
public static boolean isSQLite3File(@NonNull File file) throws IOException
{
FileInputStream fis = new FileInputStream(file);
byte[] sqliteHeader = "SQLite format 3".getBytes();
byte[] buffer = new byte[sqliteHeader.length];
int count = fis.read(buffer);
if(count < sqliteHeader.length) return false;
return Arrays.equals(buffer, sqliteHeader);
}
}

@ -1,213 +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.io;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.activeandroid.Cache;
import org.isoron.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class CSVExporter
{
private List<Habit> habits;
private Context context;
private java.text.DateFormat dateFormat;
private List<String> generateDirs;
private List<String> generateFilenames;
private String basePath;
public CSVExporter(Context context, List<Habit> habits)
{
this.habits = habits;
this.context = context;
generateDirs = new LinkedList<>();
generateFilenames = new LinkedList<>();
basePath = String.format("%s/export/", context.getFilesDir());
dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public String formatDate(long timestamp)
{
return dateFormat.format(new Date(timestamp));
}
public String formatScore(int score)
{
return String.format("%.2f", ((float) score) / Score.MAX_VALUE);
}
private void writeScores(String dirPath, Habit habit) throws IOException
{
String path = dirPath + "scores.csv";
FileWriter out = new FileWriter(basePath + path);
generateFilenames.add(path);
String query = "select timestamp, score from score where habit = ? order by timestamp";
String params[] = { habit.getId().toString() };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return;
do
{
String timestamp = formatDate(cursor.getLong(0));
String score = formatScore(cursor.getInt(1));
out.write(String.format("%s,%s\n", timestamp, score));
} while(cursor.moveToNext());
out.close();
cursor.close();
}
private void writeCheckmarks(String dirPath, Habit habit) throws IOException
{
String path = dirPath + "checkmarks.csv";
FileWriter out = new FileWriter(basePath + path);
generateFilenames.add(path);
String query = "select timestamp, value from checkmarks where habit = ? order by timestamp";
String params[] = { habit.getId().toString() };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return;
do
{
String timestamp = formatDate(cursor.getLong(0));
Integer value = cursor.getInt(1);
out.write(String.format("%s,%d\n", timestamp, value));
} while(cursor.moveToNext());
out.close();
cursor.close();
}
private void writeFiles(Habit habit) throws IOException
{
String path = String.format("%s/", habit.name);
new File(basePath + path).mkdirs();
generateDirs.add(path);
writeScores(path, habit);
writeCheckmarks(path, habit);
}
private void writeZipFile(String zipFilename) throws IOException
{
FileOutputStream fos = new FileOutputStream(zipFilename);
ZipOutputStream zos = new ZipOutputStream(fos);
for(String filename : generateFilenames)
addFileToZip(zos, filename);
zos.close();
fos.close();
}
private void addFileToZip(ZipOutputStream zos, String filename) throws IOException
{
FileInputStream fis = new FileInputStream(new File(basePath + filename));
ZipEntry ze = new ZipEntry(filename);
zos.putNextEntry(ze);
int length;
byte bytes[] = new byte[1024];
while((length = fis.read(bytes)) >= 0)
zos.write(bytes, 0, length);
zos.closeEntry();
fis.close();
}
private void cleanup()
{
for(String filename : generateFilenames)
new File(basePath + filename).delete();
for(String filename : generateDirs)
new File(basePath + filename).delete();
new File(basePath).delete();
}
public String writeArchive()
{
String date = formatDate(DateHelper.getStartOfToday());
File dir = context.getExternalCacheDir();
if(dir == null)
{
Log.e("CSVExporter", "No suitable directory found.");
return null;
}
String zipFilename = String.format("%s/habits-%s.zip", dir, date);
try
{
for (Habit h : habits)
writeFiles(h);
writeZipFile(zipFilename);
cleanup();
}
catch (IOException e)
{
e.printStackTrace();
return null;
}
return zipFilename;
}
}

@ -0,0 +1,58 @@
/*
* 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.io;
import android.support.annotation.NonNull;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
public class GenericImporter extends AbstractImporter
{
List<AbstractImporter> importers;
public GenericImporter()
{
importers = new LinkedList<>();
importers.add(new LoopDBImporter());
importers.add(new RewireDBImporter());
importers.add(new TickmateDBImporter());
importers.add(new HabitBullCSVImporter());
}
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
for(AbstractImporter importer : importers)
if(importer.canHandle(file)) return true;
return false;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
for(AbstractImporter importer : importers)
if(importer.canHandle(file))
importer.importHabitsFromFile(file);
}
}

@ -0,0 +1,104 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.io;
import android.support.annotation.NonNull;
import com.activeandroid.ActiveAndroid;
import com.opencsv.CSVReader;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap;
public class HabitBullCSVImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
BufferedReader reader = new BufferedReader(new FileReader(file));
String line = reader.readLine();
return line.startsWith("HabitName,HabitDescription,HabitCategory");
}
@Override
public void importHabitsFromFile(@NonNull final File file) throws IOException
{
ActiveAndroid.beginTransaction();
try
{
parseFile(file);
ActiveAndroid.setTransactionSuccessful();
}
finally
{
ActiveAndroid.endTransaction();
}
}
private void parseFile(@NonNull File file) throws IOException
{
CSVReader reader = new CSVReader(new FileReader(file));
HashMap<String, Habit> habits = new HashMap<>();
for(String line[] : reader)
{
String name = line[0];
if(name.equals("HabitName")) continue;
String description = line[1];
String dateString[] = line[3].split("-");
int year = Integer.parseInt(dateString[0]);
int month = Integer.parseInt(dateString[1]);
int day = Integer.parseInt(dateString[2]);
Calendar date = DateHelper.getStartOfTodayCalendar();
date.set(year, month - 1, day);
long timestamp = date.getTimeInMillis();
int value = Integer.parseInt(line[4]);
if(value != 1) continue;
Habit h = habits.get(name);
if(h == null)
{
h = new Habit();
h.name = name;
h.description = description;
h.freqNum = h.freqDen = 1;
h.save();
habits.put(name, h);
}
if(!h.repetitions.contains(timestamp))
h.repetitions.toggle(timestamp);
}
}
}

@ -0,0 +1,148 @@
/*
* 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.io;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.CheckmarkList;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.ScoreList;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.LinkedList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class HabitsCSVExporter
{
private List<Habit> habits;
private List<String> generateDirs;
private List<String> generateFilenames;
private String exportDirName;
public HabitsCSVExporter(List<Habit> habits, File dir)
{
this.habits = habits;
this.exportDirName = dir.getAbsolutePath() + "/";
generateDirs = new LinkedList<>();
generateFilenames = new LinkedList<>();
}
private void writeHabits() throws IOException
{
String filename = "Habits.csv";
new File(exportDirName).mkdirs();
FileWriter out = new FileWriter(exportDirName + filename);
generateFilenames.add(filename);
Habit.writeCSV(habits, out);
out.close();
for(Habit h : habits)
{
String habitDirName = String.format("%03d %s/", h.position + 1, h.name);
new File(exportDirName + habitDirName).mkdirs();
generateDirs.add(habitDirName);
writeScores(habitDirName, h.scores);
writeCheckmarks(habitDirName, h.checkmarks);
}
}
private void writeScores(String habitDirName, ScoreList scores) throws IOException
{
String path = habitDirName + "Scores.csv";
FileWriter out = new FileWriter(exportDirName + path);
generateFilenames.add(path);
scores.writeCSV(out);
out.close();
}
private void writeCheckmarks(String habitDirName, CheckmarkList checkmarks) throws IOException
{
String filename = habitDirName + "Checkmarks.csv";
FileWriter out = new FileWriter(exportDirName + filename);
generateFilenames.add(filename);
checkmarks.writeCSV(out);
out.close();
}
private String writeZipFile() throws IOException
{
SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat();
String date = dateFormat.format(DateHelper.getStartOfToday());
String zipFilename = String.format("%s/Loop Habits CSV %s.zip", exportDirName, date);
FileOutputStream fos = new FileOutputStream(zipFilename);
ZipOutputStream zos = new ZipOutputStream(fos);
for(String filename : generateFilenames)
addFileToZip(zos, filename);
zos.close();
fos.close();
return zipFilename;
}
private void addFileToZip(ZipOutputStream zos, String filename) throws IOException
{
FileInputStream fis = new FileInputStream(new File(exportDirName + filename));
ZipEntry ze = new ZipEntry(filename);
zos.putNextEntry(ze);
int length;
byte bytes[] = new byte[1024];
while((length = fis.read(bytes)) >= 0)
zos.write(bytes, 0, length);
zos.closeEntry();
fis.close();
}
public String writeArchive() throws IOException
{
String zipFilename;
writeHabits();
zipFilename = writeZipFile();
cleanup();
return zipFilename;
}
private void cleanup()
{
for(String filename : generateFilenames)
new File(exportDirName + filename).delete();
for(String filename : generateDirs)
new File(exportDirName + filename).delete();
new File(exportDirName).delete();
}
}

@ -0,0 +1,61 @@
/*
* 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.io;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import com.activeandroid.ActiveAndroid;
import org.isoron.uhabits.helpers.DatabaseHelper;
import java.io.File;
import java.io.IOException;
public class LoopDBImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if(!isSQLite3File(file)) return false;
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"Checkmarks", "Repetitions"});
boolean result = (c.moveToFirst() && c.getInt(0) == 2);
c.close();
db.close();
return result;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
ActiveAndroid.dispose();
File originalDB = DatabaseHelper.getDatabaseFile();
DatabaseHelper.copy(file, originalDB);
DatabaseHelper.initializeActiveAndroid();
}
}

@ -0,0 +1,193 @@
/*
* 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.io;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.io.IOException;
import java.util.GregorianCalendar;
public class RewireDBImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if(!isSQLite3File(file)) return false;
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"CHECKINS", "UNIT"});
boolean result = (c.moveToFirst() && c.getInt(0) == 2);
c.close();
db.close();
return result;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command()
{
@Override
public void execute()
{
createHabits(db);
}
});
db.close();
}
private void createHabits(SQLiteDatabase db)
{
Cursor c = null;
try
{
c = db.rawQuery("select _id, name, description, schedule, active_days, " +
"repeating_count, days, period from habits", new String[0]);
if (!c.moveToFirst()) return;
do
{
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
int schedule = c.getInt(3);
String activeDays = c.getString(4);
int repeatingCount = c.getInt(5);
int days = c.getInt(6);
int periodIndex = c.getInt(7);
Habit habit = new Habit();
habit.name = name;
habit.description = description;
int periods[] = { 7, 31, 365 };
switch (schedule)
{
case 0:
habit.freqNum = activeDays.split(",").length;
habit.freqDen = 7;
break;
case 1:
habit.freqNum = days;
habit.freqDen = periods[periodIndex];
break;
case 2:
habit.freqNum = 1;
habit.freqDen = repeatingCount;
break;
}
habit.save();
createReminder(db, habit, id);
createCheckmarks(db, habit, id);
}
while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
private void createReminder(SQLiteDatabase db, Habit habit, int rewireHabitId)
{
String[] params = { Integer.toString(rewireHabitId) };
Cursor c = null;
try
{
c = db.rawQuery("select time, active_days from reminders where habit_id=? limit 1", params);
if (!c.moveToFirst()) return;
int rewireReminder = Integer.parseInt(c.getString(0));
if (rewireReminder <= 0 || rewireReminder >= 1440) return;
boolean reminderDays[] = new boolean[7];
String activeDays[] = c.getString(1).split(",");
for(String d : activeDays)
{
int idx = (Integer.parseInt(d) + 1) % 7;
reminderDays[idx] = true;
}
habit.reminderDays = DateHelper.packWeekdayList(reminderDays);
habit.reminderHour = rewireReminder / 60;
habit.reminderMin = rewireReminder % 60;
habit.save();
}
finally
{
if(c != null) c.close();
}
}
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int rewireHabitId)
{
Cursor c = null;
try
{
String[] params = { Integer.toString(rewireHabitId) };
c = db.rawQuery("select distinct date from checkins where habit_id=? and type=2", params);
if (!c.moveToFirst()) return;
do
{
String date = c.getString(0);
int year = Integer.parseInt(date.substring(0, 4));
int month = Integer.parseInt(date.substring(4, 6));
int day = Integer.parseInt(date.substring(6, 8));
GregorianCalendar cal = DateHelper.getStartOfTodayCalendar();
cal.set(year, month - 1, day);
habit.repetitions.toggle(cal.getTimeInMillis());
}
while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
}

@ -0,0 +1,133 @@
/*
* 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.io;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.io.IOException;
import java.util.GregorianCalendar;
public class TickmateDBImporter extends AbstractImporter
{
@Override
public boolean canHandle(@NonNull File file) throws IOException
{
if(!isSQLite3File(file)) return false;
SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
Cursor c = db.rawQuery("select count(*) from SQLITE_MASTER where name=? or name=?",
new String[]{"tracks", "track2groups"});
boolean result = (c.moveToFirst() && c.getInt(0) == 2);
c.close();
db.close();
return result;
}
@Override
public void importHabitsFromFile(@NonNull File file) throws IOException
{
final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null,
SQLiteDatabase.OPEN_READONLY);
DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command()
{
@Override
public void execute()
{
createHabits(db);
}
});
db.close();
}
private void createHabits(SQLiteDatabase db)
{
Cursor c = null;
try
{
c = db.rawQuery("select _id, name, description from tracks", new String[0]);
if (!c.moveToFirst()) return;
do
{
int id = c.getInt(0);
String name = c.getString(1);
String description = c.getString(2);
Habit habit = new Habit();
habit.name = name;
habit.description = description;
habit.freqNum = 1;
habit.freqDen = 1;
habit.save();
createCheckmarks(db, habit, id);
}
while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
private void createCheckmarks(@NonNull SQLiteDatabase db, @NonNull Habit habit, int tickmateTrackId)
{
Cursor c = null;
try
{
String[] params = { Integer.toString(tickmateTrackId) };
c = db.rawQuery("select distinct year, month, day from ticks where _track_id=?", params);
if (!c.moveToFirst()) return;
do
{
int year = c.getInt(0);
int month = c.getInt(1);
int day = c.getInt(2);
GregorianCalendar cal = DateHelper.getStartOfTodayCalendar();
cal.set(year, month, day);
habit.repetitions.toggle(cal.getTimeInMillis());
}
while (c.moveToNext());
}
finally
{
if (c != null) c.close();
}
}
}

@ -24,7 +24,7 @@ import android.os.Handler;
import android.view.View; import android.view.View;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.util.HashMap; import java.util.HashMap;

@ -29,8 +29,12 @@ import com.activeandroid.Cache;
import com.activeandroid.query.Delete; import com.activeandroid.query.Delete;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List; import java.util.List;
public class CheckmarkList public class CheckmarkList
@ -72,6 +76,7 @@ public class CheckmarkList
public int[] getValues(long fromTimestamp, long toTimestamp) public int[] getValues(long fromTimestamp, long toTimestamp)
{ {
compute(fromTimestamp, toTimestamp); compute(fromTimestamp, toTimestamp);
if(fromTimestamp > toTimestamp) return new int[0]; if(fromTimestamp > toTimestamp) return new int[0];
String query = "select value, timestamp from Checkmarks where " + String query = "select value, timestamp from Checkmarks where " +
@ -123,6 +128,21 @@ public class CheckmarkList
return getValues(fromTimestamp, toTimestamp); return getValues(fromTimestamp, toTimestamp);
} }
/**
* Computes and stores one checkmark for each day, since the first repetition until today.
* Days that already have a corresponding checkmark are skipped.
*/
protected void computeAll()
{
Repetition oldestRep = habit.repetitions.getOldest();
if(oldestRep == null) return;
Long fromTimestamp = oldestRep.timestamp;
Long toTimestamp = DateHelper.getStartOfToday();
compute(fromTimestamp, toTimestamp);
}
/** /**
* Computes and stores one checkmark for each day that falls inside the specified interval of * Computes and stores one checkmark for each day that falls inside the specified interval of
* time. Days that already have a corresponding checkmark are skipped. * time. Days that already have a corresponding checkmark are skipped.
@ -229,4 +249,38 @@ public class CheckmarkList
if(today != null) return today.value; if(today != null) return today.value;
else return Checkmark.UNCHECKED; else return Checkmark.UNCHECKED;
} }
/**
* Writes the entire list of checkmarks to the given writer, in CSV format. There is one
* line for each checkmark. Each line contains two fields: timestamp and value.
*
* @param out the writer where the CSV will be output
* @throws IOException in case write operations fail
*/
public void writeCSV(Writer out) throws IOException
{
computeAll();
SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat();
String query = "select timestamp, value from checkmarks where habit = ? order by timestamp";
String params[] = { habit.getId().toString() };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return;
do
{
String timestamp = dateFormat.format(new Date(cursor.getLong(0)));
Integer value = cursor.getInt(1);
out.write(String.format("%s,%d\n", timestamp, value));
} while(cursor.moveToNext());
cursor.close();
out.close();
}
} }

@ -33,10 +33,13 @@ import com.activeandroid.query.From;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import com.activeandroid.query.Update; import com.activeandroid.query.Update;
import com.activeandroid.util.SQLiteUtils; import com.activeandroid.util.SQLiteUtils;
import com.opencsv.CSVWriter;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import java.io.IOException;
import java.io.Writer;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -470,4 +473,30 @@ public class Habit extends Model
reminderMin = null; reminderMin = null;
reminderDays = DateHelper.ALL_WEEK_DAYS; reminderDays = DateHelper.ALL_WEEK_DAYS;
} }
/**
* Writes the list of habits to the given writer, in CSV format. There is one line for each
* habit, containing the fields name, description, frequency numerator, frequency denominator
* and color. The color is written in HTML format (#000000).
*
* @param habits the list of habits to write
* @param out the writer that will receive the result
* @throws IOException if write operations fail
*/
public static void writeCSV(List<Habit> habits, Writer out) throws IOException
{
String header[] = { "Name", "Description", "NumRepetitions", "Interval", "Color" };
CSVWriter csv = new CSVWriter(out);
csv.writeNext(header, false);
for(Habit habit : habits)
{
String[] cols = { habit.name, habit.description, Integer.toString(habit.freqNum),
Integer.toString(habit.freqDen), ColorHelper.toHTML(habit.color) };
csv.writeNext(cols, false);
}
csv.close();
}
} }

@ -29,7 +29,7 @@ import com.activeandroid.query.Delete;
import com.activeandroid.query.From; import com.activeandroid.query.From;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import java.util.Arrays; import java.util.Arrays;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;

@ -24,14 +24,18 @@ import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.activeandroid.ActiveAndroid;
import com.activeandroid.Cache; import com.activeandroid.Cache;
import com.activeandroid.query.Delete; import com.activeandroid.query.Delete;
import com.activeandroid.query.From; import com.activeandroid.query.From;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import org.isoron.helpers.ActiveAndroidHelper; import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ScoreList public class ScoreList
{ {
@ -95,6 +99,19 @@ public class ScoreList
.execute(); .execute();
} }
/**
* Computes and saves the scores that are missing since the first repetition of the habit.
*/
private void computeAll()
{
Repetition oldestRep = habit.repetitions.getOldest();
if(oldestRep == null) return;
long fromTimestamp = oldestRep.timestamp;
long toTimestamp = DateHelper.getStartOfToday();
compute(fromTimestamp, toTimestamp);
}
/** /**
* Computes and saves the scores that are missing inside a given time interval. Scores that * Computes and saves the scores that are missing inside a given time interval. Scores that
* have already been computed are skipped, therefore there is no harm in calling this function * have already been computed are skipped, therefore there is no harm in calling this function
@ -122,7 +139,7 @@ public class ScoreList
final int firstScore = newestScoreValue; final int firstScore = newestScoreValue;
final long beginning = from; final long beginning = from;
ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command() DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command()
{ {
@Override @Override
public void execute() public void execute()
@ -278,4 +295,30 @@ public class ScoreList
if(score != null) return score.getStarStatus(); if(score != null) return score.getStarStatus();
else return Score.EMPTY_STAR; else return Score.EMPTY_STAR;
} }
public void writeCSV(Writer out) throws IOException
{
computeAll();
SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat();
String query = "select timestamp, score from score where habit = ? order by timestamp";
String params[] = { habit.getId().toString() };
SQLiteDatabase db = Cache.openDatabase();
Cursor cursor = db.rawQuery(query, params);
if(!cursor.moveToFirst()) return;
do
{
String timestamp = dateFormat.format(new Date(cursor.getLong(0)));
String score = String.format("%.4f", ((float) cursor.getInt(1)) / Score.MAX_VALUE);
out.write(String.format("%s,%s\n", timestamp, score));
} while(cursor.moveToNext());
cursor.close();
out.close();
}
} }

@ -23,7 +23,7 @@ import com.activeandroid.ActiveAndroid;
import com.activeandroid.query.Delete; import com.activeandroid.query.Delete;
import com.activeandroid.query.Select; import com.activeandroid.query.Select;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;

@ -0,0 +1,96 @@
/*
* 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.os.AsyncTask;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.ProgressBar;
import org.isoron.uhabits.helpers.DatabaseHelper;
import org.isoron.uhabits.io.HabitsCSVExporter;
import org.isoron.uhabits.models.Habit;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class ExportCSVTask extends AsyncTask<Void, Void, Void>
{
public interface Listener
{
void onExportCSVFinished(@Nullable String archiveFilename);
}
private ProgressBar progressBar;
private final List<Habit> selectedHabits;
private String archiveFilename;
private ExportCSVTask.Listener listener;
public ExportCSVTask(List<Habit> selectedHabits, ProgressBar progressBar)
{
this.selectedHabits = selectedHabits;
this.progressBar = progressBar;
}
public void setListener(Listener listener)
{
this.listener = listener;
}
@Override
protected void onPreExecute()
{
if(progressBar != null)
{
progressBar.setIndeterminate(true);
progressBar.setVisibility(View.VISIBLE);
}
}
@Override
protected void onPostExecute(Void aVoid)
{
if(listener != null)
listener.onExportCSVFinished(archiveFilename);
if(progressBar != null)
progressBar.setVisibility(View.GONE);
}
@Override
protected Void doInBackground(Void... params)
{
try
{
File dir = DatabaseHelper.getFilesDir("CSV");
if(dir == null) return null;
HabitsCSVExporter exporter = new HabitsCSVExporter(selectedHabits, dir);
archiveFilename = exporter.writeArchive();
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}

@ -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.tasks;
import android.os.AsyncTask;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.ProgressBar;
import org.isoron.uhabits.helpers.DatabaseHelper;
import java.io.File;
import java.io.IOException;
public class ExportDBTask extends AsyncTask<Void, Void, Void>
{
public interface Listener
{
void onExportDBFinished(@Nullable String filename);
}
private ProgressBar progressBar;
private String filename;
private Listener listener;
public ExportDBTask(ProgressBar progressBar)
{
this.progressBar = progressBar;
}
public void setListener(Listener listener)
{
this.listener = listener;
}
@Override
protected void onPreExecute()
{
if(progressBar != null)
{
progressBar.setIndeterminate(true);
progressBar.setVisibility(View.VISIBLE);
}
}
@Override
protected void onPostExecute(Void aVoid)
{
if(listener != null)
listener.onExportDBFinished(filename);
if(progressBar != null)
progressBar.setVisibility(View.GONE);
}
@Override
protected Void doInBackground(Void... params)
{
filename = null;
try
{
File dir = DatabaseHelper.getFilesDir("Backups");
if(dir == null) return null;
filename = DatabaseHelper.saveDatabaseCopy(dir);
}
catch(IOException e)
{
e.printStackTrace();
}
return null;
}
}

@ -0,0 +1,108 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.tasks;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.ProgressBar;
import org.isoron.uhabits.io.GenericImporter;
import java.io.File;
public class ImportDataTask extends AsyncTask<Void, Void, Void>
{
public static final int SUCCESS = 1;
public static final int NOT_RECOGNIZED = 2;
public static final int FAILED = 3;
public interface Listener
{
void onImportFinished(int result);
}
@Nullable
private final ProgressBar progressBar;
@NonNull
private final File file;
@Nullable
private Listener listener;
int result;
public ImportDataTask(@NonNull File file, @Nullable ProgressBar progressBar)
{
this.file = file;
this.progressBar = progressBar;
}
public void setListener(@Nullable Listener listener)
{
this.listener = listener;
}
@Override
protected void onPreExecute()
{
if(progressBar != null)
{
progressBar.setIndeterminate(true);
progressBar.setVisibility(View.VISIBLE);
}
}
@Override
protected void onPostExecute(Void aVoid)
{
if(progressBar != null)
progressBar.setVisibility(View.GONE);
if(listener != null) listener.onImportFinished(result);
}
@Override
protected Void doInBackground(Void... params)
{
try
{
GenericImporter importer = new GenericImporter();
if(importer.canHandle(file))
{
importer.importHabitsFromFile(file);
result = SUCCESS;
}
else
{
result = NOT_RECOGNIZED;
}
}
catch (Exception e)
{
result = FAILED;
e.printStackTrace();
}
return null;
}
}

@ -32,7 +32,7 @@ import android.text.TextPaint;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;

@ -26,8 +26,8 @@ import android.graphics.Paint;
import android.graphics.RectF; import android.graphics.RectF;
import android.util.AttributeSet; import android.util.AttributeSet;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;

@ -29,8 +29,8 @@ import android.os.AsyncTask;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.MotionEvent; import android.view.MotionEvent;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;

@ -29,8 +29,8 @@ import android.graphics.RectF;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.AttributeSet; import android.util.AttributeSet;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Score; import org.isoron.uhabits.models.Score;

@ -26,8 +26,8 @@ import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.AttributeSet; import android.util.AttributeSet;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.Streak; import org.isoron.uhabits.models.Streak;

@ -31,8 +31,8 @@ import android.text.TextPaint;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.DialogHelper; import org.isoron.uhabits.helpers.DialogHelper;
public class NumberView extends View public class NumberView extends View
{ {

@ -22,8 +22,8 @@ package org.isoron.uhabits.views;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet; import android.util.AttributeSet;
import org.isoron.helpers.DateHelper; import org.isoron.uhabits.helpers.DateHelper;
import org.isoron.helpers.DialogHelper; import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;
import java.util.Calendar; import java.util.Calendar;

@ -25,15 +25,14 @@ import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.RectF; import android.graphics.RectF;
import android.os.Build;
import android.text.Layout; import android.text.Layout;
import android.text.StaticLayout; import android.text.StaticLayout;
import android.text.TextPaint; import android.text.TextPaint;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import org.isoron.helpers.ColorHelper; import org.isoron.uhabits.helpers.ColorHelper;
import org.isoron.helpers.DialogHelper; import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
public class RingView extends View public class RingView extends View

@ -34,7 +34,7 @@ import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import org.isoron.helpers.DialogHelper; import org.isoron.uhabits.helpers.DialogHelper;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Habit;

@ -40,11 +40,6 @@
android:title="@string/unarchive" android:title="@string/unarchive"
android:icon="@drawable/ic_action_unarchive_dark"/> android:icon="@drawable/ic_action_unarchive_dark"/>
<item
android:id="@+id/action_export_csv"
android:title="@string/export_to_csv"
android:showAsAction="never" />
<item <item
android:id="@+id/action_delete" android:id="@+id/action_delete"
android:title="@string/delete" android:title="@string/delete"

@ -40,10 +40,6 @@
android:title="@string/unarchive" android:title="@string/unarchive"
android:icon="@drawable/ic_action_unarchive_light"/> android:icon="@drawable/ic_action_unarchive_light"/>
<item
android:id="@+id/action_export_csv"
android:title="@string/export_to_csv" />
<item <item
android:id="@+id/action_delete" android:id="@+id/action_delete"
android:title="@string/delete" /> android:title="@string/delete" />

@ -106,7 +106,7 @@
<string name="any_weekday">Monday to Friday</string> <string name="any_weekday">Monday to Friday</string>
<string name="any_day">Any day of the week</string> <string name="any_day">Any day of the week</string>
<string name="select_weekdays">Select days</string> <string name="select_weekdays">Select days</string>
<string name="export_to_csv">Export data</string> <string name="export_to_csv">Export as CSV</string>
<string name="done_label">Done</string> <string name="done_label">Done</string>
<string name="clear_label">Clear</string> <string name="clear_label">Clear</string>
@ -138,4 +138,14 @@
<string name="five_times_per_week">5 times per week</string> <string name="five_times_per_week">5 times per week</string>
<string name="custom_frequency">Custom …</string> <string name="custom_frequency">Custom …</string>
<string name="help">Help &amp; FAQ</string> <string name="help">Help &amp; FAQ</string>
<string name="could_not_export">Failed to export data.</string>
<string name="could_not_import">Failed to import data.</string>
<string name="file_not_recognized">File not recognized.</string>
<string name="habits_imported">Habits imported successfully.</string>
<string name="full_backup_success">Full backup successfully exported.</string>
<string name="import_data">Import data</string>
<string name="export_full_backup">Export full backup</string>
<string name="import_data_summary">Supports full backups exported by this app, as well as files generated by Tickmate, HabitBull or Rewire. See FAQ for more information.</string>
<string name="export_as_csv_summary">Generates files that can be opened by spreadsheet software such as Microsoft Excel or OpenOffice Calc. This file cannot be imported back.</string>
<string name="export_full_backup_summary">Generates a file that contains all your data. This file can be imported back.</string>
</resources> </resources>

@ -36,7 +36,32 @@
android:entries="@array/snooze_interval_names" android:entries="@array/snooze_interval_names"
android:entryValues="@array/snooze_interval_values" android:entryValues="@array/snooze_interval_values"
android:key="pref_snooze_interval" android:key="pref_snooze_interval"
android:title="@string/pref_snooze_interval_title"/> android:title="@string/pref_snooze_interval_title"
android:summary="%s"/>
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_links"
android:title="Database">
<Preference
android:key="exportDB"
android:summary="@string/export_full_backup_summary"
android:title="@string/export_full_backup">
</Preference>
<Preference
android:key="exportCSV"
android:summary="@string/export_as_csv_summary"
android:title="@string/export_to_csv">
</Preference>
<Preference
android:key="importData"
android:summary="@string/import_data_summary"
android:title="@string/import_data">
</Preference>
</PreferenceCategory> </PreferenceCategory>

@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.1.0-alpha2' classpath 'com.android.tools.build:gradle:2.1.0-alpha3'
} }
} }
@ -13,3 +13,15 @@ allprojects {
jcenter() jcenter()
} }
} }
project.ext.preDexLibs = !project.hasProperty('disablePreDex')
subprojects {
project.plugins.whenPluginAdded { plugin ->
if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) {
project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
} else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) {
project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
}
}
}

@ -5,12 +5,13 @@ checkout:
test: test:
override: override:
- emulator -avd circleci-android22 -no-audio -no-window: - mksdcard -l e 128M sdcard.img
- emulator -avd circleci-android22 -no-audio -no-window -sdcard sdcard.img:
background: true background: true
parallel: true parallel: true
- circle-android wait-for-boot - circle-android wait-for-boot
- adb shell input keyevent 82 - adb shell input keyevent 82
- ./gradlew connectedAndroidTest - ./gradlew -PdisablePreDex connectedAndroidTest
- cp -r app/build/outputs $CIRCLE_ARTIFACTS || echo ok - cp -r app/build/outputs $CIRCLE_ARTIFACTS || echo ok
- cp -r app/build/reports/androidTests/connected/* $CIRCLE_TEST_REPORTS || echo ok - cp -r app/build/reports/androidTests/connected/* $CIRCLE_TEST_REPORTS || echo ok
- adb logcat -d > $CIRCLE_TEST_REPORTS/logcat.txt - adb logcat -d > $CIRCLE_TEST_REPORTS/logcat.txt
Loading…
Cancel
Save