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 @@
+
+
+
+
+
+
+
+
+
+
+
+