From dfe5c4954e0c942a7fe0e95e05b1240647a90c9b Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Mon, 21 Mar 2016 06:25:48 -0400 Subject: [PATCH 01/27] Refactor CSVExporter --- app/build.gradle | 1 + .../java/org/isoron/helpers/ColorHelper.java | 5 + .../java/org/isoron/helpers/DateHelper.java | 9 + .../dialogs/HabitSelectionCallback.java | 18 +- .../org/isoron/uhabits/io/CSVExporter.java | 213 ------------------ .../org/isoron/uhabits/io/HabitsExporter.java | 159 +++++++++++++ .../isoron/uhabits/models/CheckmarkList.java | 27 +++ .../java/org/isoron/uhabits/models/Habit.java | 42 ++++ .../org/isoron/uhabits/models/ScoreList.java | 29 ++- app/src/main/res/values/strings.xml | 1 + 10 files changed, 284 insertions(+), 220 deletions(-) delete mode 100644 app/src/main/java/org/isoron/uhabits/io/CSVExporter.java create mode 100644 app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java 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/main/java/org/isoron/helpers/ColorHelper.java b/app/src/main/java/org/isoron/helpers/ColorHelper.java index de49d654d..091471699 100644 --- a/app/src/main/java/org/isoron/helpers/ColorHelper.java +++ b/app/src/main/java/org/isoron/helpers/ColorHelper.java @@ -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/helpers/DateHelper.java b/app/src/main/java/org/isoron/helpers/DateHelper.java index f89a3ef7b..97bc17082 100644 --- a/app/src/main/java/org/isoron/helpers/DateHelper.java +++ b/app/src/main/java/org/isoron/helpers/DateHelper.java @@ -24,6 +24,7 @@ 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,14 @@ 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 String formatHeaderDate(GregorianCalendar day) { String dayOfMonth = Integer.toString(day.get(GregorianCalendar.DAY_OF_MONTH)); diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java b/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java index 47a23cf28..f7c615353 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java +++ b/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java @@ -42,7 +42,7 @@ 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.io.HabitsExporter; import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.models.Habit; @@ -226,7 +226,7 @@ public class HabitSelectionCallback implements ActionMode.Callback { new AsyncTask() { - String filename; + String archiveFilename; @Override protected void onPreExecute() @@ -241,15 +241,19 @@ public class HabitSelectionCallback implements ActionMode.Callback @Override protected void onPostExecute(Void aVoid) { - if(filename != null) + 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(filename))); + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(archiveFilename))); activity.startActivity(intent); } + else + { + activity.showToast(R.string.could_not_export); + } if(progressBar != null) progressBar.setVisibility(View.GONE); @@ -258,8 +262,10 @@ public class HabitSelectionCallback implements ActionMode.Callback @Override protected Void doInBackground(Void... params) { - CSVExporter exporter = new CSVExporter(activity, selectedHabits); - filename = exporter.writeArchive(); + String dirName = String.format("%s/export/", activity.getExternalCacheDir()); + HabitsExporter exporter = new HabitsExporter(selectedHabits, dirName); + archiveFilename = exporter.writeArchive(); + return null; } }.execute(); 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/HabitsExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java new file mode 100644 index 000000000..c3674220e --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java @@ -0,0 +1,159 @@ +/* + * 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.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 HabitsExporter +{ + private List habits; + + private List generateDirs; + private List generateFilenames; + + private String exportDirName; + + public HabitsExporter(List habits, String exportDirName) + { + this.habits = habits; + this.exportDirName = exportDirName; + + if(!this.exportDirName.endsWith("/")) + this.exportDirName += "/"; + + 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("%s/", 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/habits-%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() + { + String zipFilename; + + try + { + writeHabits(); + zipFilename = writeZipFile(); + cleanup(); + } + catch (IOException e) + { + e.printStackTrace(); + return null; + } + + 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/models/CheckmarkList.java b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java index d4fd67617..99448f335 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -31,6 +31,10 @@ import com.activeandroid.query.Select; import org.isoron.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 @@ -229,4 +233,27 @@ public class CheckmarkList if(today != null) return today.value; else return Checkmark.UNCHECKED; } + + public void writeCSV(Writer out) throws IOException + { + 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(); + } } 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..e93d73abc 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -20,6 +20,7 @@ package org.isoron.uhabits.models; import android.annotation.SuppressLint; +import android.graphics.Color; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -33,10 +34,17 @@ import com.activeandroid.query.From; import com.activeandroid.query.Select; import com.activeandroid.query.Update; import com.activeandroid.util.SQLiteUtils; +import com.opencsv.CSVReader; +import com.opencsv.CSVWriter; import org.isoron.helpers.ColorHelper; import org.isoron.helpers.DateHelper; +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -470,4 +478,38 @@ public class Habit extends Model reminderMin = null; reminderDays = DateHelper.ALL_WEEK_DAYS; } + + public static void writeCSV(List habits, Writer out) throws IOException + { + CSVWriter csv = new CSVWriter(out); + + for(Habit habit : habits) + { + String[] cols = { habit.name, habit.description, Integer.toString(habit.freqNum), + Integer.toString(habit.freqDen), ColorHelper.toHTML(habit.color) }; + csv.writeAll(Collections.singletonList(cols)); + } + + csv.close(); + } + + public List parseCSV(Reader in) + { + CSVReader csv = new CSVReader(in); + List habits = new LinkedList<>(); + + for(String cols[] : csv) + { + Habit habit = new Habit(); + + habit.name = cols[0]; + habit.description = cols[1]; + habit.freqNum = Integer.parseInt(cols[2]); + habit.freqDen = Integer.parseInt(cols[3]); + habit.color = Color.parseColor(cols[4]); + habits.add(habit); + } + + return habits; + } } 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..23c10ec24 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -24,7 +24,6 @@ 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; @@ -33,6 +32,11 @@ import com.activeandroid.query.Select; import org.isoron.helpers.ActiveAndroidHelper; import org.isoron.helpers.DateHelper; +import java.io.IOException; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Date; + public class ScoreList { @NonNull @@ -278,4 +282,27 @@ public class ScoreList if(score != null) return score.getStarStatus(); else return Score.EMPTY_STAR; } + + public void writeCSV(Writer out) throws IOException + { + 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("%.2f", ((float) cursor.getInt(1)) / Score.MAX_VALUE); + out.write(String.format("%s,%s\n", timestamp, score)); + + } while(cursor.moveToNext()); + + cursor.close(); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3417ff2d1..b1933b4b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -138,4 +138,5 @@ 5 times per week Custom … Help & FAQ + Failed to export data. \ No newline at end of file From 581197be035dd870187c471672c3e517a3ba105d Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Mon, 21 Mar 2016 14:00:51 -0400 Subject: [PATCH 02/27] Import data from Tickmate and Rewire Closes #36, closes #41 --- app/src/main/AndroidManifest.xml | 6 +- .../java/org/isoron/uhabits/MainActivity.java | 41 ++++ .../uhabits/dialogs/FilePickerDialog.java | 175 ++++++++++++++++ .../fragments/ImportHabitsAsyncTask.java | 109 ++++++++++ .../uhabits/fragments/ListHabitsFragment.java | 42 +++- .../isoron/uhabits/io/AbstractImporter.java | 48 +++++ .../isoron/uhabits/io/GenericImporter.java | 56 ++++++ .../isoron/uhabits/io/RewireDBImporter.java | 190 ++++++++++++++++++ .../isoron/uhabits/io/TickmateDBImporter.java | 130 ++++++++++++ app/src/main/res/menu/list_habits_menu.xml | 6 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/preferences.xml | 19 ++ 12 files changed, 820 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java create mode 100644 app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java create mode 100644 app/src/main/java/org/isoron/uhabits/io/AbstractImporter.java create mode 100644 app/src/main/java/org/isoron/uhabits/io/GenericImporter.java create mode 100644 app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java create mode 100644 app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e3e04d0ac..4f0cb89fc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,12 +27,10 @@ + android:name="android.permission.READ_EXTERNAL_STORAGE"/> + android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + * + * 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 fileListener; + + 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 (fileListener != null) fileListener.onFileSelected(file); + dialog.dismiss(); + } + } + + public void show() + { + dialog.show(); + } + + public void setFileListener(OnFileSelectedListener fileListener) + { + this.fileListener = fileListener; + } + + 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/fragments/ImportHabitsAsyncTask.java b/app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java new file mode 100644 index 000000000..675109d1f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java @@ -0,0 +1,109 @@ +/* + * 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.fragments; + +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; +import java.io.IOException; + +public class ImportHabitsAsyncTask 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 ImportHabitsAsyncTask(@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 (IOException e) + { + result = FAILED; + e.printStackTrace(); + } + + return null; + } +} \ No newline at end of file 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..58e8a7170 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.os.Environment; import android.preference.PreferenceManager; import android.view.ActionMode; import android.view.ContextMenu; @@ -55,6 +56,7 @@ import org.isoron.helpers.DialogHelper.OnSavedListener; import org.isoron.helpers.ReplayableActivity; import org.isoron.uhabits.R; import org.isoron.uhabits.commands.ToggleRepetitionCommand; +import org.isoron.uhabits.dialogs.FilePickerDialog; import org.isoron.uhabits.dialogs.HabitSelectionCallback; import org.isoron.uhabits.dialogs.HintManager; import org.isoron.uhabits.helpers.ListHabitsHelper; @@ -62,6 +64,7 @@ import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.models.Habit; +import java.io.File; import java.util.Date; import java.util.LinkedList; import java.util.List; @@ -69,7 +72,7 @@ import java.util.List; public class ListHabitsFragment extends Fragment implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener, OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener, - HabitSelectionCallback.Listener + HabitSelectionCallback.Listener, ImportHabitsAsyncTask.Listener { long lastLongClick = 0; private boolean isShortToggleEnabled; @@ -426,4 +429,41 @@ public class ListHabitsFragment extends Fragment selectItem(position); } } + + public void showImportDialog() + { + File dir = Environment.getExternalStorageDirectory(); + FilePickerDialog picker = new FilePickerDialog(activity, dir); + picker.setFileListener(new FilePickerDialog.OnFileSelectedListener() + { + @Override + public void onFileSelected(File file) + { + ImportHabitsAsyncTask task = new ImportHabitsAsyncTask(file, progressBar); + task.setListener(ListHabitsFragment.this); + task.execute(); + } + }); + picker.show(); + } + + @Override + public void onImportFinished(int result) + { + switch (result) + { + case ImportHabitsAsyncTask.SUCCESS: + loader.updateAllHabits(true); + activity.showToast(R.string.habits_imported); + break; + + case ImportHabitsAsyncTask.NOT_RECOGNIZED: + activity.showToast(R.string.file_not_recognized); + break; + + default: + activity.showToast(R.string.could_not_import); + break; + } + } } 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/GenericImporter.java b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java new file mode 100644 index 000000000..38aa6eab8 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java @@ -0,0 +1,56 @@ +/* + * 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 RewireDBImporter()); + importers.add(new TickmateDBImporter()); + } + + @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/RewireDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java new file mode 100644 index 000000000..41a7d442b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java @@ -0,0 +1,190 @@ +/* + * 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.helpers.ActiveAndroidHelper; +import org.isoron.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(); + return result; + } + + @Override + public void importHabitsFromFile(@NonNull File file) throws IOException + { + final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, + SQLiteDatabase.OPEN_READONLY); + + ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command() + { + @Override + public void execute() + { + createHabits(db); + } + }); + } + + 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..7d471c7f6 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java @@ -0,0 +1,130 @@ +/* + * 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.helpers.ActiveAndroidHelper; +import org.isoron.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(); + return result; + } + + @Override + public void importHabitsFromFile(@NonNull File file) throws IOException + { + final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, + SQLiteDatabase.OPEN_READONLY); + + ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command() + { + @Override + public void execute() + { + createHabits(db); + } + }); + } + + 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/res/menu/list_habits_menu.xml b/app/src/main/res/menu/list_habits_menu.xml index 320dc357a..06a5c3fe8 100644 --- a/app/src/main/res/menu/list_habits_menu.xml +++ b/app/src/main/res/menu/list_habits_menu.xml @@ -28,6 +28,12 @@ android:enabled="true" android:title="@string/show_archived"/> + + Custom … Help & FAQ Failed to export data. + Failed to import habits from file. + File type not recognized. + Habits imported successfully. \ 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..82a733f67 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -40,6 +40,25 @@ + + + + + + + + + + + + From aa5df56437803000600eb1b067763eed7f61031b Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Mon, 21 Mar 2016 20:02:05 -0400 Subject: [PATCH 03/27] Use stable version of gradle --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5e38f58a7..f4d8c542e 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:1.5.0' } } From 49a80faca3e0cb4fac64afc62236fe9270a0fabe Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Tue, 22 Mar 2016 21:32:06 -0400 Subject: [PATCH 04/27] Trap all exceptions --- .../org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java b/app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java index 675109d1f..0f7d18682 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java @@ -98,7 +98,7 @@ public class ImportHabitsAsyncTask extends AsyncTask result = NOT_RECOGNIZED; } } - catch (IOException e) + catch (Exception e) { result = FAILED; e.printStackTrace(); From 1db2f69f05bc33960012e565d73a1c7a7749dc1a Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Tue, 22 Mar 2016 21:32:37 -0400 Subject: [PATCH 05/27] Import data from HabitBull Closes #44 --- .../isoron/uhabits/io/GenericImporter.java | 1 + .../uhabits/io/HabitBullCSVImporter.java | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java diff --git a/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java index 38aa6eab8..1ecc4c1fa 100644 --- a/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java @@ -35,6 +35,7 @@ public class GenericImporter extends AbstractImporter importers = new LinkedList<>(); importers.add(new RewireDBImporter()); importers.add(new TickmateDBImporter()); + importers.add(new HabitBullCSVImporter()); } @Override 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..efa4cd933 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java @@ -0,0 +1,90 @@ +/* + * 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 android.util.Log; + +import com.opencsv.CSVReader; + +import org.isoron.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 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); + } + } +} From 2d675ed9b05a4ee0d77a01e3035365b8c6dfda4b Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Tue, 22 Mar 2016 21:53:58 -0400 Subject: [PATCH 06/27] Use DB transaction to perform import --- .../uhabits/io/HabitBullCSVImporter.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java index efa4cd933..ee583e4bc 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java @@ -20,8 +20,8 @@ package org.isoron.uhabits.io; import android.support.annotation.NonNull; -import android.util.Log; +import com.activeandroid.ActiveAndroid; import com.opencsv.CSVReader; import org.isoron.helpers.DateHelper; @@ -46,7 +46,21 @@ public class HabitBullCSVImporter extends AbstractImporter } @Override - public void importHabitsFromFile(@NonNull File file) throws IOException + 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<>(); From e6b7b8b590894fa2119a0c187a480429da3add88 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Tue, 22 Mar 2016 22:50:20 -0400 Subject: [PATCH 07/27] Export all habits as CSV from the settings menu Closes #28 --- .../java/org/isoron/uhabits/MainActivity.java | 30 +++++--- .../dialogs/HabitSelectionCallback.java | 53 +------------- .../uhabits/fragments/ExportHabitsTask.java | 72 +++++++++++++++++++ .../uhabits/fragments/ListHabitsFragment.java | 5 ++ .../uhabits/fragments/SettingsFragment.java | 20 ++++++ .../org/isoron/uhabits/io/HabitsExporter.java | 10 +-- .../java/org/isoron/uhabits/models/Habit.java | 5 +- app/src/main/res/menu/list_habits_menu.xml | 6 -- app/src/main/res/values/strings.xml | 7 +- app/src/main/res/xml/preferences.xml | 25 ++++--- 10 files changed, 148 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/fragments/ExportHabitsTask.java diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index 7dd2ed5be..1888d3773 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -31,7 +31,6 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import android.os.Environment; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; @@ -43,7 +42,6 @@ import android.view.MenuItem; import org.isoron.helpers.DateHelper; import org.isoron.helpers.DialogHelper; import org.isoron.helpers.ReplayableActivity; -import org.isoron.uhabits.dialogs.FilePickerDialog; import org.isoron.uhabits.fragments.ListHabitsFragment; import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.models.Habit; @@ -53,8 +51,6 @@ import org.isoron.uhabits.widgets.HistoryWidgetProvider; import org.isoron.uhabits.widgets.ScoreWidgetProvider; import org.isoron.uhabits.widgets.StreakWidgetProvider; -import java.io.File; - public class MainActivity extends ReplayableActivity implements ListHabitsFragment.OnHabitClickListener { @@ -65,6 +61,9 @@ public class MainActivity extends ReplayableActivity public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH"; + public static final int RESULT_IMPORT_DATA = 1; + public static final int RESULT_EXPORT_ALL_AS_CSV = 2; + @Override protected void onCreate(Bundle savedInstanceState) { @@ -129,16 +128,10 @@ public class MainActivity extends ReplayableActivity { switch (item.getItemId()) { - case R.id.action_import: - { - onActionImportClicked(); - return true; - } - case R.id.action_settings: { Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); + startActivityForResult(intent, 0); return true; } @@ -154,6 +147,21 @@ public class MainActivity extends ReplayableActivity } } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + switch (resultCode) + { + case RESULT_IMPORT_DATA: + onActionImportClicked(); + break; + + case RESULT_EXPORT_ALL_AS_CSV: + listHabitsFragment.exportAllHabits(); + break; + } + } + private void onActionImportClicked() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != diff --git a/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java b/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java index f7c615353..8a577981b 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java +++ b/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java @@ -21,13 +21,9 @@ package org.isoron.uhabits.dialogs; 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; @@ -42,11 +38,10 @@ 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.HabitsExporter; +import org.isoron.uhabits.fragments.ExportHabitsTask; import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.models.Habit; -import java.io.File; import java.util.LinkedList; import java.util.List; @@ -224,50 +219,6 @@ public class HabitSelectionCallback implements ActionMode.Callback private void onExportHabitsClick(final LinkedList selectedHabits) { - new AsyncTask() - { - String archiveFilename; - - @Override - protected void onPreExecute() - { - if(progressBar != null) - { - progressBar.setIndeterminate(true); - progressBar.setVisibility(View.VISIBLE); - } - } - - @Override - protected void onPostExecute(Void aVoid) - { - 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); - } - - if(progressBar != null) - progressBar.setVisibility(View.GONE); - } - - @Override - protected Void doInBackground(Void... params) - { - String dirName = String.format("%s/export/", activity.getExternalCacheDir()); - HabitsExporter exporter = new HabitsExporter(selectedHabits, dirName); - archiveFilename = exporter.writeArchive(); - - return null; - } - }.execute(); + new ExportHabitsTask(activity, selectedHabits, progressBar).execute(); } } diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ExportHabitsTask.java b/app/src/main/java/org/isoron/uhabits/fragments/ExportHabitsTask.java new file mode 100644 index 000000000..4431fcc3d --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/fragments/ExportHabitsTask.java @@ -0,0 +1,72 @@ +package org.isoron.uhabits.fragments; + +import android.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.view.View; +import android.widget.ProgressBar; + +import org.isoron.helpers.ReplayableActivity; +import org.isoron.uhabits.R; +import org.isoron.uhabits.io.HabitsExporter; +import org.isoron.uhabits.models.Habit; + +import java.io.File; +import java.util.List; + +public class ExportHabitsTask extends AsyncTask +{ + private final ReplayableActivity activity; + private ProgressBar progressBar; + private final List selectedHabits; + String archiveFilename; + + public ExportHabitsTask(ReplayableActivity activity, List selectedHabits, + ProgressBar progressBar) + { + this.selectedHabits = selectedHabits; + this.progressBar = progressBar; + this.activity = activity; + } + + @Override + protected void onPreExecute() + { + if(progressBar != null) + { + progressBar.setIndeterminate(true); + progressBar.setVisibility(View.VISIBLE); + } + } + + @Override + protected void onPostExecute(Void aVoid) + { + 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); + } + + if(progressBar != null) + progressBar.setVisibility(View.GONE); + } + + @Override + protected Void doInBackground(Void... params) + { + String dirName = String.format("%s/export/", activity.getExternalCacheDir()); + HabitsExporter exporter = new HabitsExporter(selectedHabits, dirName); + archiveFilename = exporter.writeArchive(); + + return null; + } +} 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 58e8a7170..05c95cbc4 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java @@ -466,4 +466,9 @@ public class ListHabitsFragment extends Fragment break; } } + + public void exportAllHabits() + { + new ExportHabitsTask(activity, Habit.getAll(true), progressBar).execute(); + } } 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..20b5ce28d 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,24 @@ public class SettingsFragment extends PreferenceFragment { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); + + setResultOnPreferenceClick("importData", MainActivity.RESULT_IMPORT_DATA); + setResultOnPreferenceClick("exportCSV", MainActivity.RESULT_EXPORT_ALL_AS_CSV); + } + + 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/io/HabitsExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java index c3674220e..32aad256d 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java @@ -58,7 +58,7 @@ public class HabitsExporter private void writeHabits() throws IOException { - String filename = "habits.csv"; + String filename = "Habits.csv"; new File(exportDirName).mkdirs(); FileWriter out = new FileWriter(exportDirName + filename); generateFilenames.add(filename); @@ -67,7 +67,7 @@ public class HabitsExporter for(Habit h : habits) { - String habitDirName = String.format("%s/", h.name); + String habitDirName = String.format("%03d %s/", h.position, h.name); new File(exportDirName + habitDirName).mkdirs(); generateDirs.add(habitDirName); @@ -78,7 +78,7 @@ public class HabitsExporter private void writeScores(String habitDirName, ScoreList scores) throws IOException { - String path = habitDirName + "scores.csv"; + String path = habitDirName + "Scores.csv"; FileWriter out = new FileWriter(exportDirName + path); generateFilenames.add(path); scores.writeCSV(out); @@ -87,7 +87,7 @@ public class HabitsExporter private void writeCheckmarks(String habitDirName, CheckmarkList checkmarks) throws IOException { - String filename = habitDirName + "checkmarks.csv"; + String filename = habitDirName + "Checkmarks.csv"; FileWriter out = new FileWriter(exportDirName + filename); generateFilenames.add(filename); checkmarks.writeCSV(out); @@ -98,7 +98,7 @@ public class HabitsExporter { SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); String date = dateFormat.format(DateHelper.getStartOfToday()); - String zipFilename = String.format("%s/habits-%s.zip", exportDirName, date); + String zipFilename = String.format("%s/Loop-Habits-%s.zip", exportDirName, date); FileOutputStream fos = new FileOutputStream(zipFilename); ZipOutputStream zos = new ZipOutputStream(fos); 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 e93d73abc..b40b7a012 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -481,13 +481,16 @@ public class Habit extends Model public static void writeCSV(List habits, Writer out) throws IOException { + String header[] = { "Name", "Description", "FrequencyNumerator", "FrequencyDenominator", "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.writeAll(Collections.singletonList(cols)); + csv.writeNext(cols, false); } csv.close(); diff --git a/app/src/main/res/menu/list_habits_menu.xml b/app/src/main/res/menu/list_habits_menu.xml index 06a5c3fe8..320dc357a 100644 --- a/app/src/main/res/menu/list_habits_menu.xml +++ b/app/src/main/res/menu/list_habits_menu.xml @@ -28,12 +28,6 @@ android:enabled="true" android:title="@string/show_archived"/> - - Monday to Friday Any day of the week Select days - Export data + Export as CSV Done Clear @@ -142,4 +142,9 @@ Failed to import habits from file. File type not recognized. Habits imported successfully. + Supports files exported by Loop, Tickmate, HabitBull or Rewire. See FAQ for more information. + Import data + This archive contains files that can be opened by spreadsheet software such as Microsoft Excel or OpenOffice Calc, but cannot be imported back. + Export full backup + This file contains all your data and 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 82a733f67..b1bbf36f6 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -36,7 +36,8 @@ 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"/> @@ -44,17 +45,21 @@ android:key="pref_key_links" android:title="Database"> - - + - - + + + + From c9793df7c7c2c34621a3df8ba550babc6f512c5f Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Wed, 23 Mar 2016 17:20:26 -0400 Subject: [PATCH 08/27] Reorganize files --- .../unit/models/CheckmarkListTest.java | 2 +- .../uhabits/unit/models/HabitFixtures.java | 2 +- .../isoron/uhabits/unit/models/HabitTest.java | 2 +- .../unit/models/RepetitionListTest.java | 2 +- .../uhabits/unit/models/ScoreListTest.java | 6 ++-- .../isoron/uhabits/unit/models/ScoreTest.java | 8 ----- .../org/isoron/uhabits/AboutActivity.java | 2 +- .../uhabits/HabitBroadcastReceiver.java | 2 +- .../java/org/isoron/uhabits/MainActivity.java | 5 ++- .../ReplayableActivity.java | 3 +- .../org/isoron/uhabits/ShowHabitActivity.java | 1 - .../uhabits/dialogs/WeekdayPickerDialog.java | 2 +- .../uhabits/fragments/EditHabitFragment.java | 7 ++-- .../uhabits/fragments/HabitListAdapter.java | 2 +- .../HabitSelectionCallback.java | 13 ++++---- .../uhabits/fragments/ListHabitsFragment.java | 23 ++++++------- .../uhabits/fragments/ShowHabitFragment.java | 4 +-- .../{ => uhabits}/helpers/ColorHelper.java | 2 +- .../helpers/DatabaseHelper.java} | 8 ++--- .../{ => uhabits}/helpers/DateHelper.java | 2 +- .../{ => uhabits}/helpers/DialogHelper.java | 2 +- .../{dialogs => helpers}/HintManager.java | 3 +- .../uhabits/helpers/ListHabitsHelper.java | 1 - .../uhabits/helpers/ReminderHelper.java | 1 - .../uhabits/io/HabitBullCSVImporter.java | 2 +- ...tsExporter.java => HabitsCSVExporter.java} | 6 ++-- .../isoron/uhabits/io/RewireDBImporter.java | 6 ++-- .../isoron/uhabits/io/TickmateDBImporter.java | 6 ++-- .../uhabits/loaders/HabitListLoader.java | 2 +- .../isoron/uhabits/models/CheckmarkList.java | 2 +- .../java/org/isoron/uhabits/models/Habit.java | 5 ++- .../isoron/uhabits/models/RepetitionList.java | 2 +- .../org/isoron/uhabits/models/ScoreList.java | 6 ++-- .../org/isoron/uhabits/models/StreakList.java | 2 +- .../ExportCSVTask.java} | 33 +++++++++++++++---- .../ImportDataTask.java} | 7 ++-- .../isoron/uhabits/views/CheckmarkView.java | 2 +- .../uhabits/views/HabitFrequencyView.java | 4 +-- .../uhabits/views/HabitHistoryView.java | 4 +-- .../isoron/uhabits/views/HabitScoreView.java | 4 +-- .../isoron/uhabits/views/HabitStreakView.java | 4 +-- .../org/isoron/uhabits/views/NumberView.java | 4 +-- .../uhabits/views/RepetitionCountView.java | 4 +-- .../org/isoron/uhabits/views/RingView.java | 5 ++- .../uhabits/widgets/BaseWidgetProvider.java | 2 +- 45 files changed, 107 insertions(+), 110 deletions(-) rename app/src/main/java/org/isoron/{helpers => uhabits}/ReplayableActivity.java (98%) rename app/src/main/java/org/isoron/uhabits/{dialogs => fragments}/HabitSelectionCallback.java (95%) rename app/src/main/java/org/isoron/{ => uhabits}/helpers/ColorHelper.java (98%) rename app/src/main/java/org/isoron/{helpers/ActiveAndroidHelper.java => uhabits/helpers/DatabaseHelper.java} (75%) rename app/src/main/java/org/isoron/{ => uhabits}/helpers/DateHelper.java (99%) rename app/src/main/java/org/isoron/{ => uhabits}/helpers/DialogHelper.java (99%) rename app/src/main/java/org/isoron/uhabits/{dialogs => helpers}/HintManager.java (97%) rename app/src/main/java/org/isoron/uhabits/io/{HabitsExporter.java => HabitsCSVExporter.java} (96%) rename app/src/main/java/org/isoron/uhabits/{fragments/ExportHabitsTask.java => tasks/ExportCSVTask.java} (57%) rename app/src/main/java/org/isoron/uhabits/{fragments/ImportHabitsAsyncTask.java => tasks/ImportDataTask.java} (91%) 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..0485607fb 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,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/HabitFixtures.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java index a8c706a2c..bb2559cb8 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,7 @@ package org.isoron.uhabits.unit.models; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; public class HabitFixtures 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..c7420aa40 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,7 +23,7 @@ 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.helpers.DateHelper; import org.isoron.uhabits.models.Habit; import org.junit.Before; import org.junit.Test; 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..bc094139c 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; @@ -132,7 +132,7 @@ public class ScoreListTest 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..cfad09f37 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,19 +19,12 @@ 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.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; @@ -40,7 +33,6 @@ 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) diff --git a/app/src/main/java/org/isoron/uhabits/AboutActivity.java b/app/src/main/java/org/isoron/uhabits/AboutActivity.java index 8952bc460..3f84528e1 100644 --- a/app/src/main/java/org/isoron/uhabits/AboutActivity.java +++ b/app/src/main/java/org/isoron/uhabits/AboutActivity.java @@ -28,7 +28,7 @@ import android.os.Bundle; import android.view.View; import android.widget.TextView; -import org.isoron.helpers.ColorHelper; +import org.isoron.uhabits.helpers.ColorHelper; public class AboutActivity extends Activity implements View.OnClickListener { diff --git a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java index 685fd8bcc..501b00454 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java +++ b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java @@ -36,7 +36,7 @@ import android.preference.PreferenceManager; import android.support.v4.app.NotificationCompat; import android.support.v4.content.LocalBroadcastManager; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.models.Habit; diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index 1888d3773..95b7477b6 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -39,9 +39,8 @@ import android.support.v4.content.LocalBroadcastManager; import android.view.Menu; import android.view.MenuItem; -import org.isoron.helpers.DateHelper; -import org.isoron.helpers.DialogHelper; -import org.isoron.helpers.ReplayableActivity; +import org.isoron.uhabits.helpers.DateHelper; +import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.fragments.ListHabitsFragment; import org.isoron.uhabits.helpers.ReminderHelper; import org.isoron.uhabits.models.Habit; diff --git a/app/src/main/java/org/isoron/helpers/ReplayableActivity.java b/app/src/main/java/org/isoron/uhabits/ReplayableActivity.java similarity index 98% rename from app/src/main/java/org/isoron/helpers/ReplayableActivity.java rename to app/src/main/java/org/isoron/uhabits/ReplayableActivity.java index 8f35b4bc7..e95e8441b 100644 --- a/app/src/main/java/org/isoron/helpers/ReplayableActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ReplayableActivity.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -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/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 95% 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 8a577981b..899f2f231 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/HabitSelectionCallback.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/HabitSelectionCallback.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.dialogs; +package org.isoron.uhabits.fragments; import android.app.AlertDialog; import android.content.DialogInterface; @@ -29,16 +29,15 @@ 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.helpers.ColorHelper; +import org.isoron.uhabits.helpers.DialogHelper; +import org.isoron.uhabits.ReplayableActivity; import org.isoron.uhabits.R; 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.fragments.ExportHabitsTask; +import org.isoron.uhabits.tasks.ExportCSVTask; import org.isoron.uhabits.loaders.HabitListLoader; import org.isoron.uhabits.models.Habit; @@ -219,6 +218,6 @@ public class HabitSelectionCallback implements ActionMode.Callback private void onExportHabitsClick(final LinkedList selectedHabits) { - new ExportHabitsTask(activity, selectedHabits, progressBar).execute(); + new ExportCSVTask(activity, selectedHabits, progressBar).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 05c95cbc4..c5696b024 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java @@ -50,19 +50,20 @@ 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.helpers.DateHelper; +import org.isoron.uhabits.helpers.DialogHelper; +import org.isoron.uhabits.helpers.DialogHelper.OnSavedListener; +import org.isoron.uhabits.ReplayableActivity; import org.isoron.uhabits.R; import org.isoron.uhabits.commands.ToggleRepetitionCommand; import org.isoron.uhabits.dialogs.FilePickerDialog; -import org.isoron.uhabits.dialogs.HabitSelectionCallback; -import org.isoron.uhabits.dialogs.HintManager; +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.ImportDataTask; import java.io.File; import java.util.Date; @@ -72,7 +73,7 @@ import java.util.List; public class ListHabitsFragment extends Fragment implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener, OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener, - HabitSelectionCallback.Listener, ImportHabitsAsyncTask.Listener + HabitSelectionCallback.Listener, ImportDataTask.Listener { long lastLongClick = 0; private boolean isShortToggleEnabled; @@ -439,7 +440,7 @@ public class ListHabitsFragment extends Fragment @Override public void onFileSelected(File file) { - ImportHabitsAsyncTask task = new ImportHabitsAsyncTask(file, progressBar); + ImportDataTask task = new ImportDataTask(file, progressBar); task.setListener(ListHabitsFragment.this); task.execute(); } @@ -452,12 +453,12 @@ public class ListHabitsFragment extends Fragment { switch (result) { - case ImportHabitsAsyncTask.SUCCESS: + case ImportDataTask.SUCCESS: loader.updateAllHabits(true); activity.showToast(R.string.habits_imported); break; - case ImportHabitsAsyncTask.NOT_RECOGNIZED: + case ImportDataTask.NOT_RECOGNIZED: activity.showToast(R.string.file_not_recognized); break; @@ -469,6 +470,6 @@ public class ListHabitsFragment extends Fragment public void exportAllHabits() { - new ExportHabitsTask(activity, Habit.getAll(true), progressBar).execute(); + new ExportCSVTask(activity, Habit.getAll(true), progressBar).execute(); } } 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 98% rename from app/src/main/java/org/isoron/helpers/ColorHelper.java rename to app/src/main/java/org/isoron/uhabits/helpers/ColorHelper.java index 091471699..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; diff --git a/app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java similarity index 75% rename from app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java rename to app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index 8e89bb7e8..2891eeb5d 100644 --- a/app/src/main/java/org/isoron/helpers/ActiveAndroidHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -1,8 +1,8 @@ -package org.isoron.helpers; +package org.isoron.uhabits.helpers; import com.activeandroid.ActiveAndroid; -public class ActiveAndroidHelper +public class DatabaseHelper { public interface Command { @@ -17,10 +17,6 @@ public class ActiveAndroidHelper command.execute(); ActiveAndroid.setTransactionSuccessful(); } - catch (RuntimeException e) - { - throw e; - } finally { ActiveAndroid.endTransaction(); 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 99% rename from app/src/main/java/org/isoron/helpers/DateHelper.java rename to app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java index 97bc17082..8a05c048b 100644 --- a/app/src/main/java/org/isoron/helpers/DateHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.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.text.format.DateFormat; 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/HabitBullCSVImporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java index ee583e4bc..46be626c9 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitBullCSVImporter.java @@ -24,7 +24,7 @@ import android.support.annotation.NonNull; import com.activeandroid.ActiveAndroid; import com.opencsv.CSVReader; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; import java.io.BufferedReader; diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java similarity index 96% rename from app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java rename to app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java index 32aad256d..0ac07ec4a 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitsExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java @@ -19,7 +19,7 @@ package org.isoron.uhabits.io; -import org.isoron.helpers.DateHelper; +import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.CheckmarkList; import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.ScoreList; @@ -35,7 +35,7 @@ import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -public class HabitsExporter +public class HabitsCSVExporter { private List habits; @@ -44,7 +44,7 @@ public class HabitsExporter private String exportDirName; - public HabitsExporter(List habits, String exportDirName) + public HabitsCSVExporter(List habits, String exportDirName) { this.habits = habits; this.exportDirName = exportDirName; diff --git a/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java index 41a7d442b..66b89690c 100644 --- a/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java @@ -23,8 +23,8 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; -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 java.io.File; @@ -56,7 +56,7 @@ public class RewireDBImporter extends AbstractImporter final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, SQLiteDatabase.OPEN_READONLY); - ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command() + DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() { @Override public void execute() diff --git a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java index 7d471c7f6..be14ca2b4 100644 --- a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java @@ -23,8 +23,8 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; -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 java.io.File; @@ -56,7 +56,7 @@ public class TickmateDBImporter extends AbstractImporter final SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getPath(), null, SQLiteDatabase.OPEN_READONLY); - ActiveAndroidHelper.executeAsTransaction(new ActiveAndroidHelper.Command() + DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() { @Override public void execute() 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 99448f335..c744a226e 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -29,7 +29,7 @@ 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; 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 b40b7a012..cd8b6a7ef 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -37,13 +37,12 @@ import com.activeandroid.util.SQLiteUtils; import com.opencsv.CSVReader; 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.Reader; import java.io.Writer; -import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Locale; 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 23c10ec24..57f4e42a0 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -29,8 +29,8 @@ 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; @@ -126,7 +126,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() 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/fragments/ExportHabitsTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java similarity index 57% rename from app/src/main/java/org/isoron/uhabits/fragments/ExportHabitsTask.java rename to app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java index 4431fcc3d..526e3ff57 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ExportHabitsTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java @@ -1,4 +1,23 @@ -package org.isoron.uhabits.fragments; +/* + * 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.content.Intent; import android.net.Uri; @@ -6,23 +25,23 @@ import android.os.AsyncTask; import android.view.View; import android.widget.ProgressBar; -import org.isoron.helpers.ReplayableActivity; +import org.isoron.uhabits.ReplayableActivity; import org.isoron.uhabits.R; -import org.isoron.uhabits.io.HabitsExporter; +import org.isoron.uhabits.io.HabitsCSVExporter; import org.isoron.uhabits.models.Habit; import java.io.File; import java.util.List; -public class ExportHabitsTask extends AsyncTask +public class ExportCSVTask extends AsyncTask { private final ReplayableActivity activity; private ProgressBar progressBar; private final List selectedHabits; String archiveFilename; - public ExportHabitsTask(ReplayableActivity activity, List selectedHabits, - ProgressBar progressBar) + public ExportCSVTask(ReplayableActivity activity, List selectedHabits, + ProgressBar progressBar) { this.selectedHabits = selectedHabits; this.progressBar = progressBar; @@ -64,7 +83,7 @@ public class ExportHabitsTask extends AsyncTask protected Void doInBackground(Void... params) { String dirName = String.format("%s/export/", activity.getExternalCacheDir()); - HabitsExporter exporter = new HabitsExporter(selectedHabits, dirName); + HabitsCSVExporter exporter = new HabitsCSVExporter(selectedHabits, dirName); archiveFilename = exporter.writeArchive(); return null; diff --git a/app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java similarity index 91% rename from app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java rename to app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java index 0f7d18682..baf2da75d 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ImportHabitsAsyncTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ImportDataTask.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.fragments; +package org.isoron.uhabits.tasks; import android.os.AsyncTask; import android.support.annotation.NonNull; @@ -28,9 +28,8 @@ import android.widget.ProgressBar; import org.isoron.uhabits.io.GenericImporter; import java.io.File; -import java.io.IOException; -public class ImportHabitsAsyncTask extends AsyncTask +public class ImportDataTask extends AsyncTask { public static final int SUCCESS = 1; public static final int NOT_RECOGNIZED = 2; @@ -52,7 +51,7 @@ public class ImportHabitsAsyncTask extends AsyncTask int result; - public ImportHabitsAsyncTask(@NonNull File file, @Nullable ProgressBar progressBar) + public ImportDataTask(@NonNull File file, @Nullable ProgressBar progressBar) { this.file = file; this.progressBar = progressBar; 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; From e8bbae8ef91b9dc6a22986f3d9055ea2dc424764 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Wed, 23 Mar 2016 18:01:25 -0400 Subject: [PATCH 09/27] Allow user to export a full copy of the database --- .../org/isoron/uhabits/HabitsApplication.java | 12 +-- .../java/org/isoron/uhabits/MainActivity.java | 9 +- .../uhabits/fragments/ListHabitsFragment.java | 6 ++ .../uhabits/fragments/SettingsFragment.java | 3 +- .../uhabits/helpers/DatabaseHelper.java | 50 ++++++++++ .../isoron/uhabits/io/HabitsCSVExporter.java | 2 +- .../isoron/uhabits/tasks/ExportDBTask.java | 94 +++++++++++++++++++ app/src/main/res/values/strings.xml | 7 +- app/src/main/res/xml/preferences.xml | 1 + 9 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java diff --git a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java index a7573d946..4e4ff918e 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -24,7 +24,7 @@ import android.app.Application; import com.activeandroid.ActiveAndroid; import com.activeandroid.Configuration; -import java.io.File; +import org.isoron.uhabits.helpers.DatabaseHelper; public class HabitsApplication extends Application { @@ -41,14 +41,6 @@ public class HabitsApplication extends Application } } - private void deleteDB(String databaseFilename) - { - File databaseFile = new File(String.format("%s/../databases/%s", - getApplicationContext().getFilesDir().getPath(), databaseFilename)); - - if(databaseFile.exists()) databaseFile.delete(); - } - @Override public void onCreate() { @@ -58,7 +50,7 @@ public class HabitsApplication extends Application if (isTestMode()) { databaseFilename = "test.db"; - deleteDB(databaseFilename); + DatabaseHelper.deleteDatabase(this, databaseFilename); } Configuration dbConfig = new Configuration.Builder(this) diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index 95b7477b6..d058890be 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -61,7 +61,8 @@ public class MainActivity extends ReplayableActivity public static final String ACTION_REFRESH = "org.isoron.uhabits.ACTION_REFRESH"; public static final int RESULT_IMPORT_DATA = 1; - public static final int RESULT_EXPORT_ALL_AS_CSV = 2; + public static final int RESULT_EXPORT_CSV = 2; + public static final int RESULT_EXPORT_DB = 3; @Override protected void onCreate(Bundle savedInstanceState) @@ -155,9 +156,13 @@ public class MainActivity extends ReplayableActivity onActionImportClicked(); break; - case RESULT_EXPORT_ALL_AS_CSV: + case RESULT_EXPORT_CSV: listHabitsFragment.exportAllHabits(); break; + + case RESULT_EXPORT_DB: + listHabitsFragment.exportDB(); + break; } } 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 c5696b024..c49665d67 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java @@ -63,6 +63,7 @@ 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; @@ -472,4 +473,9 @@ public class ListHabitsFragment extends Fragment { new ExportCSVTask(activity, Habit.getAll(true), progressBar).execute(); } + + public void exportDB() + { + new ExportDBTask(activity, progressBar).execute(); + } } 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 20b5ce28d..1d3c4abb3 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/SettingsFragment.java @@ -38,7 +38,8 @@ public class SettingsFragment extends PreferenceFragment addPreferencesFromResource(R.xml.preferences); setResultOnPreferenceClick("importData", MainActivity.RESULT_IMPORT_DATA); - setResultOnPreferenceClick("exportCSV", MainActivity.RESULT_EXPORT_ALL_AS_CSV); + setResultOnPreferenceClick("exportCSV", MainActivity.RESULT_EXPORT_CSV); + setResultOnPreferenceClick("exportDB", MainActivity.RESULT_EXPORT_DB); } private void setResultOnPreferenceClick(String key, final int result) diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index 2891eeb5d..804eeb2db 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -1,9 +1,32 @@ package org.isoron.uhabits.helpers; +import android.content.Context; +import android.support.annotation.NonNull; + import com.activeandroid.ActiveAndroid; +import org.isoron.uhabits.BuildConfig; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +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); + FileChannel inChannel = inStream.getChannel(); + FileChannel outChannel = outStream.getChannel(); + inChannel.transferTo(0, inChannel.size(), outChannel); + inStream.close(); + outStream.close(); + } + public interface Command { void execute(); @@ -22,4 +45,31 @@ public class DatabaseHelper ActiveAndroid.endTransaction(); } } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static String saveDatabaseCopy(Context context, File dir) throws IOException + { + File db = getDatabaseFile(context, BuildConfig.databaseFilename); + + SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); + String date = dateFormat.format(DateHelper.getStartOfToday()); + File dbCopy = new File(String.format("%s/Loop-Habits-Backup-%s.db", dir.getAbsolutePath(), date)); + + copy(db, dbCopy); + + return dbCopy.getAbsolutePath(); + } + + public static void deleteDatabase(Context context, String databaseFilename) + { + File db = getDatabaseFile(context, databaseFilename); + if(db.exists()) db.delete(); + } + + @NonNull + private static File getDatabaseFile(Context context, String databaseFilename) + { + return new File(String.format("%s/../databases/%s", + context.getApplicationContext().getFilesDir().getPath(), databaseFilename)); + } } diff --git a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java index 0ac07ec4a..ef23e997b 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java @@ -98,7 +98,7 @@ public class HabitsCSVExporter { SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); String date = dateFormat.format(DateHelper.getStartOfToday()); - String zipFilename = String.format("%s/Loop-Habits-%s.zip", exportDirName, date); + String zipFilename = String.format("%s/Loop-Habits-CSV-%s.zip", exportDirName, date); FileOutputStream fos = new FileOutputStream(zipFilename); ZipOutputStream zos = new ZipOutputStream(fos); 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..c8ae50681 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java @@ -0,0 +1,94 @@ +/* + * 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.content.Intent; +import android.net.Uri; +import android.os.AsyncTask; +import android.view.View; +import android.widget.ProgressBar; + +import org.isoron.uhabits.R; +import org.isoron.uhabits.ReplayableActivity; +import org.isoron.uhabits.helpers.DatabaseHelper; + +import java.io.File; +import java.io.IOException; + +public class ExportDBTask extends AsyncTask +{ + private final ReplayableActivity activity; + private ProgressBar progressBar; + private String filename; + + public ExportDBTask(ReplayableActivity activity, ProgressBar progressBar) + { + this.progressBar = progressBar; + this.activity = activity; + } + + @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/octet-stream"); + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(filename))); + + activity.startActivity(intent); + } + else + { + activity.showToast(R.string.could_not_export); + } + + if(progressBar != null) + progressBar.setVisibility(View.GONE); + } + + @Override + protected Void doInBackground(Void... params) + { + filename = null; + + try + { + filename = DatabaseHelper.saveDatabaseCopy(activity, activity.getExternalCacheDir()); + } + catch(IOException e) + { + e.printStackTrace(); + } + + return null; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c314b3d37..43a91f004 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,9 +142,10 @@ Failed to import habits from file. File type not recognized. Habits imported successfully. - Supports files exported by Loop, Tickmate, HabitBull or Rewire. See FAQ for more information. + Supports full backups exported by this app, as well as files generated by Tickmate, HabitBull or Rewire. See FAQ for more information. Import data - This archive contains files that can be opened by spreadsheet software such as Microsoft Excel or OpenOffice Calc, but cannot be imported back. + Generates files that can be opened by spreadsheet software such as Microsoft Excel or OpenOffice Calc, but cannot be imported back. Export full backup - This file contains all your data and can be imported back. + Generates a file that contains all your data, and that can be imported back. + Full backup successfully exported. \ 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 b1bbf36f6..8999ef51b 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -46,6 +46,7 @@ android:title="Database"> From ad391fa79150a8c1ce3587337fd797301235401a Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Wed, 23 Mar 2016 18:47:04 -0400 Subject: [PATCH 10/27] Remove extra permissions; better organize files dir --- app/src/main/AndroidManifest.xml | 6 +++-- .../java/org/isoron/uhabits/MainActivity.java | 18 +------------ .../fragments/HabitSelectionCallback.java | 18 +++---------- .../uhabits/fragments/ListHabitsFragment.java | 13 +++++----- .../uhabits/helpers/DatabaseHelper.java | 19 +++++++++++--- .../isoron/uhabits/helpers/DateHelper.java | 8 ++++++ .../isoron/uhabits/io/HabitsCSVExporter.java | 25 ++++++------------- .../isoron/uhabits/tasks/ExportCSVTask.java | 19 +++++++++++--- .../isoron/uhabits/tasks/ExportDBTask.java | 5 +++- app/src/main/res/menu/list_habits_context.xml | 4 --- 10 files changed, 65 insertions(+), 70 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4f0cb89fc..f72968c2e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,10 +27,12 @@ + android:name="android.permission.READ_EXTERNAL_STORAGE" + android:maxSdkVersion="18" /> + android:name="android.permission.WRITE_EXTERNAL_STORAGE" + android:maxSdkVersion="18" /> selectedHabits) - { - new ExportCSVTask(activity, selectedHabits, progressBar).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 c49665d67..22a0be019 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java @@ -25,7 +25,6 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; -import android.os.Environment; import android.preference.PreferenceManager; import android.view.ActionMode; import android.view.ContextMenu; @@ -49,14 +48,14 @@ import com.mobeta.android.dslv.DragSortController; import com.mobeta.android.dslv.DragSortListView; import com.mobeta.android.dslv.DragSortListView.DropListener; +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.FilePickerDialog; import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.helpers.DialogHelper; import org.isoron.uhabits.helpers.DialogHelper.OnSavedListener; -import org.isoron.uhabits.ReplayableActivity; -import org.isoron.uhabits.R; -import org.isoron.uhabits.commands.ToggleRepetitionCommand; -import org.isoron.uhabits.dialogs.FilePickerDialog; import org.isoron.uhabits.helpers.HintManager; import org.isoron.uhabits.helpers.ListHabitsHelper; import org.isoron.uhabits.helpers.ReminderHelper; @@ -434,7 +433,9 @@ public class ListHabitsFragment extends Fragment public void showImportDialog() { - File dir = Environment.getExternalStorageDirectory(); + File dir = activity.getExternalFilesDir(null); + if(dir == null) return; + FilePickerDialog picker = new FilePickerDialog(activity, dir); picker.setFileListener(new FilePickerDialog.OnFileSelectedListener() { diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index 804eeb2db..5d52679bf 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -2,6 +2,7 @@ package org.isoron.uhabits.helpers; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.activeandroid.ActiveAndroid; @@ -51,9 +52,9 @@ public class DatabaseHelper { File db = getDatabaseFile(context, BuildConfig.databaseFilename); - SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); - String date = dateFormat.format(DateHelper.getStartOfToday()); - File dbCopy = new File(String.format("%s/Loop-Habits-Backup-%s.db", dir.getAbsolutePath(), date)); + 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); @@ -72,4 +73,16 @@ public class DatabaseHelper return new File(String.format("%s/../databases/%s", context.getApplicationContext().getFilesDir().getPath(), databaseFilename)); } + + @Nullable + public static File getFilesDir(Context context, String prefix) + { + File baseDir = context.getExternalFilesDir(null); + if(baseDir == null) return null; + if(!baseDir.canWrite()) return null; + + File dir = new File(String.format("%s/%s/", baseDir.getAbsolutePath(), prefix)); + dir.mkdirs(); + return dir; + } } diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java index 8a05c048b..85efd2630 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java @@ -105,6 +105,14 @@ public class DateHelper return dateFormat; } + public static SimpleDateFormat getBackupDateFormat() + { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", 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/uhabits/io/HabitsCSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java index ef23e997b..6370dc6bc 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java @@ -44,13 +44,10 @@ public class HabitsCSVExporter private String exportDirName; - public HabitsCSVExporter(List habits, String exportDirName) + public HabitsCSVExporter(List habits, File dir) { this.habits = habits; - this.exportDirName = exportDirName; - - if(!this.exportDirName.endsWith("/")) - this.exportDirName += "/"; + this.exportDirName = dir.getAbsolutePath() + "/"; generateDirs = new LinkedList<>(); generateFilenames = new LinkedList<>(); @@ -98,7 +95,7 @@ public class HabitsCSVExporter { SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); String date = dateFormat.format(DateHelper.getStartOfToday()); - String zipFilename = String.format("%s/Loop-Habits-CSV-%s.zip", exportDirName, date); + String zipFilename = String.format("%s/Loop Habits CSV %s.zip", exportDirName, date); FileOutputStream fos = new FileOutputStream(zipFilename); ZipOutputStream zos = new ZipOutputStream(fos); @@ -127,21 +124,13 @@ public class HabitsCSVExporter fis.close(); } - public String writeArchive() + public String writeArchive() throws IOException { String zipFilename; - try - { - writeHabits(); - zipFilename = writeZipFile(); - cleanup(); - } - catch (IOException e) - { - e.printStackTrace(); - return null; - } + writeHabits(); + zipFilename = writeZipFile(); + cleanup(); return zipFilename; } diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java index 526e3ff57..151abb242 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java @@ -25,12 +25,14 @@ import android.os.AsyncTask; import android.view.View; import android.widget.ProgressBar; -import org.isoron.uhabits.ReplayableActivity; import org.isoron.uhabits.R; +import org.isoron.uhabits.ReplayableActivity; +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 @@ -82,9 +84,18 @@ public class ExportCSVTask extends AsyncTask @Override protected Void doInBackground(Void... params) { - String dirName = String.format("%s/export/", activity.getExternalCacheDir()); - HabitsCSVExporter exporter = new HabitsCSVExporter(selectedHabits, dirName); - archiveFilename = exporter.writeArchive(); + try + { + File dir = DatabaseHelper.getFilesDir(activity, "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 index c8ae50681..02b8e5720 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java @@ -82,7 +82,10 @@ public class ExportDBTask extends AsyncTask try { - filename = DatabaseHelper.saveDatabaseCopy(activity, activity.getExternalCacheDir()); + File dir = DatabaseHelper.getFilesDir(activity, "Backups"); + if(dir == null) return null; + + filename = DatabaseHelper.saveDatabaseCopy(activity, dir); } catch(IOException e) { diff --git a/app/src/main/res/menu/list_habits_context.xml b/app/src/main/res/menu/list_habits_context.xml index 23c76426f..b48ffc38c 100644 --- a/app/src/main/res/menu/list_habits_context.xml +++ b/app/src/main/res/menu/list_habits_context.xml @@ -40,10 +40,6 @@ android:title="@string/unarchive" android:icon="@drawable/ic_action_unarchive_light"/> - - From eeb0b109aea502123be0cfd0096135a42b9d7206 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Wed, 23 Mar 2016 19:20:12 -0400 Subject: [PATCH 11/27] Remove export CSV from context menu --- app/src/main/res/menu-v21/list_habits_context.xml | 5 ----- 1 file changed, 5 deletions(-) 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"/> - - Date: Wed, 23 Mar 2016 19:20:51 -0400 Subject: [PATCH 12/27] Allow user to import full database Closes #67 --- .../org/isoron/uhabits/HabitsApplication.java | 19 ++++-- .../uhabits/helpers/DatabaseHelper.java | 21 +++++- .../isoron/uhabits/io/GenericImporter.java | 1 + .../org/isoron/uhabits/io/LoopDBImporter.java | 68 +++++++++++++++++++ 4 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java diff --git a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java index 4e4ff918e..78eefa30a 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -20,14 +20,16 @@ package org.isoron.uhabits; import android.app.Application; +import android.content.Context; import com.activeandroid.ActiveAndroid; -import com.activeandroid.Configuration; import org.isoron.uhabits.helpers.DatabaseHelper; public class HabitsApplication extends Application { + private static Context context; + private boolean isTestMode() { try @@ -41,10 +43,17 @@ public class HabitsApplication extends Application } } + public static Context getContext() + { + return context; + } + @Override public void onCreate() { super.onCreate(); + HabitsApplication.context = this; + String databaseFilename = BuildConfig.databaseFilename; if (isTestMode()) @@ -53,17 +62,13 @@ public class HabitsApplication extends Application DatabaseHelper.deleteDatabase(this, databaseFilename); } - Configuration dbConfig = new Configuration.Builder(this) - .setDatabaseName(databaseFilename) - .setDatabaseVersion(BuildConfig.databaseVersion) - .create(); - - ActiveAndroid.initialize(dbConfig); + DatabaseHelper.initializeActiveAndroid(this, databaseFilename); } @Override public void onTerminate() { + HabitsApplication.context = null; ActiveAndroid.dispose(); super.onTerminate(); } diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index 5d52679bf..901e9d218 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -5,8 +5,14 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.activeandroid.ActiveAndroid; +import com.activeandroid.Configuration; import org.isoron.uhabits.BuildConfig; +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; @@ -68,7 +74,7 @@ public class DatabaseHelper } @NonNull - private static File getDatabaseFile(Context context, String databaseFilename) + public static File getDatabaseFile(Context context, String databaseFilename) { return new File(String.format("%s/../databases/%s", context.getApplicationContext().getFilesDir().getPath(), databaseFilename)); @@ -85,4 +91,17 @@ public class DatabaseHelper dir.mkdirs(); return dir; } + + @SuppressWarnings("unchecked") + public static void initializeActiveAndroid(Context context, String databaseFilename) + { + Configuration dbConfig = new Configuration.Builder(context) + .setDatabaseName(databaseFilename) + .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/uhabits/io/GenericImporter.java b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java index 1ecc4c1fa..c08a3a72f 100644 --- a/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/GenericImporter.java @@ -33,6 +33,7 @@ public class GenericImporter extends AbstractImporter public GenericImporter() { importers = new LinkedList<>(); + importers.add(new LoopDBImporter()); importers.add(new RewireDBImporter()); importers.add(new TickmateDBImporter()); importers.add(new HabitBullCSVImporter()); 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..66bd80dd2 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java @@ -0,0 +1,68 @@ +/* + * 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.support.annotation.NonNull; + +import com.activeandroid.ActiveAndroid; + +import org.isoron.uhabits.BuildConfig; +import org.isoron.uhabits.HabitsApplication; +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(); + return result; + } + + @Override + public void importHabitsFromFile(@NonNull File file) throws IOException + { + ActiveAndroid.dispose(); + Context context = HabitsApplication.getContext(); + + File originalDB = DatabaseHelper.getDatabaseFile(context, BuildConfig.databaseFilename); + File backupDir = DatabaseHelper.getFilesDir(context, "Backups"); + + DatabaseHelper.saveDatabaseCopy(context, backupDir); + DatabaseHelper.copy(file, originalDB); + DatabaseHelper.initializeActiveAndroid(context, BuildConfig.databaseFilename); + } +} From 743431ef6798a1df27bfd77818a5a76d968f81da Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Mar 2016 06:59:41 -0400 Subject: [PATCH 13/27] Refactor DatabaseHelper; write tests for data import --- app/src/androidTest/assets/habitbull.csv | 19 ++ app/src/androidTest/assets/loop.db | Bin 0 -> 53248 bytes app/src/androidTest/assets/rewire.db | Bin 0 -> 57344 bytes app/src/androidTest/assets/tickmate.db | Bin 0 -> 32768 bytes .../isoron/uhabits/unit/io/ImportTest.java | 173 ++++++++++++++++++ .../uhabits/unit/models/HabitFixtures.java | 6 +- .../org/isoron/uhabits/HabitsApplication.java | 18 +- .../uhabits/helpers/DatabaseHelper.java | 74 ++++++-- .../org/isoron/uhabits/io/LoopDBImporter.java | 7 +- .../isoron/uhabits/tasks/ExportDBTask.java | 2 +- 10 files changed, 268 insertions(+), 31 deletions(-) create mode 100644 app/src/androidTest/assets/habitbull.csv create mode 100644 app/src/androidTest/assets/loop.db create mode 100644 app/src/androidTest/assets/rewire.db create mode 100644 app/src/androidTest/assets/tickmate.db create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/io/ImportTest.java 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/loop.db b/app/src/androidTest/assets/loop.db new file mode 100644 index 0000000000000000000000000000000000000000..25f0f32cd9471930dac3dc90ec784e8a69f73588 GIT binary patch literal 53248 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 + * + * 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; + private Context targetContext; + + @Before + public void setup() + { + HabitFixtures.purgeHabits(); + + context = InstrumentationRegistry.getInstrumentation().getContext(); + targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + baseDir = DatabaseHelper.getFilesDir(targetContext, "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/HabitFixtures.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/HabitFixtures.java index bb2559cb8..1a15249ff 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 @@ -28,7 +28,7 @@ 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.freqNum = 2; @@ -45,7 +45,7 @@ public class HabitFixtures return habit; } - static Habit createEmptyHabit() + public static Habit createEmptyHabit() { Habit habit = new Habit(); habit.freqNum = 1; @@ -54,7 +54,7 @@ public class HabitFixtures return habit; } - static void purgeHabits() + public static void purgeHabits() { for(Habit h : Habit.getAll(true)) h.cascadeDelete(); diff --git a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java index 78eefa30a..810620465 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -21,20 +21,25 @@ package org.isoron.uhabits; import android.app.Application; import android.content.Context; +import android.support.annotation.Nullable; import com.activeandroid.ActiveAndroid; import org.isoron.uhabits.helpers.DatabaseHelper; +import java.io.File; + public class HabitsApplication extends Application { + @Nullable private static Context context; - private boolean isTestMode() + public static boolean isTestMode() { try { - getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest"); + if(context != null) + context.getClassLoader().loadClass("org.isoron.uhabits.unit.models.HabitTest"); return true; } catch (final Exception e) @@ -43,6 +48,7 @@ public class HabitsApplication extends Application } } + @Nullable public static Context getContext() { return context; @@ -54,15 +60,13 @@ public class HabitsApplication extends Application super.onCreate(); HabitsApplication.context = this; - String databaseFilename = BuildConfig.databaseFilename; - if (isTestMode()) { - databaseFilename = "test.db"; - DatabaseHelper.deleteDatabase(this, databaseFilename); + File db = DatabaseHelper.getDatabaseFile(); + if(db.exists()) db.delete(); } - DatabaseHelper.initializeActiveAndroid(this, databaseFilename); + DatabaseHelper.initializeActiveAndroid(this); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index 901e9d218..476d6e2aa 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -1,3 +1,22 @@ +/* + * 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; @@ -8,6 +27,7 @@ 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; @@ -18,7 +38,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.nio.channels.FileChannel; +import java.io.InputStream; +import java.io.OutputStream; import java.text.SimpleDateFormat; public class DatabaseHelper @@ -27,11 +48,22 @@ public class DatabaseHelper { FileInputStream inStream = new FileInputStream(src); FileOutputStream outStream = new FileOutputStream(dst); - FileChannel inChannel = inStream.getChannel(); - FileChannel outChannel = outStream.getChannel(); - inChannel.transferTo(0, inChannel.size(), outChannel); - inStream.close(); - outStream.close(); + 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 @@ -54,9 +86,9 @@ public class DatabaseHelper } @SuppressWarnings("ResultOfMethodCallIgnored") - public static String saveDatabaseCopy(Context context, File dir) throws IOException + public static String saveDatabaseCopy(File dir) throws IOException { - File db = getDatabaseFile(context, BuildConfig.databaseFilename); + File db = getDatabaseFile(); SimpleDateFormat dateFormat = DateHelper.getBackupDateFormat(); String date = dateFormat.format(DateHelper.getLocalTime()); @@ -67,17 +99,27 @@ public class DatabaseHelper return dbCopy.getAbsolutePath(); } - public static void deleteDatabase(Context context, String databaseFilename) + @NonNull + public static File getDatabaseFile() { - File db = getDatabaseFile(context, databaseFilename); - if(db.exists()) db.delete(); + 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 File getDatabaseFile(Context context, String databaseFilename) + public static String getDatabaseFilename() { - return new File(String.format("%s/../databases/%s", - context.getApplicationContext().getFilesDir().getPath(), databaseFilename)); + String databaseFilename = BuildConfig.databaseFilename; + + if (HabitsApplication.isTestMode()) + databaseFilename = "test.db"; + + return databaseFilename; } @Nullable @@ -93,10 +135,10 @@ public class DatabaseHelper } @SuppressWarnings("unchecked") - public static void initializeActiveAndroid(Context context, String databaseFilename) + public static void initializeActiveAndroid(Context context) { Configuration dbConfig = new Configuration.Builder(context) - .setDatabaseName(databaseFilename) + .setDatabaseName(getDatabaseFilename()) .setDatabaseVersion(BuildConfig.databaseVersion) .addModelClasses(Checkmark.class, Habit.class, Repetition.class, Score.class, Streak.class) diff --git a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java index 66bd80dd2..5ae993d73 100644 --- a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java @@ -57,12 +57,11 @@ public class LoopDBImporter extends AbstractImporter { ActiveAndroid.dispose(); Context context = HabitsApplication.getContext(); - - File originalDB = DatabaseHelper.getDatabaseFile(context, BuildConfig.databaseFilename); + File originalDB = DatabaseHelper.getDatabaseFile(); File backupDir = DatabaseHelper.getFilesDir(context, "Backups"); - DatabaseHelper.saveDatabaseCopy(context, backupDir); + DatabaseHelper.saveDatabaseCopy(backupDir); DatabaseHelper.copy(file, originalDB); - DatabaseHelper.initializeActiveAndroid(context, BuildConfig.databaseFilename); + DatabaseHelper.initializeActiveAndroid(context); } } diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java index 02b8e5720..f5f70809f 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java @@ -85,7 +85,7 @@ public class ExportDBTask extends AsyncTask File dir = DatabaseHelper.getFilesDir(activity, "Backups"); if(dir == null) return null; - filename = DatabaseHelper.saveDatabaseCopy(activity, dir); + filename = DatabaseHelper.saveDatabaseCopy(dir); } catch(IOException e) { From dcc771abe7bbf8571c8b40cdee9d5b8a950b2b7e Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Mar 2016 18:39:14 -0400 Subject: [PATCH 14/27] Chose external dir better --- .../uhabits/helpers/DatabaseHelper.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index 476d6e2aa..e0d0d757c 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -22,6 +22,7 @@ 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; @@ -125,12 +126,22 @@ public class DatabaseHelper @Nullable public static File getFilesDir(Context context, String prefix) { - File baseDir = context.getExternalFilesDir(null); - if(baseDir == null) return null; - if(!baseDir.canWrite()) return null; + File chosenDir = null; + File externalFilesDirs[] = ContextCompat.getExternalFilesDirs(context, null); + if(externalFilesDirs == null) return null; - File dir = new File(String.format("%s/%s/", baseDir.getAbsolutePath(), prefix)); + for(File dir : externalFilesDirs) + { + if(!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; } From 6c05366f0f8b2dc79a230b912631fb58f4f058e7 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Mar 2016 18:39:32 -0400 Subject: [PATCH 15/27] Remove illegal characters from filename --- app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java index 85efd2630..371e2ae47 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DateHelper.java @@ -107,7 +107,7 @@ public class DateHelper public static SimpleDateFormat getBackupDateFormat() { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HHmmss", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat; From 90a3964f0fb749627861e5b6b4a10a28c0b290e5 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Mar 2016 18:39:49 -0400 Subject: [PATCH 16/27] Close database --- app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java | 1 + app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java | 3 +++ .../main/java/org/isoron/uhabits/io/TickmateDBImporter.java | 3 +++ 3 files changed, 7 insertions(+) diff --git a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java index 5ae993d73..74d6dc7bb 100644 --- a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java @@ -49,6 +49,7 @@ public class LoopDBImporter extends AbstractImporter boolean result = (c.moveToFirst() && c.getInt(0) == 2); c.close(); + db.close(); return result; } diff --git a/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java index 66b89690c..47fc92020 100644 --- a/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/RewireDBImporter.java @@ -47,6 +47,7 @@ public class RewireDBImporter extends AbstractImporter boolean result = (c.moveToFirst() && c.getInt(0) == 2); c.close(); + db.close(); return result; } @@ -64,6 +65,8 @@ public class RewireDBImporter extends AbstractImporter createHabits(db); } }); + + db.close(); } private void createHabits(SQLiteDatabase db) diff --git a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java index be14ca2b4..f0b6b9770 100644 --- a/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/TickmateDBImporter.java @@ -47,6 +47,7 @@ public class TickmateDBImporter extends AbstractImporter boolean result = (c.moveToFirst() && c.getInt(0) == 2); c.close(); + db.close(); return result; } @@ -64,6 +65,8 @@ public class TickmateDBImporter extends AbstractImporter createHabits(db); } }); + + db.close(); } private void createHabits(SQLiteDatabase db) From 2dfbcfcdb0f7c5f6f498ef178ae98e6817feac2d Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Mar 2016 20:14:26 -0400 Subject: [PATCH 17/27] Revert to alpha version of gradle Build fails otherwise --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f4d8c542e..eb536f4d1 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.5.0' + classpath 'com.android.tools.build:gradle:2.1.0-alpha3' } } From 1120f56dd4e7b3698016dbc73e549e49e336686a Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Mar 2016 21:36:41 -0400 Subject: [PATCH 18/27] Write tests for CSV export --- .../unit/models/CheckmarkListTest.java | 24 +++++++++++++ .../uhabits/unit/models/HabitFixtures.java | 6 ++++ .../isoron/uhabits/unit/models/HabitTest.java | 20 +++++++++++ .../uhabits/unit/models/ScoreListTest.java | 27 ++++++++++++++ .../isoron/uhabits/unit/models/ScoreTest.java | 9 ++--- .../isoron/uhabits/models/CheckmarkList.java | 27 ++++++++++++++ .../java/org/isoron/uhabits/models/Habit.java | 35 ++++++------------- .../org/isoron/uhabits/models/ScoreList.java | 18 +++++++++- 8 files changed, 133 insertions(+), 33 deletions(-) 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 0485607fb..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 @@ -29,6 +29,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; 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 1a15249ff..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,6 +19,7 @@ package org.isoron.uhabits.unit.models; +import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.DateHelper; import org.isoron.uhabits.models.Habit; @@ -31,6 +32,8 @@ public class HabitFixtures 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(); @@ -48,6 +51,9 @@ public class HabitFixtures 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(); 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 c7420aa40..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.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/ScoreListTest.java b/app/src/androidTest/java/org/isoron/uhabits/unit/models/ScoreListTest.java index bc094139c..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 @@ -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,6 +133,30 @@ 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) { DatabaseHelper.executeAsTransaction(new DatabaseHelper.Command() 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 cfad09f37..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 @@ -22,18 +22,13 @@ package org.isoron.uhabits.unit.models; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import org.isoron.uhabits.models.Checkmark; +import org.isoron.uhabits.models.Score; import org.junit.Test; import org.junit.runner.RunWith; 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.Checkmark; @RunWith(AndroidJUnit4.class) @SmallTest 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 c744a226e..8c5f5f577 100644 --- a/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java +++ b/app/src/main/java/org/isoron/uhabits/models/CheckmarkList.java @@ -76,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 " + @@ -127,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. @@ -234,8 +250,18 @@ public class CheckmarkList 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"; @@ -255,5 +281,6 @@ public class CheckmarkList } 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 cd8b6a7ef..a3c51e3fe 100644 --- a/app/src/main/java/org/isoron/uhabits/models/Habit.java +++ b/app/src/main/java/org/isoron/uhabits/models/Habit.java @@ -20,7 +20,6 @@ package org.isoron.uhabits.models; import android.annotation.SuppressLint; -import android.graphics.Color; import android.net.Uri; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -34,16 +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.CSVReader; import com.opencsv.CSVWriter; import org.isoron.uhabits.helpers.ColorHelper; import org.isoron.uhabits.helpers.DateHelper; import java.io.IOException; -import java.io.Reader; import java.io.Writer; -import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -478,9 +474,18 @@ public class Habit extends Model 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", "FrequencyNumerator", "FrequencyDenominator", "Color" }; + String header[] = { "Name", "Description", "NumRepetitions", "Interval", "Color" }; CSVWriter csv = new CSVWriter(out); csv.writeNext(header, false); @@ -494,24 +499,4 @@ public class Habit extends Model csv.close(); } - - public List parseCSV(Reader in) - { - CSVReader csv = new CSVReader(in); - List habits = new LinkedList<>(); - - for(String cols[] : csv) - { - Habit habit = new Habit(); - - habit.name = cols[0]; - habit.description = cols[1]; - habit.freqNum = Integer.parseInt(cols[2]); - habit.freqDen = Integer.parseInt(cols[3]); - habit.color = Color.parseColor(cols[4]); - habits.add(habit); - } - - return habits; - } } 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 57f4e42a0..1972dc411 100644 --- a/app/src/main/java/org/isoron/uhabits/models/ScoreList.java +++ b/app/src/main/java/org/isoron/uhabits/models/ScoreList.java @@ -99,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 @@ -285,6 +298,8 @@ public class ScoreList public void writeCSV(Writer out) throws IOException { + computeAll(); + SimpleDateFormat dateFormat = DateHelper.getCSVDateFormat(); String query = "select timestamp, score from score where habit = ? order by timestamp"; @@ -298,11 +313,12 @@ public class ScoreList do { String timestamp = dateFormat.format(new Date(cursor.getLong(0))); - String score = String.format("%.2f", ((float) cursor.getInt(1)) / Score.MAX_VALUE); + 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(); } } From 3c595bc79d8be033423d19fedf51e731f06dbae2 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Mar 2016 21:51:02 -0400 Subject: [PATCH 19/27] Add null check on DatabaseHelper --- .../main/java/org/isoron/uhabits/helpers/DatabaseHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index e0d0d757c..cde9ca9f2 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -132,7 +132,7 @@ public class DatabaseHelper for(File dir : externalFilesDirs) { - if(!dir.canWrite()) continue; + if (dir == null || !dir.canWrite()) continue; chosenDir = dir; break; } From d3ebb4ff25ef0ba4d6848a6c4666b43ab446541b Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Mar 2016 21:52:59 -0400 Subject: [PATCH 20/27] Create SD card --- circle.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 3b88e723c..ad1d0980e 100644 --- a/circle.yml +++ b/circle.yml @@ -5,7 +5,8 @@ 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 From 3656c51e95d29a7b3576f93832f8d995b9e7a3f6 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Thu, 24 Mar 2016 22:14:54 -0400 Subject: [PATCH 21/27] Disable pre-dexing --- build.gradle | 12 ++++++++++++ circle.yml | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eb536f4d1..d71fd8517 100644 --- a/build.gradle +++ b/build.gradle @@ -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 ad1d0980e..f10bb66de 100644 --- a/circle.yml +++ b/circle.yml @@ -11,7 +11,7 @@ test: 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 From 9e410f839582070986a196b20610cb942870ae3e Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 25 Mar 2016 06:15:53 -0400 Subject: [PATCH 22/27] Refactor and write tests for IO tasks --- app/src/androidTest/assets/icon.png | Bin 0 -> 49120 bytes .../isoron/uhabits/unit/io/ImportTest.java | 6 +- .../uhabits/unit/tasks/ExportCSVTaskTest.java | 77 +++++++++++++ .../uhabits/unit/tasks/ExportDBTaskTest.java | 71 ++++++++++++ .../unit/tasks/ImportDataTaskTest.java | 108 ++++++++++++++++++ .../uhabits/dialogs/FilePickerDialog.java | 8 +- .../uhabits/fragments/ListHabitsFragment.java | 49 +++++++- .../uhabits/helpers/DatabaseHelper.java | 5 +- .../isoron/uhabits/tasks/ExportCSVTask.java | 40 +++---- .../isoron/uhabits/tasks/ExportDBTask.java | 37 +++--- app/src/main/res/values/strings.xml | 12 +- 11 files changed, 349 insertions(+), 64 deletions(-) create mode 100644 app/src/androidTest/assets/icon.png create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportCSVTaskTest.java create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ExportDBTaskTest.java create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/tasks/ImportDataTaskTest.java diff --git a/app/src/androidTest/assets/icon.png b/app/src/androidTest/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7907954db759e36b4bacfb99e9c15b9266083b0a GIT binary patch literal 49120 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 + * + * 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/java/org/isoron/uhabits/dialogs/FilePickerDialog.java b/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java index a7a8520fd..94c574fbd 100644 --- a/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/dialogs/FilePickerDialog.java @@ -48,7 +48,7 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener void onFileSelected(File file); } - private OnFileSelectedListener fileListener; + private OnFileSelectedListener listener; public FilePickerDialog(Activity activity, File initialDirectory) { @@ -81,7 +81,7 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener } else { - if (fileListener != null) fileListener.onFileSelected(file); + if (listener != null) listener.onFileSelected(file); dialog.dismiss(); } } @@ -91,9 +91,9 @@ public class FilePickerDialog implements AdapterView.OnItemClickListener dialog.show(); } - public void setFileListener(OnFileSelectedListener fileListener) + public void setListener(OnFileSelectedListener listener) { - this.fileListener = fileListener; + this.listener = listener; } private void navigateTo(File path) 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 22a0be019..02ca46b92 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; @@ -73,7 +74,8 @@ import java.util.List; public class ListHabitsFragment extends Fragment implements OnSavedListener, OnItemClickListener, OnLongClickListener, DropListener, OnClickListener, HabitListLoader.Listener, AdapterView.OnItemLongClickListener, - HabitSelectionCallback.Listener, ImportDataTask.Listener + HabitSelectionCallback.Listener, ImportDataTask.Listener, ExportCSVTask.Listener, + ExportDBTask.Listener { long lastLongClick = 0; private boolean isShortToggleEnabled; @@ -437,7 +439,7 @@ public class ListHabitsFragment extends Fragment if(dir == null) return; FilePickerDialog picker = new FilePickerDialog(activity, dir); - picker.setFileListener(new FilePickerDialog.OnFileSelectedListener() + picker.setListener(new FilePickerDialog.OnFileSelectedListener() { @Override public void onFileSelected(File file) @@ -447,6 +449,7 @@ public class ListHabitsFragment extends Fragment task.execute(); } }); + picker.show(); } @@ -472,11 +475,49 @@ public class ListHabitsFragment extends Fragment public void exportAllHabits() { - new ExportCSVTask(activity, Habit.getAll(true), progressBar).execute(); + 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() { - new ExportDBTask(activity, progressBar).execute(); + 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/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index cde9ca9f2..cd411328f 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -124,8 +124,11 @@ public class DatabaseHelper } @Nullable - public static File getFilesDir(Context context, String prefix) + 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; diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java index 151abb242..2ebd05cc8 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportCSVTask.java @@ -19,14 +19,11 @@ package org.isoron.uhabits.tasks; -import android.content.Intent; -import android.net.Uri; import android.os.AsyncTask; +import android.support.annotation.Nullable; import android.view.View; import android.widget.ProgressBar; -import org.isoron.uhabits.R; -import org.isoron.uhabits.ReplayableActivity; import org.isoron.uhabits.helpers.DatabaseHelper; import org.isoron.uhabits.io.HabitsCSVExporter; import org.isoron.uhabits.models.Habit; @@ -37,17 +34,25 @@ import java.util.List; public class ExportCSVTask extends AsyncTask { - private final ReplayableActivity activity; + public interface Listener + { + void onExportCSVFinished(@Nullable String archiveFilename); + } + private ProgressBar progressBar; private final List selectedHabits; - String archiveFilename; + private String archiveFilename; + private ExportCSVTask.Listener listener; - public ExportCSVTask(ReplayableActivity activity, List selectedHabits, - ProgressBar progressBar) + public ExportCSVTask(List selectedHabits, ProgressBar progressBar) { this.selectedHabits = selectedHabits; this.progressBar = progressBar; - this.activity = activity; + } + + public void setListener(Listener listener) + { + this.listener = listener; } @Override @@ -63,19 +68,8 @@ public class ExportCSVTask extends AsyncTask @Override protected void onPostExecute(Void aVoid) { - 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); - } + if(listener != null) + listener.onExportCSVFinished(archiveFilename); if(progressBar != null) progressBar.setVisibility(View.GONE); @@ -86,7 +80,7 @@ public class ExportCSVTask extends AsyncTask { try { - File dir = DatabaseHelper.getFilesDir(activity, "CSV"); + File dir = DatabaseHelper.getFilesDir("CSV"); if(dir == null) return null; HabitsCSVExporter exporter = new HabitsCSVExporter(selectedHabits, dir); diff --git a/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java index f5f70809f..abc31b490 100644 --- a/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java +++ b/app/src/main/java/org/isoron/uhabits/tasks/ExportDBTask.java @@ -19,14 +19,11 @@ package org.isoron.uhabits.tasks; -import android.content.Intent; -import android.net.Uri; import android.os.AsyncTask; +import android.support.annotation.Nullable; import android.view.View; import android.widget.ProgressBar; -import org.isoron.uhabits.R; -import org.isoron.uhabits.ReplayableActivity; import org.isoron.uhabits.helpers.DatabaseHelper; import java.io.File; @@ -34,14 +31,23 @@ import java.io.IOException; public class ExportDBTask extends AsyncTask { - private final ReplayableActivity activity; + public interface Listener + { + void onExportDBFinished(@Nullable String filename); + } + private ProgressBar progressBar; private String filename; + private Listener listener; - public ExportDBTask(ReplayableActivity activity, ProgressBar progressBar) + public ExportDBTask(ProgressBar progressBar) { this.progressBar = progressBar; - this.activity = activity; + } + + public void setListener(Listener listener) + { + this.listener = listener; } @Override @@ -57,19 +63,8 @@ public class ExportDBTask extends AsyncTask @Override protected void onPostExecute(Void aVoid) { - 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); - } + if(listener != null) + listener.onExportDBFinished(filename); if(progressBar != null) progressBar.setVisibility(View.GONE); @@ -82,7 +77,7 @@ public class ExportDBTask extends AsyncTask try { - File dir = DatabaseHelper.getFilesDir(activity, "Backups"); + File dir = DatabaseHelper.getFilesDir("Backups"); if(dir == null) return null; filename = DatabaseHelper.saveDatabaseCopy(dir); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 43a91f004..394b5dd39 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -139,13 +139,13 @@ Custom … Help & FAQ Failed to export data. - Failed to import habits from file. - File type not recognized. + Failed to import data. + File not recognized. Habits imported successfully. - Supports full backups exported by this app, as well as files generated by Tickmate, HabitBull or Rewire. See FAQ for more information. + Full backup successfully exported. Import data - Generates files that can be opened by spreadsheet software such as Microsoft Excel or OpenOffice Calc, but cannot be imported back. Export full backup - Generates a file that contains all your data, and that can be imported back. - Full backup successfully exported. + 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 From 28dad560a6efd962060f433beba2b05fcaafc71e Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 25 Mar 2016 07:09:06 -0400 Subject: [PATCH 23/27] Write tests for CSV exporter --- .../unit/io/HabitsCSVExporterTest.java | 115 ++++++++++++++++++ .../isoron/uhabits/io/HabitsCSVExporter.java | 2 +- 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 app/src/androidTest/java/org/isoron/uhabits/unit/io/HabitsCSVExporterTest.java 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/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java index 6370dc6bc..f84ad666c 100644 --- a/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/HabitsCSVExporter.java @@ -64,7 +64,7 @@ public class HabitsCSVExporter for(Habit h : habits) { - String habitDirName = String.format("%03d %s/", h.position, h.name); + String habitDirName = String.format("%03d %s/", h.position + 1, h.name); new File(exportDirName + habitDirName).mkdirs(); generateDirs.add(habitDirName); From 59c0af17d769adeaeb0205f455612824d8aa5804 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 25 Mar 2016 07:09:41 -0400 Subject: [PATCH 24/27] Use application context to initialize ActiveAndroid --- .../main/java/org/isoron/uhabits/HabitsApplication.java | 2 +- .../java/org/isoron/uhabits/helpers/DatabaseHelper.java | 5 ++++- .../main/java/org/isoron/uhabits/io/LoopDBImporter.java | 8 ++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java index 810620465..accba2f7f 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -66,7 +66,7 @@ public class HabitsApplication extends Application if(db.exists()) db.delete(); } - DatabaseHelper.initializeActiveAndroid(this); + DatabaseHelper.initializeActiveAndroid(); } @Override diff --git a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java index cd411328f..03f66ec5a 100644 --- a/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java +++ b/app/src/main/java/org/isoron/uhabits/helpers/DatabaseHelper.java @@ -149,8 +149,11 @@ public class DatabaseHelper } @SuppressWarnings("unchecked") - public static void initializeActiveAndroid(Context context) + 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) diff --git a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java index 74d6dc7bb..8f1d6b433 100644 --- a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java @@ -19,15 +19,12 @@ package org.isoron.uhabits.io; -import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.NonNull; import com.activeandroid.ActiveAndroid; -import org.isoron.uhabits.BuildConfig; -import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.helpers.DatabaseHelper; import java.io.File; @@ -57,12 +54,11 @@ public class LoopDBImporter extends AbstractImporter public void importHabitsFromFile(@NonNull File file) throws IOException { ActiveAndroid.dispose(); - Context context = HabitsApplication.getContext(); File originalDB = DatabaseHelper.getDatabaseFile(); - File backupDir = DatabaseHelper.getFilesDir(context, "Backups"); + File backupDir = DatabaseHelper.getFilesDir("Backups"); DatabaseHelper.saveDatabaseCopy(backupDir); DatabaseHelper.copy(file, originalDB); - DatabaseHelper.initializeActiveAndroid(context); + DatabaseHelper.initializeActiveAndroid(); } } From 790beb185aa6b07450339c639d21d80e95a44450 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 25 Mar 2016 08:09:33 -0400 Subject: [PATCH 25/27] Do not save backup when importing Loop DB --- app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java index 8f1d6b433..27b7ecb15 100644 --- a/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java +++ b/app/src/main/java/org/isoron/uhabits/io/LoopDBImporter.java @@ -55,9 +55,6 @@ public class LoopDBImporter extends AbstractImporter { ActiveAndroid.dispose(); File originalDB = DatabaseHelper.getDatabaseFile(); - File backupDir = DatabaseHelper.getFilesDir("Backups"); - - DatabaseHelper.saveDatabaseCopy(backupDir); DatabaseHelper.copy(file, originalDB); DatabaseHelper.initializeActiveAndroid(); } From 62df660079bb301c253e2cbf327351f276123732 Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 25 Mar 2016 08:10:10 -0400 Subject: [PATCH 26/27] Implement UI tests for import/export and help --- .../java/org/isoron/uhabits/ui/MainTest.java | 136 +++++++++++++++++- .../java/org/isoron/uhabits/MainActivity.java | 9 ++ .../uhabits/fragments/ListHabitsFragment.java | 9 -- 3 files changed, 138 insertions(+), 16 deletions(-) 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..0dc698227 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.hamcrest.core.AnyOf.anyOf; 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); + + onView(withText(R.string.export_full_backup)).perform(click()); + intended(hasAction(Intent.ACTION_SEND)); + + deleteHabit(name); + + openActionBarOverflowOrOptionsMenu(targetContext); + onView(withText(R.string.settings)).perform(click()); + onView(withText(R.string.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()); + onView(withText(R.string.export_to_csv)).perform(click()); + intended(hasAction(Intent.ACTION_SEND)); + } } diff --git a/app/src/main/java/org/isoron/uhabits/MainActivity.java b/app/src/main/java/org/isoron/uhabits/MainActivity.java index 7cc4e54e3..701bc175d 100644 --- a/app/src/main/java/org/isoron/uhabits/MainActivity.java +++ b/app/src/main/java/org/isoron/uhabits/MainActivity.java @@ -142,6 +142,15 @@ public class MainActivity extends ReplayableActivity 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); } 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 02ca46b92..4185c9b6b 100644 --- a/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java +++ b/app/src/main/java/org/isoron/uhabits/fragments/ListHabitsFragment.java @@ -230,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); } From d176ea91fb5e515442b7e016d63180990c88b59c Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Fri, 25 Mar 2016 09:04:07 -0400 Subject: [PATCH 27/27] Use custom matcher for settings activity --- .../org/isoron/uhabits/ui/HabitMatchers.java | 21 +++++++++++++++++++ .../java/org/isoron/uhabits/ui/MainTest.java | 8 +++---- 2 files changed, 25 insertions(+), 4 deletions(-) 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 0dc698227..4d2066501 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/MainTest.java @@ -69,7 +69,7 @@ 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.hamcrest.core.AnyOf.anyOf; +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; @@ -312,14 +312,14 @@ public class MainTest String date = DateHelper.getBackupDateFormat().format(DateHelper.getLocalTime()); date = date.substring(0, date.length() - 2); - onView(withText(R.string.export_full_backup)).perform(click()); + onData(isPreferenceWithText("Export full backup")).perform(click()); intended(hasAction(Intent.ACTION_SEND)); deleteHabit(name); openActionBarOverflowOrOptionsMenu(targetContext); onView(withText(R.string.settings)).perform(click()); - onView(withText(R.string.import_data)).perform(click()); + onData(isPreferenceWithText("Import data")).perform(click()); onData(allOf(is(instanceOf(String.class)), startsWith("Backups"))) .perform(click()); @@ -340,7 +340,7 @@ public class MainTest addHabit(); openActionBarOverflowOrOptionsMenu(targetContext); onView(withText(R.string.settings)).perform(click()); - onView(withText(R.string.export_to_csv)).perform(click()); + onData(isPreferenceWithText("Export as CSV")).perform(click()); intended(hasAction(Intent.ACTION_SEND)); } }