From 581197be035dd870187c471672c3e517a3ba105d Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Mon, 21 Mar 2016 14:00:51 -0400 Subject: [PATCH] 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 @@ + + + + + + + + + + + +