diff --git a/app/build.gradle b/app/build.gradle index 2921e8b81..695bd53a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -35,6 +35,7 @@ dependencies { compile 'com.android.support:support-v4:23.1.1' compile 'com.github.paolorotolo:appintro:3.4.0' compile 'org.apmem.tools:layouts:1.10@aar' + compile 'com.opencsv:opencsv:3.7' compile project(':libs:drag-sort-listview:library') compile files('libs/ActiveAndroid.jar') diff --git a/app/src/androidTest/assets/habitbull.csv b/app/src/androidTest/assets/habitbull.csv new file mode 100644 index 000000000..977a8e8df --- /dev/null +++ b/app/src/androidTest/assets/habitbull.csv @@ -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, diff --git a/app/src/androidTest/assets/icon.png b/app/src/androidTest/assets/icon.png new file mode 100644 index 000000000..7907954db Binary files /dev/null and b/app/src/androidTest/assets/icon.png differ diff --git a/app/src/androidTest/assets/loop.db b/app/src/androidTest/assets/loop.db new file mode 100644 index 000000000..25f0f32cd Binary files /dev/null and b/app/src/androidTest/assets/loop.db differ diff --git a/app/src/androidTest/assets/rewire.db b/app/src/androidTest/assets/rewire.db new file mode 100644 index 000000000..21f55032f Binary files /dev/null and b/app/src/androidTest/assets/rewire.db differ diff --git a/app/src/androidTest/assets/tickmate.db b/app/src/androidTest/assets/tickmate.db new file mode 100644 index 000000000..a4bec769c Binary files /dev/null and b/app/src/androidTest/assets/tickmate.db differ diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java b/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java index 0fbb13f34..ee8b810b8 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/HabitMatchers.java @@ -19,10 +19,12 @@ package org.isoron.uhabits.ui; +import android.preference.Preference; import android.view.View; import android.widget.Adapter; import android.widget.AdapterView; +import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; 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)); + } + }; + } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java index c73e1bc30..4d2066501 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java @@ -1,6 +1,28 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + package org.isoron.uhabits.ui; +import android.app.Activity; +import android.app.Instrumentation; import android.content.Context; +import android.content.Intent; import android.support.test.InstrumentationRegistry; import android.support.test.espresso.NoMatchingViewException; import android.support.test.espresso.intent.rule.IntentsTestRule; @@ -9,6 +31,7 @@ import android.test.suitebuilder.annotation.LargeTest; import org.isoron.uhabits.MainActivity; import org.isoron.uhabits.R; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; import org.junit.After; import org.junit.Before; @@ -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.swipeUp; import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.intent.Intents.intended; +import static android.support.test.espresso.intent.Intents.intending; +import static android.support.test.espresso.intent.matcher.IntentMatchers.hasAction; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.isRoot; import static android.support.test.espresso.matcher.ViewMatchers.withClassName; import static android.support.test.espresso.matcher.ViewMatchers.withId; import static android.support.test.espresso.matcher.ViewMatchers.withText; import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; +import static org.isoron.uhabits.ui.HabitMatchers.isPreferenceWithText; import static org.isoron.uhabits.ui.HabitMatchers.withName; import static org.isoron.uhabits.ui.HabitViewActions.clickAtRandomLocations; import static org.isoron.uhabits.ui.HabitViewActions.toggleAllCheckmarks; @@ -65,6 +95,8 @@ public class MainTest public IntentsTestRule activityRule = new IntentsTestRule<>( MainActivity.class); + private Context targetContext; + @Before public void setup() { @@ -74,6 +106,14 @@ public class MainTest sys.acquireWakeLock(); sys.unlockScreen(); + targetContext = InstrumentationRegistry.getTargetContext(); + + Instrumentation.ActivityResult okResult = new Instrumentation.ActivityResult( + Activity.RESULT_OK, new Intent()); + + intending(hasAction(equalTo(Intent.ACTION_SEND))).respondWith(okResult); + intending(hasAction(equalTo(Intent.ACTION_VIEW))).respondWith(okResult); + 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 public void testArchiveHabits() { List names = new LinkedList<>(); - Context context = InstrumentationRegistry.getTargetContext(); for(int i = 0; i < 3; i++) names.add(addHabit()); @@ -111,7 +154,7 @@ public class MainTest clickActionModeMenuItem(R.string.archive); assertHabitsDontExist(names); - openActionBarOverflowOrOptionsMenu(context); + openActionBarOverflowOrOptionsMenu(targetContext); onView(withText(R.string.show_archived)) .perform(click()); @@ -119,7 +162,7 @@ public class MainTest selectHabits(names); clickActionModeMenuItem(R.string.unarchive); - openActionBarOverflowOrOptionsMenu(context); + openActionBarOverflowOrOptionsMenu(targetContext); onView(withText(R.string.show_archived)) .perform(click()); @@ -127,6 +170,10 @@ public class MainTest deleteHabits(names); } + /** + * User opens the app, clicks the add button, types some bogus information, tries to save, + * dialog displays an error. + */ @Test public void testAddInvalidHabit() { @@ -139,6 +186,10 @@ public class MainTest onView(withId(R.id.input_name)).check(matches(isDisplayed())); } + /** + * User creates a habit, toggles a bunch of checkmarks, clicks the habit to open the statistics + * screen, scrolls down to some views, then scrolls the views backwards and forwards in time. + */ @Test public void testAddHabitAndViewStats() throws InterruptedException { @@ -161,6 +212,11 @@ public class MainTest .perform(scrollTo(), swipeRight()); } + /** + * User creates a habit, selects the habit, clicks edit button, changes some information about + * the habit, click save button, sees changes on the main window, selects habit again, + * changes color, then deletes the habit. + */ @Test public void testEditHabit() { @@ -187,6 +243,10 @@ public class MainTest deleteHabit(modifiedName); } + /** + * User creates a habit, opens statistics page, clicks button to edit history, adds some + * checkmarks, closes dialog, sees the modified history calendar. + */ @Test public void testEditHistory() { @@ -205,20 +265,82 @@ public class MainTest .perform(scrollTo(), swipeRight(), swipeLeft()); } + /** + * User opens menu, clicks settings, sees settings screen. + */ @Test public void testSettings() { - Context context = InstrumentationRegistry.getContext(); - openActionBarOverflowOrOptionsMenu(context); + openActionBarOverflowOrOptionsMenu(targetContext); onView(withText(R.string.settings)).perform(click()); } + /** + * User opens menu, clicks about, sees about screen. + */ @Test public void testAbout() { - Context context = InstrumentationRegistry.getContext(); - openActionBarOverflowOrOptionsMenu(context); + openActionBarOverflowOrOptionsMenu(targetContext); onView(withText(R.string.about)).perform(click()); onView(isRoot()).perform(swipeUp()); } + + /** + * User opens menu, clicks Help, sees website. + */ + @Test + public void testHelp() + { + openActionBarOverflowOrOptionsMenu(targetContext); + onView(withText(R.string.help)).perform(click()); + intended(hasAction(Intent.ACTION_VIEW)); + } + + /** + * User creates a habit, exports full backup, deletes the habit, restores backup, sees that the + * previously created habit has appeared back. + */ + @Test + public void testExportImportDB() + { + String name = addHabit(); + + openActionBarOverflowOrOptionsMenu(targetContext); + onView(withText(R.string.settings)).perform(click()); + + String date = DateHelper.getBackupDateFormat().format(DateHelper.getLocalTime()); + date = date.substring(0, date.length() - 2); + + onData(isPreferenceWithText("Export full backup")).perform(click()); + intended(hasAction(Intent.ACTION_SEND)); + + deleteHabit(name); + + openActionBarOverflowOrOptionsMenu(targetContext); + onView(withText(R.string.settings)).perform(click()); + onData(isPreferenceWithText("Import data")).perform(click()); + + onData(allOf(is(instanceOf(String.class)), startsWith("Backups"))) + .perform(click()); + + onData(allOf(is(instanceOf(String.class)), containsString(date))) + .perform(click()); + + selectHabit(name); + } + + /** + * User creates a habit, opens settings, clicks export as CSV, is asked what activity should + * handle the file. + */ + @Test + public void testExportCSV() + { + addHabit(); + openActionBarOverflowOrOptionsMenu(targetContext); + onView(withText(R.string.settings)).perform(click()); + onData(isPreferenceWithText("Export as CSV")).perform(click()); + intended(hasAction(Intent.ACTION_SEND)); + } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java new file mode 100644 index 000000000..6cd998515 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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 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 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()); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java new file mode 100644 index 000000000..99208a7c5 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.unit.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 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 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 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 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)); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java index 6413a566b..2fe1f8c1b 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/CheckmarkListTest.java @@ -22,13 +22,16 @@ package org.isoron.uhabits.unit.models; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; +import java.io.StringWriter; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.isoron.uhabits.models.Checkmark.CHECKED_EXPLICITLY; @@ -140,6 +143,27 @@ public class CheckmarkListTest 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) { DateHelper.setFixedLocalTime(HabitFixtures.FIXED_LOCAL_TIME + diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java index a8c706a2c..6b53fd35a 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java @@ -19,7 +19,8 @@ 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; public class HabitFixtures @@ -28,9 +29,11 @@ public class HabitFixtures public static boolean NON_DAILY_HABIT_CHECKS[] = { true, false, false, true, true, true, false, false, true, true }; - static Habit createNonDailyHabit() + public static Habit createNonDailyHabit() { Habit habit = new Habit(); + habit.name = "Wake up early"; + habit.description = "Did you wake up before 6am?"; habit.freqNum = 2; habit.freqDen = 3; habit.save(); @@ -45,16 +48,19 @@ public class HabitFixtures return habit; } - static Habit createEmptyHabit() + public static Habit createEmptyHabit() { Habit habit = new Habit(); + habit.name = "Meditate"; + habit.description = "Did you meditate this morning?"; + habit.color = ColorHelper.palette[3]; habit.freqNum = 1; habit.freqDen = 1; habit.save(); return habit; } - static void purgeHabits() + public static void purgeHabits() { for(Habit h : Habit.getAll(true)) h.cascadeDelete(); diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java index e6be6b150..82bc600bd 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitTest.java @@ -23,12 +23,15 @@ import android.graphics.Color; import android.support.test.runner.AndroidJUnit4; 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.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; +import java.io.StringWriter; import java.util.LinkedList; import java.util.List; @@ -353,4 +356,21 @@ public class HabitTest h.clearReminder(); 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)); + } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java index 173af0b51..9ca8ad4b2 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/RepetitionListTest.java @@ -22,7 +22,7 @@ package org.isoron.uhabits.unit.models; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; import org.junit.After; import org.junit.Before; diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java index 350ce4676..04821f23e 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java @@ -22,8 +22,8 @@ package org.isoron.uhabits.unit.models; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import org.isoron.helpers.ActiveAndroidHelper; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DatabaseHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Score; import org.junit.After; @@ -31,6 +31,9 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; +import java.io.StringWriter; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -130,9 +133,33 @@ public class ScoreListTest 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) { - ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command() + DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() { @Override public void execute() diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java index d666ff1df..886f41e90 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreTest.java @@ -19,29 +19,16 @@ package org.isoron.uhabits.unit.models; -import android.graphics.Color; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import org.isoron.helpers.DateHelper; -import org.isoron.uhabits.models.Habit; -import org.junit.Before; +import org.isoron.uhabits.models.Checkmark; +import org.isoron.uhabits.models.Score; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.LinkedList; -import java.util.List; - import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.core.IsNot.not; import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - -import org.isoron.uhabits.models.Score; -import org.isoron.uhabits.models.Repetition; -import org.isoron.uhabits.models.Checkmark; @RunWith(AndroidJUnit4.class) @SmallTest diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java new file mode 100644 index 000000000..a899a86f0 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.unit.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 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); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportDBTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportDBTaskTest.java new file mode 100644 index 000000000..e744b5ede --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportDBTaskTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.unit.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); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.java new file mode 100644 index 000000000..dc3f0681a --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.unit.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"); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e3e04d0ac..f72968c2e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,11 +28,11 @@ + android:maxSdkVersion="18" /> + android:maxSdkVersion="18" /> . */ -package org.isoron.helpers; +package org.isoron.uhabits; import android.app.Activity; import android.app.backup.BackupManager; @@ -25,7 +25,6 @@ import android.os.AsyncTask; import android.os.Bundle; import android.widget.Toast; -import org.isoron.uhabits.R; import org.isoron.uhabits.commands.Command; import java.util.LinkedList; diff --git a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java index 041685e6e..7a269a733 100644 --- a/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ShowHabitActivity.java @@ -30,7 +30,6 @@ import android.net.Uri; import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; -import org.isoron.helpers.ReplayableActivity; import org.isoron.uhabits.fragments.ShowHabitFragment; import org.isoron.uhabits.models.Habit; diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java b/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java new file mode 100644 index 000000000..94c574fbd --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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 + { + 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(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java b/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java index 300dcd3ea..f2d54f5a2 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/dialogs/WeekdayPickerDialog.java @@ -25,7 +25,7 @@ import android.app.DialogFragment; import android.content.DialogInterface; import android.os.Bundle; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.R; public class WeekdayPickerDialog extends DialogFragment diff --git a/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java index 342e32ec3..628f85148 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/EditHabitFragment.java @@ -32,7 +32,6 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.Button; import android.widget.ImageButton; -import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; @@ -41,9 +40,9 @@ import com.android.colorpicker.ColorPickerSwatch; import com.android.datetimepicker.time.RadialPickerLayout; import com.android.datetimepicker.time.TimePickerDialog; -import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.DateHelper; -import org.isoron.helpers.DialogHelper.OnSavedListener; +import org.isoron.uhabits.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DateHelper; +import org.isoron.uhabits.helpers.DialogHelper.OnSavedListener; import org.isoron.uhabits.R; import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.CreateHabitCommand; diff --git a/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java b/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java index 3ef39713e..dfbd959f2 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/HabitListAdapter.java @@ -27,7 +27,7 @@ import android.widget.BaseAdapter; import android.widget.LinearLayout; import android.widget.TextView; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.R; import org.isoron.uhabits.helpers.ListHabitsHelper; import org.isoron.uhabits.loaders.HabitListLoader; diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java b/app/src/main/java/org/isoron/uhabits/fragments/HabitSelectionCallback.java similarity index 79% rename from app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java rename to app/src/main/java/org/isoron/uhabits/fragments/HabitSelectionCallback.java index 47a23cf28..1e9c6b5df 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/HabitSelectionCallback.java @@ -17,36 +17,29 @@ * with this program. If not, see . */ -package org.isoron.uhabits.dialogs; +package org.isoron.uhabits.fragments; import android.app.AlertDialog; import android.content.DialogInterface; -import android.content.Intent; -import android.net.Uri; -import android.os.AsyncTask; import android.view.ActionMode; import android.view.Menu; import android.view.MenuItem; -import android.view.View; import android.widget.ProgressBar; import com.android.colorpicker.ColorPickerDialog; 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.ReplayableActivity; import org.isoron.uhabits.commands.ArchiveHabitsCommand; import org.isoron.uhabits.commands.ChangeHabitColorCommand; import org.isoron.uhabits.commands.DeleteHabitsCommand; import org.isoron.uhabits.commands.UnarchiveHabitsCommand; -import org.isoron.uhabits.fragments.EditHabitFragment; -import org.isoron.uhabits.io.CSVExporter; +import org.isoron.uhabits.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.models.Habit; -import java.io.File; import java.util.LinkedList; import java.util.List; @@ -205,12 +198,6 @@ public class HabitSelectionCallback implements ActionMode.Callback return true; } - - case R.id.action_export_csv: - { - onExportHabitsClick(selectedHabits); - return true; - } } return false; @@ -221,47 +208,4 @@ public class HabitSelectionCallback implements ActionMode.Callback { if(listener != null) listener.onActionModeDestroyed(mode); } - - private void onExportHabitsClick(final LinkedList selectedHabits) - { - new AsyncTask() - { - 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(); - } } diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java index b6ea51dd9..4185c9b6b 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java @@ -26,6 +26,7 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; +import android.support.annotation.Nullable; import android.view.ActionMode; import android.view.ContextMenu; 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.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.ReplayableActivity; +import org.isoron.uhabits.commands.Command; import org.isoron.uhabits.commands.ToggleRepetitionCommand; -import org.isoron.uhabits.dialogs.HabitSelectionCallback; -import org.isoron.uhabits.dialogs.HintManager; +import org.isoron.uhabits.dialogs.FilePickerDialog; +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.ReminderHelper; import org.isoron.uhabits.loaders.HabitListLoader; 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.LinkedList; import java.util.List; @@ -69,7 +74,8 @@ import java.util.List; public class ListHabitsFragment extends Fragment implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener, OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener, - HabitSelectionCallback.Listener + HabitSelectionCallback.Listener, ImportDataTask.Listener, ExportCSVTask.Listener, + ExportDBTask.Listener { long lastLongClick = 0; private boolean isShortToggleEnabled; @@ -224,15 +230,6 @@ public class ListHabitsFragment extends Fragment return true; } - case R.id.action_faq: - { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.setData(Uri.parse(getString(R.string.helpURL))); - startActivity(intent); - return true; - } - default: return super.onOptionsItemSelected(item); } @@ -426,4 +423,92 @@ public class ListHabitsFragment extends Fragment 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); + } + } } diff --git a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java index fb5c04f60..1d3c4abb3 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java @@ -22,8 +22,10 @@ package org.isoron.uhabits.fragments; import android.app.backup.BackupManager; import android.content.SharedPreferences; import android.os.Bundle; +import android.preference.Preference; import android.preference.PreferenceFragment; +import org.isoron.uhabits.MainActivity; import org.isoron.uhabits.R; public class SettingsFragment extends PreferenceFragment @@ -34,6 +36,25 @@ public class SettingsFragment extends PreferenceFragment { super.onCreate(savedInstanceState); 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 diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java index 1a4e38f6c..61b99cca1 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ShowHabitFragment.java @@ -37,8 +37,8 @@ import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; -import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.DialogHelper; +import org.isoron.uhabits.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.R; import org.isoron.uhabits.ShowHabitActivity; diff --git a/app/src/main/java/org/isoron/helpers/ColorHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/ColorHelper.java similarity index 95% rename from app/src/main/java/org/isoron/helpers/ColorHelper.java rename to app/src/main/java/org/isoron/uhabits/helpers/ColorHelper.java index de49d654d..e58a73745 100644 --- a/app/src/main/java/org/isoron/helpers/ColorHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/ColorHelper.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.helpers; +package org.isoron.uhabits.helpers; import android.graphics.Color; @@ -91,4 +91,9 @@ public class ColorHelper hsv[index] = newValue; return Color.HSVToColor(hsv); } + + public static String toHTML(int color) + { + return String.format("#%06X", 0xFFFFFF & color); + } } \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java new file mode 100644 index 000000000..03f66ec5a --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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); + } +} diff --git a/app/src/main/java/org/isoron/helpers/DateHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java similarity index 91% rename from app/src/main/java/org/isoron/helpers/DateHelper.java rename to app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java index f89a3ef7b..371e2ae47 100644 --- a/app/src/main/java/org/isoron/helpers/DateHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java @@ -17,13 +17,14 @@ * with this program. If not, see . */ -package org.isoron.helpers; +package org.isoron.uhabits.helpers; import android.content.Context; import android.text.format.DateFormat; import org.isoron.uhabits.R; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; @@ -96,6 +97,22 @@ public class DateHelper 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) { String dayOfMonth = Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH)); diff --git a/app/src/main/java/org/isoron/helpers/DialogHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DialogHelper.java similarity index 99% rename from app/src/main/java/org/isoron/helpers/DialogHelper.java rename to app/src/main/java/org/isoron/uhabits/helpers/DialogHelper.java index c24083a7b..9fce7035c 100644 --- a/app/src/main/java/org/isoron/helpers/DialogHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DialogHelper.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.helpers; +package org.isoron.uhabits.helpers; import android.content.Context; import android.content.SharedPreferences; diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/HintManager.java b/app/src/main/java/org/isoron/uhabits/helpers/HintManager.java similarity index 97% rename from app/src/main/java/org/isoron/uhabits/dialogs/HintManager.java rename to app/src/main/java/org/isoron/uhabits/helpers/HintManager.java index 0a3d4dead..998939ed9 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/HintManager.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/HintManager.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.dialogs; +package org.isoron.uhabits.helpers; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -27,7 +27,6 @@ import android.preference.PreferenceManager; import android.view.View; import android.widget.TextView; -import org.isoron.helpers.DateHelper; import org.isoron.uhabits.R; public class HintManager diff --git a/app/src/main/java/org/isoron/uhabits/helpers/ListHabitsHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/ListHabitsHelper.java index 846283a63..75d4b45a3 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/ListHabitsHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/ListHabitsHelper.java @@ -30,7 +30,6 @@ import android.widget.Button; import android.widget.LinearLayout; import android.widget.TextView; -import org.isoron.helpers.DateHelper; import org.isoron.uhabits.R; import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.models.Habit; diff --git a/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java index 24c2fe9e8..876c229fb 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/ReminderHelper.java @@ -28,7 +28,6 @@ import android.os.Build; import android.support.annotation.Nullable; import android.util.Log; -import org.isoron.helpers.DateHelper; import org.isoron.uhabits.HabitBroadcastReceiver; import org.isoron.uhabits.models.Habit; diff --git a/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java b/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java new file mode 100644 index 000000000..83cfddcb8 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java deleted file mode 100644 index d5690e0a1..000000000 --- a/app/src/main/java/org/isoron/uhabits/io/CSVExporter.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.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 habits; - private Context context; - private java.text.DateFormat dateFormat; - - private List generateDirs; - private List generateFilenames; - - private String basePath; - - public CSVExporter(Context context, List 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; - } - - -} diff --git a/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java new file mode 100644 index 000000000..c08a3a72f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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 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); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java new file mode 100644 index 000000000..46be626c9 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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 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); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java new file mode 100644 index 000000000..f84ad666c --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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 habits; + + private List generateDirs; + private List generateFilenames; + + private String exportDirName; + + public HabitsCSVExporter(List 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(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java new file mode 100644 index 000000000..27b7ecb15 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java new file mode 100644 index 000000000..47fc92020 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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(); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java new file mode 100644 index 000000000..f0b6b9770 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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(); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java b/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java index 2890c872d..48a736455 100644 --- a/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java +++ b/app/src/main/java/org/isoron/uhabits/loaders/HabitListLoader.java @@ -24,7 +24,7 @@ import android.os.Handler; import android.view.View; import android.widget.ProgressBar; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; import java.util.HashMap; diff --git a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java index d4fd67617..8c5f5f577 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -29,8 +29,12 @@ import com.activeandroid.Cache; import com.activeandroid.query.Delete; 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; public class CheckmarkList @@ -72,6 +76,7 @@ public class CheckmarkList public int[] getValues(long fromTimestamp, long toTimestamp) { compute(fromTimestamp, toTimestamp); + if(fromTimestamp > toTimestamp) return new int[0]; String query = "select value, timestamp from Checkmarks where " + @@ -123,6 +128,21 @@ public class CheckmarkList 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 * time. Days that already have a corresponding checkmark are skipped. @@ -229,4 +249,38 @@ public class CheckmarkList if(today != null) return today.value; 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(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/Habit.java b/app/src/main/java/org/isoron/uhabits/models/Habit.java index cc89521e4..a3c51e3fe 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -33,10 +33,13 @@ import com.activeandroid.query.From; import com.activeandroid.query.Select; import com.activeandroid.query.Update; import com.activeandroid.util.SQLiteUtils; +import com.opencsv.CSVWriter; -import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DateHelper; +import java.io.IOException; +import java.io.Writer; import java.util.List; import java.util.Locale; @@ -470,4 +473,30 @@ public class Habit extends Model reminderMin = null; 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 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(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java index 475f6e1fa..6e6d720ed 100644 --- a/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java +++ b/app/src/main/java/org/isoron/uhabits/models/RepetitionList.java @@ -29,7 +29,7 @@ import com.activeandroid.query.Delete; import com.activeandroid.query.From; import com.activeandroid.query.Select; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import java.util.Arrays; import java.util.GregorianCalendar; diff --git a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java index 1a267c301..1972dc411 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -24,14 +24,18 @@ import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.activeandroid.ActiveAndroid; import com.activeandroid.Cache; import com.activeandroid.query.Delete; import com.activeandroid.query.From; import com.activeandroid.query.Select; -import org.isoron.helpers.ActiveAndroidHelper; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DatabaseHelper; +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 { @@ -95,6 +99,19 @@ public class ScoreList .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 * 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 long beginning = from; - ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command() + DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() { @Override public void execute() @@ -278,4 +295,30 @@ public class ScoreList if(score != null) return score.getStarStatus(); 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(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/models/StreakList.java b/app/src/main/java/org/isoron/uhabits/models/StreakList.java index 5f34e0634..81c3a57ea 100644 --- a/app/src/main/java/org/isoron/uhabits/models/StreakList.java +++ b/app/src/main/java/org/isoron/uhabits/models/StreakList.java @@ -23,7 +23,7 @@ import com.activeandroid.ActiveAndroid; import com.activeandroid.query.Delete; import com.activeandroid.query.Select; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import java.util.ArrayList; import java.util.List; diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java new file mode 100644 index 000000000..2ebd05cc8 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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 +{ + public interface Listener + { + void onExportCSVFinished(@Nullable String archiveFilename); + } + + private ProgressBar progressBar; + private final List selectedHabits; + private String archiveFilename; + private ExportCSVTask.Listener listener; + + public ExportCSVTask(List 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; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java new file mode 100644 index 000000000..abc31b490 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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 +{ + 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; + } +} diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java new file mode 100644 index 000000000..baf2da75d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.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 +{ + 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; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java b/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java index 6de4cffeb..3f0950fe6 100644 --- a/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java +++ b/app/src/main/java/org/isoron/uhabits/views/CheckmarkView.java @@ -32,7 +32,7 @@ import android.text.TextPaint; import android.util.AttributeSet; import android.view.View; -import org.isoron.helpers.ColorHelper; +import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java b/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java index 9a64aa44b..aca5c4fff 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitFrequencyView.java @@ -26,8 +26,8 @@ import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; -import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; import java.text.SimpleDateFormat; diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java index d4dc33e3e..9d0211066 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitHistoryView.java @@ -29,8 +29,8 @@ import android.os.AsyncTask; import android.util.AttributeSet; import android.view.MotionEvent; -import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java index 1e4c974ee..2128f4e46 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitScoreView.java @@ -29,8 +29,8 @@ import android.graphics.RectF; import android.support.annotation.Nullable; import android.util.AttributeSet; -import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.Score; diff --git a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java index c4a5115ce..07a606017 100644 --- a/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java +++ b/app/src/main/java/org/isoron/uhabits/views/HabitStreakView.java @@ -26,8 +26,8 @@ import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; -import org.isoron.helpers.ColorHelper; -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.Streak; diff --git a/app/src/main/java/org/isoron/uhabits/views/NumberView.java b/app/src/main/java/org/isoron/uhabits/views/NumberView.java index 32cf2f3aa..a73af4a76 100644 --- a/app/src/main/java/org/isoron/uhabits/views/NumberView.java +++ b/app/src/main/java/org/isoron/uhabits/views/NumberView.java @@ -31,8 +31,8 @@ import android.text.TextPaint; import android.util.AttributeSet; import android.view.View; -import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.DialogHelper; +import org.isoron.uhabits.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DialogHelper; public class NumberView extends View { diff --git a/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java b/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java index 38b2aab61..8f617dba6 100644 --- a/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java +++ b/app/src/main/java/org/isoron/uhabits/views/RepetitionCountView.java @@ -22,8 +22,8 @@ package org.isoron.uhabits.views; import android.content.Context; import android.util.AttributeSet; -import org.isoron.helpers.DateHelper; -import org.isoron.helpers.DialogHelper; +import org.isoron.uhabits.helpers.DateHelper; +import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.models.Habit; import java.util.Calendar; diff --git a/app/src/main/java/org/isoron/uhabits/views/RingView.java b/app/src/main/java/org/isoron/uhabits/views/RingView.java index 2d82fd0f8..a8314a453 100644 --- a/app/src/main/java/org/isoron/uhabits/views/RingView.java +++ b/app/src/main/java/org/isoron/uhabits/views/RingView.java @@ -25,15 +25,14 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; -import android.os.Build; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; import android.util.AttributeSet; import android.view.View; -import org.isoron.helpers.ColorHelper; -import org.isoron.helpers.DialogHelper; +import org.isoron.uhabits.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.R; public class RingView extends View diff --git a/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java index 1610c243a..4dc96e4a1 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java @@ -34,7 +34,7 @@ import android.view.View; import android.widget.ImageView; import android.widget.RemoteViews; -import org.isoron.helpers.DialogHelper; +import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.R; import org.isoron.uhabits.models.Habit; diff --git a/app/src/main/res/menu-v21/list_habits_context.xml b/app/src/main/res/menu-v21/list_habits_context.xml index a2160ff44..b632f3c99 100644 --- a/app/src/main/res/menu-v21/list_habits_context.xml +++ b/app/src/main/res/menu-v21/list_habits_context.xml @@ -40,11 +40,6 @@ android:title="@string/unarchive" android:icon="@drawable/ic_action_unarchive_dark"/> - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3417ff2d1..394b5dd39 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -106,7 +106,7 @@ Monday to Friday Any day of the week Select days - Export data + Export as CSV Done Clear @@ -138,4 +138,14 @@ 5 times per week Custom … Help & FAQ + Failed to export data. + Failed to import data. + File not recognized. + Habits imported successfully. + Full backup successfully exported. + Import data + Export full backup + Supports full backups exported by this app, as well as files generated by Tickmate, HabitBull or Rewire. See FAQ for more information. + Generates files that can be opened by spreadsheet software such as Microsoft Excel or OpenOffice Calc. This file cannot be imported back. + Generates a file that contains all your data. This file can be imported back. \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index c1b5f5b0a..8999ef51b 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -36,7 +36,32 @@ android:entries="@array/snooze_interval_names" android:entryValues="@array/snooze_interval_values" android:key="pref_snooze_interval" - android:title="@string/pref_snooze_interval_title"/> + android:title="@string/pref_snooze_interval_title" + android:summary="%s"/> + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle index 5e38f58a7..d71fd8517 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } 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() } } + +project.ext.preDexLibs = !project.hasProperty('disablePreDex') + +subprojects { + project.plugins.whenPluginAdded { plugin -> + if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) { + project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs + } else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) { + project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs + } + } +} \ No newline at end of file diff --git a/circle.yml b/circle.yml index 3b88e723c..f10bb66de 100644 --- a/circle.yml +++ b/circle.yml @@ -5,12 +5,13 @@ checkout: test: 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 parallel: true - circle-android wait-for-boot - adb shell input keyevent 82 - - ./gradlew connectedAndroidTest + - ./gradlew -PdisablePreDex connectedAndroidTest - cp -r app/build/outputs $CIRCLE_ARTIFACTS || echo ok - cp -r app/build/reports/androidTests/connected/* $CIRCLE_TEST_REPORTS || echo ok - adb logcat -d > $CIRCLE_TEST_REPORTS/logcat.txt \ No newline at end of file