Weekend mode

fixes #63

Weekend notification delay

Weekend notification delay
It should be possible to configure an "earliest notification time" for weekends or configurable days. This setting will result in all earlier reminders for that day to be scheduled at that time (like a "do not disturb" setting).

./gradlew :app:testDebugUnitTest --tests "com.futsch1.medtimer.WeekendModeTest"

diff --git a/app/src/main/java/com/futsch1/medtimer/PreferencesActivity.java b/app/src/main/java/com/futsch1/medtimer/PreferencesActivity.java
index 701f5cbed..584e2f9eb 100644
--- a/app/src/main/java/com/futsch1/medtimer/PreferencesActivity.java
+++ b/app/src/main/java/com/futsch1/medtimer/PreferencesActivity.java
@@ -27,7 +27,11 @@ protected void onCreate(Bundle savedInstanceState) {
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         if (item.getItemId() == android.R.id.home) {
-            finish();
+            if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
+                getSupportFragmentManager().popBackStack();
+            } else {
+                finish();
+            }
             return true;
         }
         return super.onOptionsItemSelected(item);
diff --git a/app/src/main/java/com/futsch1/medtimer/WeekendModePreferencesFragment.java b/app/src/main/java/com/futsch1/medtimer/WeekendModePreferencesFragment.java
new file mode 100644
index 000000000..da1c1ad56
--- /dev/null
+++ b/app/src/main/java/com/futsch1/medtimer/WeekendModePreferencesFragment.java
@@ -0,0 +1,77 @@
+package com.futsch1.medtimer;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+
+import androidx.preference.MultiSelectListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceManager;
+
+import com.futsch1.medtimer.helpers.TimeHelper;
+import com.futsch1.medtimer.reminders.ReminderProcessor;
+import com.takisoft.preferencex.PreferenceFragmentCompat;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class WeekendModePreferencesFragment extends PreferenceFragmentCompat {
+
+    public static final String WEEKEND_TIME = "weekend_time";
+    public static final String WEEKEND_MODE = "weekend_mode";
+    public static final String WEEKEND_DAYS = "weekend_days";
+
+    @Override
+    public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
+        setPreferencesFromResource(R.xml.weekend_mode_preferences, rootKey);
+
+        setupWeekendMode();
+        setupTimePicker();
+        setupDays();
+    }
+
+    private void setupWeekendMode() {
+        Preference preference = getPreferenceScreen().findPreference(WEEKEND_MODE);
+        if (preference != null) {
+            preference.setOnPreferenceChangeListener((preference1, newValue) -> {
+                requestReschedule();
+                return true;
+            });
+        }
+    }
+
+    private void setupTimePicker() {
+        Preference preference = getPreferenceScreen().findPreference(WEEKEND_TIME);
+        if (preference != null) {
+            SharedPreferences defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
+            preference.setSummary(TimeHelper.minutesToTime(defaultSharedPreferences.getInt(WEEKEND_TIME, 540)));
+            preference.setOnPreferenceClickListener(preference1 -> {
+                int weekendTime = defaultSharedPreferences.getInt(WEEKEND_TIME, 540);
+                new TimeHelper.TimePickerWrapper(requireContext()).show(weekendTime / 60, weekendTime % 60, minutes -> {
+                    defaultSharedPreferences.edit().putInt(WEEKEND_TIME, minutes).apply();
+                    preference1.setSummary(TimeHelper.minutesToTime(minutes));
+                    requestReschedule();
+                });
+                return true;
+            });
+        }
+    }
+
+    private void setupDays() {
+        Preference preference = getPreferenceScreen().findPreference(WEEKEND_DAYS);
+        if (preference != null) {
+            preference.setSummaryProvider((Preference.SummaryProvider<MultiSelectListPreference>) preference1 -> {
+                @SuppressWarnings("java:S6204") // Using SDK 33
+                List<CharSequence> values = preference1.getValues().stream().map(s -> preference1.getEntries()[Integer.parseInt(s) - 1]).collect(Collectors.toList());
+                return String.join(", ", values);
+            });
+            preference.setOnPreferenceChangeListener((preference12, newValue) -> {
+                requestReschedule();
+                return true;
+            });
+        }
+    }
+
+    private void requestReschedule() {
+        ReminderProcessor.requestReschedule(requireContext());
+    }
+}
diff --git a/app/src/main/java/com/futsch1/medtimer/helpers/TimeHelper.java b/app/src/main/java/com/futsch1/medtimer/helpers/TimeHelper.java
index 84ae901cc..ada99cde5 100644
--- a/app/src/main/java/com/futsch1/medtimer/helpers/TimeHelper.java
+++ b/app/src/main/java/com/futsch1/medtimer/helpers/TimeHelper.java
@@ -1,5 +1,9 @@
 package com.futsch1.medtimer.helpers;

+import android.app.TimePickerDialog;
+import android.content.Context;
+import android.text.format.DateFormat;
+
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
@@ -13,4 +17,21 @@ private TimeHelper() {
     public static String minutesToTime(long minutes) {
         return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(LocalTime.of((int) (minutes / 60), (int) (minutes % 60)));
     }
+
+    public interface TimePickerResult {
+        void onTimeSelected(int minutes);
+    }
+
+    public static class TimePickerWrapper {
+        Context context;
+
+        public TimePickerWrapper(Context context) {
+            this.context = context;
+        }
+
+        public void show(int hourOfDay, int minute, TimePickerResult timePickerResult) {
+            TimePickerDialog timePickerDialog = new TimePickerDialog(context, (view, hourOfDayLocal, minuteLocal) -> timePickerResult.onTimeSelected(hourOfDayLocal * 60 + minuteLocal), hourOfDay, minute, DateFormat.is24HourFormat(context));
+            timePickerDialog.show();
+        }
+    }
 }
diff --git a/app/src/main/java/com/futsch1/medtimer/medicine/EditMedicine.java b/app/src/main/java/com/futsch1/medtimer/medicine/EditMedicine.java
index 99bf77f78..37d20b555 100644
--- a/app/src/main/java/com/futsch1/medtimer/medicine/EditMedicine.java
+++ b/app/src/main/java/com/futsch1/medtimer/medicine/EditMedicine.java
@@ -5,7 +5,6 @@
 import static com.futsch1.medtimer.ActivityCodes.EXTRA_MEDICINE_NAME;
 import static com.futsch1.medtimer.ActivityCodes.EXTRA_USE_COLOR;

-import android.app.TimePickerDialog;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.graphics.Color;
@@ -13,7 +12,6 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
-import android.text.format.DateFormat;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.EditText;
@@ -34,6 +32,7 @@
 import com.futsch1.medtimer.helpers.DeleteHelper;
 import com.futsch1.medtimer.helpers.DialogHelper;
 import com.futsch1.medtimer.helpers.SwipeHelper;
+import com.futsch1.medtimer.helpers.TimeHelper;
 import com.futsch1.medtimer.helpers.ViewColorHelper;
 import com.google.android.material.button.MaterialButton;
 import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;
@@ -160,11 +159,10 @@ private void createReminder(String amount) {
         reminder.amount = amount;
         reminder.createdTimestamp = Instant.now().toEpochMilli() / 1000;

-        TimePickerDialog timePickerDialog = new TimePickerDialog(this, (tpView, hourOfDay, minute) -> {
-            reminder.timeInMinutes = hourOfDay * 60 + minute;
+        new TimeHelper.TimePickerWrapper(getApplicationContext()).show(0, 0, minutes -> {
+            reminder.timeInMinutes = minutes;
             medicineViewModel.insertReminder(reminder);
-        }, 8, 0, DateFormat.is24HourFormat(getApplicationContext()));
-        timePickerDialog.show();
+        });
     }

     @Override
diff --git a/app/src/main/java/com/futsch1/medtimer/medicine/ReminderViewHolder.java b/app/src/main/java/com/futsch1/medtimer/medicine/ReminderViewHolder.java
index e06026293..ec2da1b13 100644
--- a/app/src/main/java/com/futsch1/medtimer/medicine/ReminderViewHolder.java
+++ b/app/src/main/java/com/futsch1/medtimer/medicine/ReminderViewHolder.java
@@ -4,10 +4,8 @@

 import android.annotation.SuppressLint;
 import android.app.AlertDialog;
-import android.app.TimePickerDialog;
 import android.content.Context;
 import android.content.SharedPreferences;
-import android.text.format.DateFormat;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -19,6 +17,7 @@

 import com.futsch1.medtimer.R;
 import com.futsch1.medtimer.database.Reminder;
+import com.futsch1.medtimer.helpers.TimeHelper;
 import com.google.android.material.button.MaterialButton;

 public class ReminderViewHolder extends RecyclerView.ViewHolder {
@@ -55,15 +54,13 @@ public void bind(Reminder reminder, DeleteCallback deleteCallback) {
         editTime.setText(minutesToTime(reminder.timeInMinutes));
         editDaysBetweenReminders.setText(Integer.toString(reminder.daysBetweenReminders));

-        TimePickerDialog timePickerDialog = new TimePickerDialog(editTime.getContext(), (view, hourOfDay, minute) -> {
-            String selectedTime = minutesToTime(hourOfDay * 60L + minute);
-            editTime.setText(selectedTime);
-            reminder.timeInMinutes = hourOfDay * 60 + minute;
-        }, 0, 0, DateFormat.is24HourFormat(editTime.getContext()));
         editTime.setOnFocusChangeListener((v, hasFocus) -> {
             if (hasFocus) {
-                timePickerDialog.updateTime(reminder.timeInMinutes / 60, reminder.timeInMinutes % 60);
-                timePickerDialog.show();
+                new TimeHelper.TimePickerWrapper(editTime.getContext()).show(reminder.timeInMinutes / 60, reminder.timeInMinutes % 60, minutes -> {
+                    String selectedTime = minutesToTime(minutes);
+                    editTime.setText(selectedTime);
+                    reminder.timeInMinutes = minutes;
+                });
             }
         });

diff --git a/app/src/main/java/com/futsch1/medtimer/reminders/RescheduleWork.java b/app/src/main/java/com/futsch1/medtimer/reminders/RescheduleWork.java
index 64a4bc978..143a91df6 100644
--- a/app/src/main/java/com/futsch1/medtimer/reminders/RescheduleWork.java
+++ b/app/src/main/java/com/futsch1/medtimer/reminders/RescheduleWork.java
@@ -34,10 +34,12 @@
 public class RescheduleWork extends Worker {
     protected final Context context;
     private final AlarmManager alarmManager;
+    private final WeekendMode weekendMode;

     public RescheduleWork(@NonNull Context context, @NonNull WorkerParameters workerParams) {
         super(context, workerParams);
         this.context = context;
+        this.weekendMode = new WeekendMode(PreferenceManager.getDefaultSharedPreferences(context));
         alarmManager = context.getSystemService(AlarmManager.class);
     }

@@ -66,6 +68,8 @@ public LocalDate localDate() {
     }

     protected void schedule(Instant timestamp, int reminderId, String medicineName, int requestCode, int reminderEventId) {
+        // Apply weekend mode shift
+        timestamp = weekendMode.adjustInstant(timestamp);
         // If the alarm is in the future, schedule with alarm manager
         if (timestamp.isAfter(Instant.now())) {
             PendingIntent pendingIntent = getPendingIntent(context, reminderId, requestCode, reminderEventId);
diff --git a/app/src/main/java/com/futsch1/medtimer/reminders/WeekendMode.java b/app/src/main/java/com/futsch1/medtimer/reminders/WeekendMode.java
new file mode 100644
index 000000000..114eda91d
--- /dev/null
+++ b/app/src/main/java/com/futsch1/medtimer/reminders/WeekendMode.java
@@ -0,0 +1,40 @@
+package com.futsch1.medtimer.reminders;
+
+import android.content.SharedPreferences;
+import android.util.ArraySet;
+
+import com.futsch1.medtimer.WeekendModePreferencesFragment;
+
+import java.time.DayOfWeek;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Set;
+
+public class WeekendMode {
+
+    private final SharedPreferences preferences;
+
+    public WeekendMode(SharedPreferences preferences) {
+        this.preferences = preferences;
+    }
+
+    public Instant adjustInstant(Instant instant) {
+        if (isWeekendModeEnabled()) {
+            int weekendTime = preferences.getInt(WeekendModePreferencesFragment.WEEKEND_TIME, 540);
+            Set<String> weekendDays = preferences.getStringSet(WeekendModePreferencesFragment.WEEKEND_DAYS, new ArraySet<>());
+            ZonedDateTime localDateTime = instant.atZone(ZoneId.systemDefault());
+            DayOfWeek dayOfWeek = localDateTime.getDayOfWeek();
+            int minutes = localDateTime.getMinute() + localDateTime.getHour() * 60;
+            int deltaMinutes = weekendTime - minutes;
+            if (weekendDays.contains(String.valueOf(dayOfWeek.getValue())) && deltaMinutes > 0) {
+                instant = instant.plusSeconds(deltaMinutes * 60L);
+            }
+        }
+        return instant;
+    }
+
+    private boolean isWeekendModeEnabled() {
+        return preferences.getBoolean(WeekendModePreferencesFragment.WEEKEND_MODE, false);
+    }
+}
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 1dce31c5b..f08225588 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -92,4 +92,14 @@
     <string name="minutes_60">60 Minuten</string>
     <string name="notification_skipped">ÜBERSPRUNGEN</string>
     <string name="notification_snooze">SNOOZE</string>
+    <string name="weekend_mode">Wochenend-Modus</string>
+    <string name="weekend_mode_days">Tage</string>
+    <string name="monday">Montag</string>
+    <string name="tuesday">Dienstag</string>
+    <string name="wednesday">Mittwoch</string>
+    <string name="thursday">Donnerstag</string>
+    <string name="friday">Freitag</string>
+    <string name="saturday">Samstag</string>
+    <string name="sunday">Sonntag</string>
+    <string name="weekend_mode_summary">Verzögerung von Benachrichtigungen an bestimmten Wochentagen bis zu einer bestimmten Uhrzeit</string>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 576666089..97d8048f9 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -91,4 +91,14 @@
     <string name="minutes_60">60 minutos</string>
     <string name="notification_skipped">OMITIDO</string>
     <string name="notification_snooze">SNOOZE</string>
+    <string name="weekend_mode">Modo fin de semana</string>
+    <string name="weekend_mode_days">Días</string>
+    <string name="monday">Lunes</string>
+    <string name="tuesday">Martes</string>
+    <string name="wednesday">Miércoles</string>
+    <string name="thursday">Jueves</string>
+    <string name="friday">Viernes</string>
+    <string name="saturday">Sábado</string>
+    <string name="sunday">Domingo</string>
+    <string name="weekend_mode_summary">Retrasar las notificaciones de ciertos días de la semana hasta una hora determinada</string>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index af4d8e5e2..048ea7230 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -54,4 +54,26 @@
         <item>30</item>
         <item>60</item>
     </string-array>
+    <string-array name="days">
+        <item>@string/monday</item>
+        <item>@string/tuesday</item>
+        <item>@string/wednesday</item>
+        <item>@string/thursday</item>
+        <item>@string/friday</item>
+        <item>@string/saturday</item>
+        <item>@string/sunday</item>
+    </string-array>
+    <string-array name="one_to_seven">
+        <item>1</item>
+        <item>2</item>
+        <item>3</item>
+        <item>4</item>
+        <item>5</item>
+        <item>6</item>
+        <item>7</item>
+    </string-array>
+    <string-array name="default_weekend_days">
+        <item>6</item>
+        <item>7</item>
+    </string-array>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fa4040c12..af96f20c6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -95,4 +95,14 @@
     <string name="minutes_60">60 minutes</string>
     <string name="notification_skipped">SKIPPED</string>
     <string name="notification_snooze">SNOOZE</string>
+    <string name="weekend_mode">Weekend mode</string>
+    <string name="weekend_mode_days">Days</string>
+    <string name="monday">Monday</string>
+    <string name="tuesday">Tuesday</string>
+    <string name="wednesday">Wednesday</string>
+    <string name="thursday">Thursday</string>
+    <string name="friday">Friday</string>
+    <string name="saturday">Saturday</string>
+    <string name="sunday">Sunday</string>
+    <string name="weekend_mode_summary">Delay notifications on certain days of the week until a given time</string>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index d9ddca94d..ceb6e68d8 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -34,6 +34,11 @@
         android:summary="@string/exact_reminders_summary"
         android:title="@string/exact_reminders"
         app:key="exact_reminders" />
+    <Preference
+        app:fragment="com.futsch1.medtimer.WeekendModePreferencesFragment"
+        app:key="weekend_mode"
+        app:summary="@string/weekend_mode_summary"
+        app:title="@string/weekend_mode" />
     <ListPreference
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/app/src/main/res/xml/weekend_mode_preferences.xml b/app/src/main/res/xml/weekend_mode_preferences.xml
new file mode 100644
index 000000000..3ffdc4a40
--- /dev/null
+++ b/app/src/main/res/xml/weekend_mode_preferences.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <SwitchPreferenceCompat
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:defaultValue="false"
+        app:key="weekend_mode"
+        app:title="@string/weekend_mode" />
+    <MultiSelectListPreference
+        app:defaultValue="@array/default_weekend_days"
+        app:entries="@array/days"
+        app:entryValues="@array/one_to_seven"
+        app:key="weekend_days"
+        app:title="@string/weekend_mode_days" />
+    <Preference
+        android:defaultValue="540"
+        app:key="weekend_time"
+        app:title="@string/time" />
+
+</androidx.preference.PreferenceScreen>
\ No newline at end of file

diff --git a/app/src/test/java/com/futsch1/medtimer/WeekendModeTest.java b/app/src/test/java/com/futsch1/medtimer/WeekendModeTest.java
new file mode 100644
index 000000000..2e86d4147
--- /dev/null
+++ b/app/src/test/java/com/futsch1/medtimer/WeekendModeTest.java
@@ -0,0 +1,118 @@
+package com.futsch1.medtimer;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.SharedPreferences;
+import android.util.ArraySet;
+
+import com.futsch1.medtimer.reminders.WeekendMode;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+
+import java.time.DayOfWeek;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(RobolectricTestRunner.class)
+public class WeekendModeTest {
+    @Test
+    public void test_adjustInstant_ReturnsSameInstant_WeekendModeDisabled() {
+        SharedPreferences preferences = Mockito.mock(SharedPreferences.class);
+        WeekendMode weekendMode = new WeekendMode(preferences);
+        Instant instant = Instant.now();
+
+        Mockito.when(preferences.getBoolean(eq(WeekendModePreferencesFragment.WEEKEND_MODE), anyBoolean())).thenReturn(false);
+
+        Instant adjustedInstant = weekendMode.adjustInstant(instant);
+
+        assertEquals(instant, adjustedInstant);
+    }
+
+    @Test
+    public void test_adjustInstant_ReturnsSameInstant_WeekendDaysEmpty() {
+        SharedPreferences preferences = Mockito.mock(SharedPreferences.class);
+        WeekendMode weekendMode = new WeekendMode(preferences);
+        Instant instant = Instant.now();
+
+        Mockito.when(preferences.getBoolean(WeekendModePreferencesFragment.WEEKEND_MODE, false)).thenReturn(true);
+        Mockito.when(preferences.getStringSet(WeekendModePreferencesFragment.WEEKEND_DAYS, new ArraySet<>())).thenReturn(new HashSet<>());
+
+        Instant adjustedInstant = weekendMode.adjustInstant(instant);
+
+        assertEquals(instant, adjustedInstant);
+    }
+
+    @Test
+    public void test_adjustInstant_ReturnsSameInstant_CurrentDayIsNotWeekend() {
+        SharedPreferences preferences = Mockito.mock(SharedPreferences.class);
+        Instant instant = Mockito.mock(Instant.class);
+        WeekendMode weekendMode = new WeekendMode(preferences);
+        ZonedDateTime testZonedDateTime = ZonedDateTime.of(2024, 4, 6, 8, 0, 0, 0, ZoneId.of("Z"));
+
+        Mockito.when(preferences.getBoolean(eq(WeekendModePreferencesFragment.WEEKEND_MODE), anyBoolean())).thenReturn(true);
+        Mockito.when(preferences.getInt(eq(WeekendModePreferencesFragment.WEEKEND_TIME), anyInt())).thenReturn(10 * 60);
+        Mockito.when(instant.atZone(any())).thenReturn(testZonedDateTime);
+        Set<String> weekendDays = new HashSet<>();
+        weekendDays.add(String.valueOf(DayOfWeek.TUESDAY.getValue()));
+        weekendDays.add(String.valueOf(DayOfWeek.WEDNESDAY.getValue()));
+        Mockito.when(preferences.getStringSet(WeekendModePreferencesFragment.WEEKEND_DAYS, new ArraySet<>())).thenReturn(weekendDays);
+
+        Instant adjustedInstant = weekendMode.adjustInstant(instant);
+
+        assertEquals(instant, adjustedInstant);
+    }
+
+    @Test
+    public void test_adjustInstant_ReturnsAdjustedInstant_WeekendModeEnabled_CurrentDayIsWeekend() {
+        SharedPreferences preferences = Mockito.mock(SharedPreferences.class);
+        Instant instant = Mockito.mock(Instant.class);
+        WeekendMode weekendMode = new WeekendMode(preferences);
+        ZonedDateTime testZonedDateTime = ZonedDateTime.of(2024, 4, 6, 8, 0, 0, 0, ZoneId.of("Z"));
+
+        Mockito.when(preferences.getBoolean(eq(WeekendModePreferencesFragment.WEEKEND_MODE), anyBoolean())).thenReturn(true);
+        Mockito.when(preferences.getInt(eq(WeekendModePreferencesFragment.WEEKEND_TIME), anyInt())).thenReturn(10 * 60);
+        Mockito.when(instant.atZone(any())).thenReturn(testZonedDateTime);
+        Set<String> weekendDays = new HashSet<>();
+        weekendDays.add(String.valueOf(DayOfWeek.SATURDAY.getValue()));
+        weekendDays.add(String.valueOf(DayOfWeek.SUNDAY.getValue()));
+        Mockito.when(preferences.getStringSet(WeekendModePreferencesFragment.WEEKEND_DAYS, new ArraySet<>())).thenReturn(weekendDays);
+
+        weekendMode.adjustInstant(instant);
+
+        verify(instant, times(1)).plusSeconds(2 * 60 * 60);
+    }
+
+    @Test
+    public void test_adjustInstant_ReturnsAdjustedInstant_WeekendModeEnabled_CurrentDayIsWeekendReminderIsLater() {
+        SharedPreferences preferences = Mockito.mock(SharedPreferences.class);
+        Instant instant = Mockito.mock(Instant.class);
+        WeekendMode weekendMode = new WeekendMode(preferences);
+        ZonedDateTime testZonedDateTime = ZonedDateTime.of(2024, 4, 6, 11, 0, 0, 0, ZoneId.of("Z"));
+
+        Mockito.when(preferences.getBoolean(eq(WeekendModePreferencesFragment.WEEKEND_MODE), anyBoolean())).thenReturn(true);
+        Mockito.when(preferences.getInt(eq(WeekendModePreferencesFragment.WEEKEND_TIME), anyInt())).thenReturn(10 * 60);
+        Mockito.when(instant.atZone(any())).thenReturn(testZonedDateTime);
+        Set<String> weekendDays = new HashSet<>();
+        weekendDays.add(String.valueOf(DayOfWeek.SATURDAY.getValue()));
+        weekendDays.add(String.valueOf(DayOfWeek.SUNDAY.getValue()));
+        Mockito.when(preferences.getStringSet(WeekendModePreferencesFragment.WEEKEND_DAYS, new ArraySet<>())).thenReturn(weekendDays);
+
+        weekendMode.adjustInstant(instant);
+
+        verify(instant, times(0)).plusSeconds(anyLong());
+    }
+}
+


Json backup

fixes #49

Export/import med settings/data plan

Export/import med settings/data plan
Unfortunately the app (1.4.2) crashed at one point so bad, that all reinstalling didn't help. Not sure if it was a Xiaomi issue. First notifications stopped. Then after checking and editing some energy saving mode, the app didn't start any more at all.

What helped was a simple uninstall and install.

To prevent in the future a full new creation and configuration of the medication plan, a Export/import function would really be helpful.

Thanks, Michael

I am sorry that the app stopped working on your device. I haven't experienced a similar behavior and have not yet received any crash reports via the Google Play dashboard.

However, I will look into your feature request to support an export and import of the settings in one of the next releases.
Was installed via f-droid and crash log was only provided to be sent to Xiaomi... If it happens again (which we don't hope) I can provide the log as well.

Anyway, thanks for considering the feature request! :)

./gradlew :app:testDebugUnitTest --tests "com.futsch1.medtimer.JSONBackupUnitTest"

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 21d0ca638..1515faa4b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -42,6 +42,11 @@ android {
     androidResources {
         generateLocaleConfig = true
     }
+    testOptions {
+        unitTests {
+            isIncludeAndroidResources = true
+        }
+    }
 }

 dependencies {
@@ -65,9 +70,11 @@ dependencies {
     implementation("com.takisoft.preferencex:preferencex:$preferenceXVersion")
     implementation("com.takisoft.preferencex:preferencex-ringtone:$preferenceXVersion")
     implementation("com.github.wwdablu:SimplyPDF:2.1.1")
+    implementation("com.google.code.gson:gson:2.10.1")

     testImplementation("junit:junit:4.13.2")
     testImplementation("org.mockito:mockito-inline:5.2.0")
+    testImplementation("org.robolectric:robolectric:4.11.1")

     androidTestImplementation("androidx.test.ext:junit:1.1.5")
     androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e395cce81..caf7c2d39 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -23,6 +23,10 @@
         android:roundIcon="@mipmap/logo_round"
         android:supportsRtl="true"
         android:theme="@style/Theme.MedTimer">
+        <activity
+            android:name=".PreferencesActivity"
+            android:exported="false"
+            android:label="@string/tab_settings" />

         <service
             android:name=".ReminderSchedulerService"
diff --git a/app/src/main/java/com/futsch1/medtimer/MainActivity.java b/app/src/main/java/com/futsch1/medtimer/MainActivity.java
index eda3f9c48..c3fd54a86 100644
--- a/app/src/main/java/com/futsch1/medtimer/MainActivity.java
+++ b/app/src/main/java/com/futsch1/medtimer/MainActivity.java
@@ -2,10 +2,13 @@

 import static android.Manifest.permission.POST_NOTIFICATIONS;

+import android.app.Activity;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuInflater;

 import androidx.activity.result.ActivityResultLauncher;
 import androidx.activity.result.contract.ActivityResultContracts;
@@ -29,6 +32,13 @@ public class MainActivity extends AppCompatActivity {
     TabLayout tabLayout;
     ViewPager2 viewPager;
     ViewPagerAdapter viewPagerAdapter;
+    OptionsMenu optionsMenu;
+    private final ActivityResultLauncher<Intent> requestFileLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
+        if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
+            optionsMenu.fileSelected(result.getData().getData());
+        }
+    });
+

     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -67,4 +77,19 @@ private void checkPermissions() {
             requestPermissionLauncher.launch(POST_NOTIFICATIONS);
         }
     }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        optionsMenu.onDestroy();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.main, menu);
+        menu.setGroupDividerEnabled(true);
+        optionsMenu = new OptionsMenu(this, menu, new MedicineViewModel(getApplication()), requestFileLauncher);
+        return true;
+    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futsch1/medtimer/OptionsMenu.java b/app/src/main/java/com/futsch1/medtimer/OptionsMenu.java
new file mode 100644
index 000000000..ef7dbccee
--- /dev/null
+++ b/app/src/main/java/com/futsch1/medtimer/OptionsMenu.java
@@ -0,0 +1,214 @@
+package com.futsch1.medtimer;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.core.content.FileProvider;
+
+import com.futsch1.medtimer.database.JSONBackup;
+import com.futsch1.medtimer.database.MedicineWithReminders;
+import com.futsch1.medtimer.exporters.CSVExport;
+import com.futsch1.medtimer.exporters.Exporter;
+import com.futsch1.medtimer.exporters.PDFExport;
+import com.futsch1.medtimer.helpers.FileHelper;
+import com.futsch1.medtimer.helpers.PathHelper;
+import com.futsch1.medtimer.reminders.ReminderProcessor;
+
+import java.io.File;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.TimeZone;
+
+public class OptionsMenu {
+    private final Menu menu;
+    private final Context context;
+    private final MedicineViewModel medicineViewModel;
+    private final HandlerThread backgroundThread;
+    private final ActivityResultLauncher<Intent> openFileLauncher;
+
+    public OptionsMenu(Context context, Menu menu, MedicineViewModel medicineViewModel, ActivityResultLauncher<Intent> openFileLauncher) {
+        this.menu = menu;
+        this.context = context;
+        this.medicineViewModel = medicineViewModel;
+        this.openFileLauncher = openFileLauncher;
+        backgroundThread = new HandlerThread("Export");
+        backgroundThread.start();
+
+        setupSettings();
+        setupVersion();
+        setupAppURL();
+        setupClearEvents();
+        setupExport();
+        setupGenerateTestData();
+        setupBackup();
+    }
+
+    private void setupSettings() {
+        MenuItem item = menu.findItem(R.id.settings);
+        item.setOnMenuItemClickListener(menuItem -> {
+            Intent intent = new Intent(context, PreferencesActivity.class);
+            context.startActivity(intent);
+            return true;
+        });
+    }
+
+    private void setupVersion() {
+        MenuItem item = menu.findItem(R.id.version);
+        item.setTitle(context.getString(R.string.version, BuildConfig.VERSION_NAME));
+    }
+
+    private void setupAppURL() {
+        MenuItem item = menu.findItem(R.id.app_url);
+        item.setOnMenuItemClickListener(menuItem -> {
+            Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Futsch1/medTimer"));
+            context.startActivity(myIntent);
+            return true;
+        });
+    }
+
+    private void setupClearEvents() {
+        MenuItem item = menu.findItem(R.id.clear_events);
+        item.setOnMenuItemClickListener(menuItem -> {
+            AlertDialog.Builder builder = new AlertDialog.Builder(context);
+            builder.setTitle(R.string.confirm);
+            builder.setMessage(R.string.are_you_sure_delete_events);
+            builder.setCancelable(false);
+            builder.setPositiveButton(R.string.yes, (dialogInterface, i) -> medicineViewModel.deleteReminderEvents());
+            builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> { // Intentionally left empty
+            });
+            builder.show();
+            ReminderProcessor.requestReschedule(context);
+            return true;
+        });
+    }
+
+    private void setupExport() {
+        Handler handler = new Handler(backgroundThread.getLooper());
+
+        MenuItem item = menu.findItem(R.id.export_csv);
+        item.setOnMenuItemClickListener(menuItem -> {
+            handler.post(() -> export(new CSVExport(medicineViewModel.medicineRepository.getAllReminderEvents(), context, TimeZone.getDefault().toZoneId())));
+            return true;
+        });
+        item = menu.findItem(R.id.export_pdf);
+        item.setOnMenuItemClickListener(menuItem -> {
+            handler.post(() -> export(new PDFExport(medicineViewModel.medicineRepository.getAllReminderEvents(), context, TimeZone.getDefault().toZoneId())));
+            return true;
+        });
+    }
+
+    void setupGenerateTestData() {
+        MenuItem item = menu.findItem(R.id.generate_test_data);
+        if (BuildConfig.DEBUG) {
+            item.setVisible(true);
+            item.setOnMenuItemClickListener(menuItem -> {
+                final Handler handler = new Handler(backgroundThread.getLooper());
+                handler.post(() -> {
+                    GenerateTestData generateTestData = new GenerateTestData(medicineViewModel);
+                    generateTestData.deleteAll();
+                    generateTestData.generateTestMedicine();
+                });
+                return true;
+            });
+        } else {
+            item.setVisible(false);
+        }
+    }
+
+    private void setupBackup() {
+        Handler handler = new Handler(backgroundThread.getLooper());
+
+        MenuItem item = menu.findItem(R.id.perform_backup);
+        item.setOnMenuItemClickListener(menuItem -> {
+            JSONBackup jsonBackup = new JSONBackup();
+            handler.post(() -> {
+                String jsonContent = jsonBackup.createBackup(medicineViewModel.medicineRepository.getVersion(), medicineViewModel.medicineRepository.getMedicines());
+                createAndSave(jsonContent);
+            });
+            return true;
+        });
+
+        item = menu.findItem(R.id.restore_backup);
+        item.setOnMenuItemClickListener(menuItem -> {
+            new AlertDialog.Builder(context)
+                    .setTitle(R.string.restore)
+                    .setMessage(R.string.restore_start)
+                    .setCancelable(true)
+                    .setPositiveButton(R.string.ok, (dialog, which) -> openBackup())
+                    .setNegativeButton(R.string.cancel, (dialog, which) -> { // Intentionally left empty
+                    }).show();
+            return true;
+        });
+    }
+
+    private void export(Exporter exporter) {
+        File csvFile = new File(context.getCacheDir(), PathHelper.getExportFilename(exporter));
+        try {
+            exporter.export(csvFile);
+            shareFile(csvFile);
+        } catch (Exporter.ExporterException e) {
+            Toast.makeText(context, R.string.export_failed, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    private void createAndSave(String fileContent) {
+        File file = new File(context.getCacheDir(), PathHelper.getBackupFilename());
+        if (FileHelper.saveToFile(file, fileContent)) {
+            shareFile(file);
+        } else {
+            Toast.makeText(context, R.string.backup_failed, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    private void openBackup() {
+        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        intent.setType("application/json");
+        openFileLauncher.launch(intent);
+    }
+
+    private void shareFile(File file) {
+        Uri uri = FileProvider.getUriForFile(context, "com.futsch1.medtimer.fileprovider", file);
+        Intent intentShareFile = new Intent(Intent.ACTION_SEND);
+
+        intentShareFile.setDataAndType(uri, URLConnection.guessContentTypeFromName(file.getName()));
+        //Allow sharing apps to read the file Uri
+        intentShareFile.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        //Pass the file Uri instead of the path
+        intentShareFile.putExtra(Intent.EXTRA_STREAM,
+                uri);
+        context.startActivity(Intent.createChooser(intentShareFile, "Share File"));
+    }
+
+    public void onDestroy() {
+        backgroundThread.quitSafely();
+    }
+
+    public void fileSelected(Uri data) {
+        String json = FileHelper.readFromUri(data, context.getContentResolver());
+        boolean restoreSuccessful = false;
+        if (json != null) {
+            JSONBackup jsonBackup = new JSONBackup();
+            List<MedicineWithReminders> backupData = jsonBackup.parseBackup(json);
+            if (backupData != null) {
+                jsonBackup.applyBackup(backupData, medicineViewModel.medicineRepository);
+                restoreSuccessful = true;
+            }
+        }
+
+        new AlertDialog.Builder(context)
+                .setMessage(restoreSuccessful ? R.string.restore_successful : R.string.restore_failed)
+                .setPositiveButton(R.string.ok, (dialog, which) -> {
+                    // Intentionally empty
+                })
+                .show();
+    }
+}
diff --git a/app/src/main/java/com/futsch1/medtimer/PreferencesActivity.java b/app/src/main/java/com/futsch1/medtimer/PreferencesActivity.java
new file mode 100644
index 000000000..701f5cbed
--- /dev/null
+++ b/app/src/main/java/com/futsch1/medtimer/PreferencesActivity.java
@@ -0,0 +1,35 @@
+package com.futsch1.medtimer;
+
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+
+public class PreferencesActivity extends AppCompatActivity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.preferences_activity);
+        if (savedInstanceState == null) {
+            getSupportFragmentManager()
+                    .beginTransaction()
+                    .replace(R.id.settings, new PreferencesFragment())
+                    .commit();
+        }
+        ActionBar actionBar = getSupportActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/futsch1/medtimer/PreferencesFragment.java b/app/src/main/java/com/futsch1/medtimer/PreferencesFragment.java
index 74d07740c..6219424e7 100644
--- a/app/src/main/java/com/futsch1/medtimer/PreferencesFragment.java
+++ b/app/src/main/java/com/futsch1/medtimer/PreferencesFragment.java
@@ -4,95 +4,33 @@

 import android.app.AlarmManager;
 import android.app.AlertDialog;
-import android.app.Application;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.HandlerThread;
 import android.provider.Settings;
-import android.util.Log;

 import androidx.core.content.ContextCompat;
-import androidx.core.content.FileProvider;
-import androidx.lifecycle.ViewModelProvider;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceManager;
 import androidx.preference.SwitchPreferenceCompat;

-import com.futsch1.medtimer.database.MedicineRepository;
-import com.futsch1.medtimer.exporters.CSVExport;
-import com.futsch1.medtimer.exporters.Exporter;
-import com.futsch1.medtimer.exporters.PDFExport;
-import com.futsch1.medtimer.helpers.PathHelper;
-import com.futsch1.medtimer.reminders.ReminderProcessor;
 import com.takisoft.preferencex.PreferenceFragmentCompat;
 import com.takisoft.preferencex.RingtonePreference;

-import java.io.File;
-import java.net.URLConnection;
-import java.util.TimeZone;
-
 public class PreferencesFragment extends PreferenceFragmentCompat {
     public static final String EXACT_REMINDERS = "exact_reminders";

-    private MedicineViewModel medicineViewModel;
-    private HandlerThread backgroundThread;
-
     @Override
     public void onCreatePreferencesFix(Bundle savedInstanceState, String rootKey) {
         setPreferencesFromResource(R.xml.root_preferences, rootKey);

-        setupVersion();
-        setupAppURL();
-        setupClearEvents();
         setupShowNotifications();
         setupTheme();
         setupExactReminders();
-        setupExport();
-        setupGenerateTestData();
         setupNotificationTone();
     }

-    private void setupVersion() {
-        Preference preference = getPreferenceScreen().findPreference("version");
-        if (preference != null) {
-            preference.setTitle(getString(R.string.version, BuildConfig.VERSION_NAME));
-        }
-    }
-
-    private void setupAppURL() {
-        Preference preference = getPreferenceScreen().findPreference("app_url");
-        if (preference != null) {
-            preference.setOnPreferenceClickListener(preference12 -> {
-                Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/Futsch1/medTimer"));
-                startActivity(myIntent);
-                return true;
-            });
-        }
-    }
-
-    private void setupClearEvents() {
-        medicineViewModel = new ViewModelProvider(this).get(MedicineViewModel.class);
-
-        Preference preference = getPreferenceScreen().findPreference("clear_events");
-        if (preference != null) {
-            preference.setOnPreferenceClickListener(preference1 -> {
-                AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
-                builder.setTitle(R.string.confirm);
-                builder.setMessage(R.string.are_you_sure_delete_events);
-                builder.setCancelable(false);
-                builder.setPositiveButton(R.string.yes, (dialogInterface, i) -> medicineViewModel.deleteReminderEvents());
-                builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> {
-                });
-                builder.show();
-                ReminderProcessor.requestReschedule(requireContext());
-                return true;
-            });
-        }
-    }
-
     private void setupShowNotifications() {
         if (ContextCompat.checkSelfPermission(requireContext(), POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
             Preference preference = getPreferenceScreen().findPreference("show_notification");
@@ -143,52 +81,6 @@ private void setupExactReminders() {
         }
     }

-    void setupExport() {
-        backgroundThread = new HandlerThread("Export");
-        backgroundThread.start();
-
-        Preference preference = getPreferenceScreen().findPreference("export");
-        if (preference != null) {
-            preference.setOnPreferenceClickListener(preference1 -> {
-                AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
-                builder.setTitle(R.string.select_format);
-                builder.setItems(R.array.export_formats, (dialog, which) -> {
-                    final Handler handler = new Handler(backgroundThread.getLooper());
-                    handler.post(() -> {
-                        MedicineRepository medicineRepository = new MedicineRepository((Application) requireContext().getApplicationContext());
-                        if (which == 0) {
-                            export(new CSVExport(medicineRepository.getAllReminderEvents(), requireContext(), TimeZone.getDefault().toZoneId()));
-                        } else {
-                            export(new PDFExport(medicineRepository.getAllReminderEvents(), requireContext(), TimeZone.getDefault().toZoneId()));
-                        }
-                    });
-                });
-                builder.show();
-                return true;
-            });
-        }
-    }
-
-    void setupGenerateTestData() {
-        Preference preference = getPreferenceScreen().findPreference("generate_test_data");
-        if (preference != null) {
-            if (BuildConfig.DEBUG) {
-                preference.setVisible(true);
-                preference.setOnPreferenceClickListener(preference1 -> {
-                    final Handler handler = new Handler(backgroundThread.getLooper());
-                    handler.post(() -> {
-                        GenerateTestData generateTestData = new GenerateTestData(medicineViewModel);
-                        generateTestData.deleteAll();
-                        generateTestData.generateTestMedicine();
-                    });
-                    return true;
-                });
-            } else {
-                preference.setVisible(false);
-            }
-        }
-    }
-
     private void setupNotificationTone() {
         RingtonePreference preference = getPreferenceScreen().findPreference("notification_ringtone");
         if (preference != null) {
@@ -199,29 +91,6 @@ private void setupNotificationTone() {
         }
     }

-    private void export(Exporter exporter) {
-        File csvFile = new File(requireContext().getCacheDir(), PathHelper.getExportFilename(exporter));
-        try {
-            exporter.export(csvFile);
-
-            shareFile(csvFile);
-        } catch (Exporter.ExporterException e) {
-            Log.e("Error", "IO exception creating file");
-        }
-    }
-
-    private void shareFile(File file) {
-        Uri uri = FileProvider.getUriForFile(requireContext(), "com.futsch1.medtimer.fileprovider", file);
-        Intent intentShareFile = new Intent(Intent.ACTION_SEND);
-
-        intentShareFile.setDataAndType(uri, URLConnection.guessContentTypeFromName(file.getName()));
-        //Allow sharing apps to read the file Uri
-        intentShareFile.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
-        //Pass the file Uri instead of the path
-        intentShareFile.putExtra(Intent.EXTRA_STREAM,
-                uri);
-        startActivity(Intent.createChooser(intentShareFile, "Share File"));
-    }

     @Override
     public void onResume() {
@@ -235,9 +104,4 @@ public void onResume() {
         }
     }

-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        backgroundThread.quitSafely();
-    }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/futsch1/medtimer/ViewPagerAdapter.java b/app/src/main/java/com/futsch1/medtimer/ViewPagerAdapter.java
index b54d37557..d2ac6bcf4 100644
--- a/app/src/main/java/com/futsch1/medtimer/ViewPagerAdapter.java
+++ b/app/src/main/java/com/futsch1/medtimer/ViewPagerAdapter.java
@@ -22,16 +22,14 @@ public Fragment createFragment(int position) {
         Fragment fragment;
         if (position == 0)
             fragment = new OverviewFragment();
-        else if (position == 1)
-            fragment = new MedicinesFragment();
         else
-            fragment = new PreferencesFragment();
+            fragment = new MedicinesFragment();

         return fragment;
     }

     @Override
     public int getItemCount() {
-        return 3;
+        return 2;
     }
 }
diff --git a/app/src/main/java/com/futsch1/medtimer/database/JSONBackup.java b/app/src/main/java/com/futsch1/medtimer/database/JSONBackup.java
new file mode 100644
index 000000000..296a1ee50
--- /dev/null
+++ b/app/src/main/java/com/futsch1/medtimer/database/JSONBackup.java
@@ -0,0 +1,83 @@
+package com.futsch1.medtimer.database;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.annotations.Expose;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Type;
+import java.time.Instant;
+import java.util.List;
+
+public class JSONBackup {
+    public String createBackup(int databaseVersion, List<MedicineWithReminders> medicinesWithReminders) {
+        DatabaseContentWithVersion content = new DatabaseContentWithVersion(databaseVersion, medicinesWithReminders);
+        Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().setPrettyPrinting().create();
+        return gson.toJson(content);
+    }
+
+    public @Nullable List<MedicineWithReminders> parseBackup(String jsonFile) {
+        // In a first step, parse with the version set to 0
+        Gson gson = new GsonBuilder()
+                .excludeFieldsWithoutExposeAnnotation()
+                .setVersion(0.0)
+                .registerTypeAdapter(Medicine.class, new FullDeserialize<Medicine>())
+                .registerTypeAdapter(Reminder.class, new FullDeserialize<Reminder>())
+                .create();
+        try {
+            DatabaseContentWithVersion content = gson.fromJson(jsonFile, DatabaseContentWithVersion.class);
+            gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().setVersion(content.version).create();
+            return gson.fromJson(jsonFile, DatabaseContentWithVersion.class).medicinesWithReminders;
+        } catch (JsonParseException e) {
+            Log.e("JSONBackup", e.getMessage());
+            return null;
+        }
+    }
+
+    public void applyBackup(List<MedicineWithReminders> listOfMedicineWithReminders, MedicineRepository medicineRepository) {
+        medicineRepository.deleteReminders();
+        medicineRepository.deleteMedicines();
+
+        for (MedicineWithReminders medicineWithReminders : listOfMedicineWithReminders) {
+            long medicineId = medicineRepository.insertMedicine(medicineWithReminders.medicine);
+            for (Reminder reminder : medicineWithReminders.reminders) {
+                reminder.medicineRelId = (int) medicineId;
+                reminder.createdTimestamp = Instant.now().toEpochMilli() / 1000;
+                medicineRepository.insertReminder(reminder);
+            }
+        }
+    }
+
+    private record DatabaseContentWithVersion(@Expose int version,
+                                              @Expose List<MedicineWithReminders> medicinesWithReminders) {
+    }
+
+    private static class FullDeserialize<T> implements JsonDeserializer<T> {
+
+        public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException {
+            T pojo = new Gson().fromJson(je, type);
+
+            Field[] fields = pojo.getClass().getDeclaredFields();
+            for (Field f : fields) {
+                try {
+                    if (f.get(pojo) == null) {
+                        throw new JsonParseException("Missing field in JSON: " + f.getName());
+                    }
+                } catch (IllegalArgumentException | IllegalAccessException ex) {
+                    Log.e("JSONBackup", "Internal error");
+                }
+
+            }
+            return pojo;
+
+        }
+    }
+}
diff --git a/app/src/main/java/com/futsch1/medtimer/database/Medicine.java b/app/src/main/java/com/futsch1/medtimer/database/Medicine.java
index 15da8c033..104f7db3e 100644
--- a/app/src/main/java/com/futsch1/medtimer/database/Medicine.java
+++ b/app/src/main/java/com/futsch1/medtimer/database/Medicine.java
@@ -6,17 +6,22 @@
 import androidx.room.Entity;
 import androidx.room.PrimaryKey;

+import com.google.gson.annotations.Expose;
+
 @Entity
 public class Medicine {

     @ColumnInfo(name = "medicineName")
+    @Expose
     public final String name;
     @PrimaryKey(autoGenerate = true)
     public int medicineId;

     @ColumnInfo(defaultValue = "0xFFFF0000")
+    @Expose
     public int color;
     @ColumnInfo(defaultValue = "false")
+    @Expose
     public boolean useColor;

     public Medicine(String name) {
diff --git a/app/src/main/java/com/futsch1/medtimer/database/MedicineDao.java b/app/src/main/java/com/futsch1/medtimer/database/MedicineDao.java
index 97dcf7314..7e0d52292 100644
--- a/app/src/main/java/com/futsch1/medtimer/database/MedicineDao.java
+++ b/app/src/main/java/com/futsch1/medtimer/database/MedicineDao.java
@@ -67,4 +67,10 @@ public interface MedicineDao {

     @Query("DELETE FROM ReminderEvent")
     void deleteReminderEvents();
+
+    @Query("DELETE FROM Reminder")
+    void deleteReminders();
+
+    @Query("DELETE FROM Medicine")
+    void deleteMedicines();
 }
diff --git a/app/src/main/java/com/futsch1/medtimer/database/MedicineRepository.java b/app/src/main/java/com/futsch1/medtimer/database/MedicineRepository.java
index d13c28e1e..b5c3e9f6c 100644
--- a/app/src/main/java/com/futsch1/medtimer/database/MedicineRepository.java
+++ b/app/src/main/java/com/futsch1/medtimer/database/MedicineRepository.java
@@ -7,7 +7,6 @@
 import java.time.Instant;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;

 public class MedicineRepository {

@@ -19,6 +18,10 @@ public MedicineRepository(Application application) {
         medicineDao = database.medicineDao();
     }

+    public int getVersion() {
+        return database.getVersion();
+    }
+
     public LiveData<List<MedicineWithReminders>> getLiveMedicines() {
         return medicineDao.getLiveMedicines();
     }
@@ -57,9 +60,12 @@ public List<ReminderEvent> getLastDaysReminderEvents(int days) {
     }

     public long insertMedicine(Medicine medicine) {
-        final Future<Long> future = MedicineRoomDatabase.databaseWriteExecutor.submit(() -> medicineDao.insertMedicine(medicine));
+        return internalInsert(medicine, medicineDao::insertMedicine);
+    }
+
+    private <T> long internalInsert(T insertType, Insert<T> f) {
         try {
-            return future.get();
+            return MedicineRoomDatabase.databaseWriteExecutor.submit(() -> f.insert(insertType)).get();
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         } catch (ExecutionException e1) {
@@ -90,18 +96,7 @@ public void deleteReminder(Reminder reminder) {
     }

     public long insertReminderEvent(ReminderEvent reminderEvent) {
-        Future<Long> future = MedicineRoomDatabase.databaseWriteExecutor.submit(() -> medicineDao.insertReminderEvent(reminderEvent));
-        long rowId = 0;
-
-        try {
-            rowId = future.get();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-        } catch (ExecutionException e1) {
-            //noinspection CallToPrintStackTrace
-            e1.printStackTrace();
-        }
-        return rowId;
+        return internalInsert(reminderEvent, medicineDao::insertReminderEvent);
     }

     public ReminderEvent getReminderEvent(int reminderEventId) {
@@ -109,22 +104,26 @@ public ReminderEvent getReminderEvent(int reminderEventId) {
     }

     public void updateReminderEvent(ReminderEvent reminderEvent) {
-        @SuppressWarnings("unchecked") Future<Void> future = (Future<Void>) MedicineRoomDatabase.databaseWriteExecutor.submit(() -> medicineDao.updateReminderEvent(reminderEvent));
-        try {
-            future.get();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-        } catch (ExecutionException e1) {
-            //noinspection CallToPrintStackTrace
-            e1.printStackTrace();
-        }
+        MedicineRoomDatabase.databaseWriteExecutor.execute(() -> medicineDao.updateReminderEvent(reminderEvent));
     }

     public void deleteReminderEvents() {
         MedicineRoomDatabase.databaseWriteExecutor.execute(medicineDao::deleteReminderEvents);
     }

+    public void deleteReminders() {
+        MedicineRoomDatabase.databaseWriteExecutor.execute(medicineDao::deleteReminders);
+    }
+
+    public void deleteMedicines() {
+        MedicineRoomDatabase.databaseWriteExecutor.execute(medicineDao::deleteMedicines);
+    }
+
     public void deleteAll() {
         MedicineRoomDatabase.databaseWriteExecutor.execute(database::clearAllTables);
     }
+
+    interface Insert<T> {
+        long insert(T item);
+    }
 }
diff --git a/app/src/main/java/com/futsch1/medtimer/database/MedicineRoomDatabase.java b/app/src/main/java/com/futsch1/medtimer/database/MedicineRoomDatabase.java
index 69d0ac884..1aea62af6 100644
--- a/app/src/main/java/com/futsch1/medtimer/database/MedicineRoomDatabase.java
+++ b/app/src/main/java/com/futsch1/medtimer/database/MedicineRoomDatabase.java
@@ -24,7 +24,7 @@
 )
 @SuppressWarnings("java:S6548")
 public abstract class MedicineRoomDatabase extends RoomDatabase {
-    private static final int NUMBER_OF_THREADS = 4;
+    private static final int NUMBER_OF_THREADS = 1;
     static final ExecutorService databaseWriteExecutor =
             Executors.newFixedThreadPool(NUMBER_OF_THREADS);
     // marking the instance as volatile to ensure atomic access to the variable
@@ -44,6 +44,10 @@ static MedicineRoomDatabase getDatabase(final Context context) {
         return instance;
     }

+    public int getVersion() {
+        return getOpenHelper().getReadableDatabase().getVersion();
+    }
+
     public abstract MedicineDao medicineDao();

     @RenameColumn(fromColumnName = "raisedTimestamp", toColumnName = "remindedTimestamp", tableName = "ReminderEvent")
diff --git a/app/src/main/java/com/futsch1/medtimer/database/MedicineWithReminders.java b/app/src/main/java/com/futsch1/medtimer/database/MedicineWithReminders.java
index 321283663..619163873 100644
--- a/app/src/main/java/com/futsch1/medtimer/database/MedicineWithReminders.java
+++ b/app/src/main/java/com/futsch1/medtimer/database/MedicineWithReminders.java
@@ -3,6 +3,8 @@
 import androidx.room.Embedded;
 import androidx.room.Relation;

+import com.google.gson.annotations.Expose;
+
 import java.util.List;

 public class MedicineWithReminders {
@@ -10,7 +12,9 @@ public class MedicineWithReminders {
             parentColumn = "medicineId",
             entityColumn = "medicineRelId"
     )
+    @Expose
     public List<Reminder> reminders;
     @Embedded
+    @Expose
     public Medicine medicine;
 }
diff --git a/app/src/main/java/com/futsch1/medtimer/database/Reminder.java b/app/src/main/java/com/futsch1/medtimer/database/Reminder.java
index c7d2e4a28..2218a0253 100644
--- a/app/src/main/java/com/futsch1/medtimer/database/Reminder.java
+++ b/app/src/main/java/com/futsch1/medtimer/database/Reminder.java
@@ -4,20 +4,26 @@
 import androidx.room.Entity;
 import androidx.room.PrimaryKey;

+import com.google.gson.annotations.Expose;
+
 @Entity
 @SuppressWarnings("java:S1104")
 public class Reminder {
-    public final int medicineRelId;
+    public int medicineRelId;
     @PrimaryKey(autoGenerate = true)
     public int reminderId;
+    @Expose
     public int timeInMinutes;
     @ColumnInfo(defaultValue = "0")
     public long createdTimestamp;
     @ColumnInfo(defaultValue = "1")
+    @Expose
     public int daysBetweenReminders;
     @ColumnInfo(defaultValue = "")
+    @Expose
     public String instructions;

+    @Expose
     public String amount;

     public Reminder(int medicineRelId) {
diff --git a/app/src/main/java/com/futsch1/medtimer/helpers/FileHelper.java b/app/src/main/java/com/futsch1/medtimer/helpers/FileHelper.java
new file mode 100644
index 000000000..f65c497d2
--- /dev/null
+++ b/app/src/main/java/com/futsch1/medtimer/helpers/FileHelper.java
@@ -0,0 +1,62 @@
+package com.futsch1.medtimer.helpers;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+public class FileHelper {
+    private FileHelper() {
+        // Intentionally empty
+    }
+
+    public static boolean saveToFile(File file, String content) {
+        try {
+            try (FileWriter writer = new FileWriter(file)) {
+                writer.write(content);
+            }
+        } catch (IOException e) {
+            Log.e("FileHelper", e.toString());
+            return false;
+        }
+        return true;
+    }
+
+    public static @Nullable String readFromUri(@Nullable Uri uri, ContentResolver resolver) {
+        if (uri != null && uri.getPath() != null) {
+            try (InputStream inputStream = resolver.openInputStream(uri)) {
+                try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
+                    StringBuilder stringBuilder = getStringBuilder(inputStreamReader);
+                    return stringBuilder.toString();
+                }
+            } catch (IOException e) {
+                Log.e("FileHelper", e.toString());
+            }
+        }
+        return null;
+    }
+
+    @NonNull
+    private static StringBuilder getStringBuilder(InputStreamReader inputStreamReader) throws IOException {
+        StringBuilder stringBuilder = new StringBuilder();
+        try (BufferedReader reader = new BufferedReader(inputStreamReader)) {
+            String line = reader.readLine();
+            while (line != null) {
+                stringBuilder.append(line);
+                stringBuilder.append("\n");
+                line = reader.readLine();
+            }
+        }
+        return stringBuilder;
+    }
+}
diff --git a/app/src/main/java/com/futsch1/medtimer/helpers/PathHelper.java b/app/src/main/java/com/futsch1/medtimer/helpers/PathHelper.java
index 5d28756f5..caca377c5 100644
--- a/app/src/main/java/com/futsch1/medtimer/helpers/PathHelper.java
+++ b/app/src/main/java/com/futsch1/medtimer/helpers/PathHelper.java
@@ -17,4 +17,9 @@ public static String getExportFilename(Exporter exporter) {
         String fileName = String.format("MedTimer Export %s.%s", ZonedDateTime.now().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)), exporter.getExtension());
         return fileName.replaceAll(RESERVED_CHARS, "_");
     }
+
+    public static String getBackupFilename() {
+        String fileName = String.format("MedTimer Backup %s.json", ZonedDateTime.now().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)));
+        return fileName.replaceAll(RESERVED_CHARS, "_");
+    }
 }
diff --git a/app/src/main/play/listings/de-DE/full-description.txt b/app/src/main/play/listings/de-DE/full-description.txt
index b3caf6bfb..9c447fa8d 100644
--- a/app/src/main/play/listings/de-DE/full-description.txt
+++ b/app/src/main/play/listings/de-DE/full-description.txt
@@ -19,4 +19,4 @@ Datenschutz und Offline-Zugriff:

 MedTimer ist eine kostenlose, werbefreie App.

-Weitere Informationen auf GitHub: https://www.google.com/url?q=https://github.com/Futsch1/medTimer
+Weitere Informationen auf GitHub: https://github.com/Futsch1/medTimer
diff --git a/app/src/main/play/listings/en-US/full-description.txt b/app/src/main/play/listings/en-US/full-description.txt
index 7ff153f1a..c45c308cc 100644
--- a/app/src/main/play/listings/en-US/full-description.txt
+++ b/app/src/main/play/listings/en-US/full-description.txt
@@ -22,4 +22,4 @@ Privacy & Offline Accessibility:

 MedTimer is a free app without adds.

-See the project page on GitHub: https://www.google.com/url?q=https://github.com/Futsch1/medTimer
+See the project page on GitHub: https://github.com/Futsch1/medTimer
diff --git a/app/src/main/play/listings/es-ES/full-description.txt b/app/src/main/play/listings/es-ES/full-description.txt
index c6f2f7eea..d515bd6df 100644
--- a/app/src/main/play/listings/es-ES/full-description.txt
+++ b/app/src/main/play/listings/es-ES/full-description.txt
@@ -2,24 +2,23 @@ Historial y recordatorios de medicación con total privacidad offline

 Controle su medicación con MedTimer

-MedTimer es una aplicación de recordatorio de medicación de código abierto diseñada para ayudarle a gestionar sus medicamentos de forma eficaz y con un control total de sus datos confidenciales.
+MedTimer es una aplicación de código abierto que permite asignar recordatorios de medicación, diseñada para ayudarle a gestionar sus medicamentos de forma eficaz mientras mantiene el control total de sus datos confidenciales.

 Recordatorios flexibles y personalizados:

-- Añada un número ilimitado de medicaciones con múltiples recordatorios diarios por medicación.
+- Añada un número ilimitado de medicamentos con múltiples recordatorios diarios por medicamento.
 - Personaliza los recordatorios para adaptarlos a tu horario de dosificación.

 Gestión de datos sencilla y segura:

-- Confirme o descarte los recordatorios para registrar con precisión el cumplimiento de la medicación.
+- Confirme o descarte los recordatorios para registrar con precisión el seguimiento de la medicación.
 - Exportar su historial de medicación como un archivo CSV para facilitar el mantenimiento de registros o compartir con profesionales de la salud.
-  profesionales de la salud.

 Privacidad y accesibilidad sin conexión:

 - Todos los datos se almacenan de forma segura en su dispositivo, lo que garantiza una total privacidad y accesibilidad sin conexión.
 - No requiere conexión a Internet - sus recordatorios de medicación están siempre disponibles.

-MedTimer es una aplicación gratuita sin añadidos.
+MedTimer es una aplicación gratuita sin seguimiento.

-Consulte la página del proyecto en GitHub: https://www.google.com/url?q=https://github.com/Futsch1/medTimer
+Consulte la página del proyecto en GitHub: https://github.com/Futsch1/medTimer
diff --git a/app/src/main/res/layout/preferences_activity.xml b/app/src/main/res/layout/preferences_activity.xml
new file mode 100644
index 000000000..de6591a20
--- /dev/null
+++ b/app/src/main/res/layout/preferences_activity.xml
@@ -0,0 +1,9 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <FrameLayout
+        android:id="@+id/settings"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml
new file mode 100644
index 000000000..520716a01
--- /dev/null
+++ b/app/src/main/res/menu/main.xml
@@ -0,0 +1,52 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <group android:id="@+id/settings_group">
+        <item
+            android:id="@+id/settings"
+            android:icon="@android:drawable/ic_menu_preferences"
+            android:title="@string/tab_settings" />
+    </group>
+    <item
+        android:id="@+id/export"
+        android:icon="@drawable/floppy2"
+        android:title="@string/export">
+        <menu>
+            <item
+                android:id="@+id/export_csv"
+                android:title="@string/csv" />
+            <item
+                android:id="@+id/export_pdf"
+                android:title="@string/pdf" />
+        </menu>
+    </item>
+    <item
+        android:id="@+id/clear_events"
+        android:icon="@drawable/x_circle"
+        android:title="@string/clear_events" />
+    <item
+        android:id="@+id/backup"
+        android:title="@string/backup_medicine">
+        <menu>
+            <item
+                android:id="@+id/perform_backup"
+                android:title="@string/create" />
+            <item
+                android:id="@+id/restore_backup"
+                android:title="@string/restore" />
+        </menu>
+    </item>
+    <item
+        android:id="@+id/generate_test_data"
+        android:icon="@drawable/x_circle"
+        android:title="@string/generate_test_data" />
+    <group android:id="@+id/information">
+        <item
+            android:id="@+id/version"
+            android:icon="@drawable/info_circle"
+            android:title="@string/app_version" />
+        <item
+            android:id="@+id/app_url"
+            android:icon="@drawable/link"
+            android:title="@string/app_url" />
+    </group>
+</menu>
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index de9dfaee6..b4900a997 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -72,4 +72,12 @@
     <string name="time_after">x Stunden nach Medikament y</string>
     <string name="instruction_templates">Hinweisvorlagen</string>
     <string name="instructions_label">Zusätzliche Hinweise (z.B. vor einer Mahlzeit)</string>
+    <string name="backup_medicine">Backup der Medikamente</string>
+    <string name="create">Erstellen</string>
+    <string name="restore">Wiederherstellen</string>
+    <string name="restore_failed">Wiederherstellen fehlgeschlagen</string>
+    <string name="backup_failed">Erstellen fehlgeschlagen</string>
+    <string name="export_failed">Exportieren fehlgeschlagen</string>
+    <string name="restore_successful">Wiederherstellen erfolgreich</string>
+    <string name="restore_start">Alle Medikamenteneinstellungen werden überschrieben.\n\nFortfahren?</string>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 9a9164726..10de8b3d5 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -71,4 +71,12 @@
     <string name="time_after">x horas después del medicamento y</string>
     <string name="instruction_templates">Plantillas de instrucciones</string>
     <string name="instructions_label">Instrucciones adicionales (por ejemplo, antes de las comidas)</string>
+    <string name="backup_medicine">Backup de los medicamentos</string>
+    <string name="create">Crear</string>
+    <string name="restore">Restaurar</string>
+    <string name="restore_failed">Error de restauración</string>
+    <string name="backup_failed">Error de creación</string>
+    <string name="export_failed">Error de exportación</string>
+    <string name="restore_successful">Restaurar con éxito</string>
+    <string name="restore_start">Esto reemplazará todos los ajustes de medicina.\n\n¿Continuar?</string>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 242786b08..f217a3c25 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -29,10 +29,6 @@
         <item>48</item>
         <item>72</item>
     </string-array>
-    <string-array name="export_formats">
-        <item>CSV</item>
-        <item>PDF</item>
-    </string-array>
     <string-array name="instructions_suggestions">
         <item>@string/no_instructions</item>
         <item>@string/before_meal</item>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ab450d8dc..dbadaa5cd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -73,4 +73,14 @@
     <string name="time_after">x hours after medicine y</string>
     <string name="instruction_templates">Instruction templates</string>
     <string name="instructions_label">Additional instructions (e.g. before meals)</string>
+    <string name="backup_medicine">Backup medicine settings</string>
+    <string name="csv" translatable="false">CSV</string>
+    <string name="pdf" translatable="false">PDF</string>
+    <string name="create">Create</string>
+    <string name="restore">Restore</string>
+    <string name="restore_failed">Restore failed</string>
+    <string name="backup_failed">Backup failed</string>
+    <string name="export_failed">Export failed</string>
+    <string name="restore_successful">Restore successful</string>
+    <string name="restore_start">This will replace all medicine settings.\n\nContinue?</string>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 2ae542517..24f32a525 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -47,37 +47,4 @@
         android:title="@string/theme"
         app:key="theme"
         app:useSimpleSummaryProvider="true" />
-    <PreferenceCategory
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:title="@string/actions" />
-    <Preference
-        app:icon="@drawable/floppy2"
-        app:key="export"
-        app:summary=""
-        app:title="@string/export" />
-    <Preference
-        app:icon="@drawable/x_circle"
-        app:key="clear_events"
-        app:summary=""
-        app:title="@string/clear_events" />
-    <Preference
-        app:icon="@drawable/x_circle"
-        app:key="generate_test_data"
-        app:summary=""
-        app:title="@string/generate_test_data" />
-    <PreferenceCategory
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:title="@string/information" />
-    <Preference
-        app:icon="@drawable/info_circle"
-        app:key="version"
-        app:summary=""
-        app:title="@string/app_version" />
-    <Preference
-        app:icon="@drawable/link"
-        app:key="app_url"
-        app:summary=""
-        app:title="@string/app_url" />
 </PreferenceScreen>
\ No newline at end of file

diff --git a/app/src/test/java/com/futsch1/medtimer/JSONBackupUnitTest.java b/app/src/test/java/com/futsch1/medtimer/JSONBackupUnitTest.java
new file mode 100644
index 000000000..d32b450d5
--- /dev/null
+++ b/app/src/test/java/com/futsch1/medtimer/JSONBackupUnitTest.java
@@ -0,0 +1,227 @@
+package com.futsch1.medtimer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.graphics.Color;
+
+import com.futsch1.medtimer.database.JSONBackup;
+import com.futsch1.medtimer.database.Medicine;
+import com.futsch1.medtimer.database.MedicineWithReminders;
+import com.futsch1.medtimer.database.Reminder;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public class JSONBackupUnitTest {
+
+    // Generated by CodiumAI
+
+    // creates a backup object with a version number and a medicines array
+    @Test
+    public void test_creates_backup_with_version_and_medicines_array() {
+        JSONBackup jsonBackup = new JSONBackup();
+        List<MedicineWithReminders> medicinesWithReminders = new ArrayList<>();
+        MedicineWithReminders medicineWithReminders = new MedicineWithReminders();
+        Reminder reminder = new Reminder(0);
+        reminder.timeInMinutes = 60;
+        reminder.amount = "1";
+        reminder.daysBetweenReminders = 1;
+        reminder.instructions = "Take with water";
+        medicineWithReminders.reminders = Collections.singletonList(reminder);
+        medicineWithReminders.medicine = new Medicine("Medicine A");
+        medicineWithReminders.medicine.useColor = true;
+        medicineWithReminders.medicine.color = Color.RED;
+        medicinesWithReminders.add(medicineWithReminders);
+
+        String result = jsonBackup.createBackup(5, medicinesWithReminders);
+
+        assertNotNull(result);
+
+        // @formatter:off
+        assertEquals("""
+{
+  "version": 5,
+  "medicinesWithReminders": [
+    {
+      "reminders": [
+        {
+          "timeInMinutes": 60,
+          "daysBetweenReminders": 1,
+          "instructions": "Take with water",
+          "amount": "1"
+        }
+      ],
+      "medicine": {
+        "name": "Medicine A",
+        "color": -65536,
+        "useColor": true
+      }
+    }
+  ]
+}""", result);
+        // @formatter:on
+
+        List<MedicineWithReminders> parsedReminders = jsonBackup.parseBackup(result);
+        assertNotNull(parsedReminders);
+        compareListMedicineWithReminders(parsedReminders, medicinesWithReminders);
+    }
+
+    private void compareListMedicineWithReminders(List<MedicineWithReminders> actual, List<MedicineWithReminders> expected) {
+        assertEquals(actual.size(), expected.size());
+        for (int i = 0; i < actual.size(); i++) {
+            compareMedicineWithReminders(actual.get(i), expected.get(i));
+        }
+    }
+
+    private void compareMedicineWithReminders(MedicineWithReminders medicineWithReminders, MedicineWithReminders medicineWithReminders1) {
+        assertEquals(medicineWithReminders.reminders.size(), medicineWithReminders1.reminders.size());
+        for (int i = 0; i < medicineWithReminders.reminders.size(); i++) {
+            compareReminder(medicineWithReminders.reminders.get(i), medicineWithReminders1.reminders.get(i));
+        }
+        assertEquals(medicineWithReminders.medicine.name, medicineWithReminders1.medicine.name);
+        assertEquals(medicineWithReminders.medicine.useColor, medicineWithReminders1.medicine.useColor);
+        assertEquals(medicineWithReminders.medicine.color, medicineWithReminders1.medicine.color);
+    }
+
+    private void compareReminder(Reminder reminder, Reminder reminder1) {
+        assertEquals(reminder.timeInMinutes, reminder1.timeInMinutes);
+        assertEquals(reminder.daysBetweenReminders, reminder1.daysBetweenReminders);
+        assertEquals(reminder.instructions, reminder1.instructions);
+        assertEquals(reminder.amount, reminder1.amount);
+    }
+
+    // iterates over the list of MedicineWithReminders and adds each one to the medicines array as a JSONObject
+    @Test
+    public void test_iterates_over_medicinesWithReminders_and_adds_to_medicines_array() {
+        JSONBackup jsonBackup = new JSONBackup();
+        List<MedicineWithReminders> medicinesWithReminders = new ArrayList<>();
+        MedicineWithReminders medicineWithReminders1 = new MedicineWithReminders();
+        Reminder reminder1 = new Reminder(0);
+        reminder1.timeInMinutes = 60;
+        reminder1.amount = "1";
+        reminder1.daysBetweenReminders = 1;
+        reminder1.instructions = "Take with water";
+        medicineWithReminders1.reminders = Collections.singletonList(reminder1);
+        medicineWithReminders1.medicine = new Medicine("Medicine A");
+        medicineWithReminders1.medicine.useColor = true;
+        medicineWithReminders1.medicine.color = Color.RED;
+        medicinesWithReminders.add(medicineWithReminders1);
+
+        MedicineWithReminders medicineWithReminders2 = new MedicineWithReminders();
+        Reminder reminder2 = new Reminder(0);
+        reminder2.timeInMinutes = 120;
+        reminder2.amount = "2";
+        reminder2.daysBetweenReminders = 2;
+        reminder2.instructions = "Take after meal";
+        medicineWithReminders2.reminders = new ArrayList<>() {{
+            add(reminder1);
+            add(reminder2);
+        }};
+        medicineWithReminders2.medicine = new Medicine("Medicine B");
+        medicineWithReminders2.medicine.useColor = false;
+        medicineWithReminders2.medicine.color = Color.BLUE;
+        medicinesWithReminders.add(medicineWithReminders2);
+
+        String result = jsonBackup.createBackup(4, medicinesWithReminders);
+
+        assertNotNull(result);
+
+        // @formatter:off
+        assertEquals("""
+{
+  "version": 4,
+  "medicinesWithReminders": [
+    {
+      "reminders": [
+        {
+          "timeInMinutes": 60,
+          "daysBetweenReminders": 1,
+          "instructions": "Take with water",
+          "amount": "1"
+        }
+      ],
+      "medicine": {
+        "name": "Medicine A",
+        "color": -65536,
+        "useColor": true
+      }
+    },
+    {
+      "reminders": [
+        {
+          "timeInMinutes": 60,
+          "daysBetweenReminders": 1,
+          "instructions": "Take with water",
+          "amount": "1"
+        },
+        {
+          "timeInMinutes": 120,
+          "daysBetweenReminders": 2,
+          "instructions": "Take after meal",
+          "amount": "2"
+        }
+      ],
+      "medicine": {
+        "name": "Medicine B",
+        "color": -16776961,
+        "useColor": false
+      }
+    }
+  ]
+}""", result);
+        // @formatter:on
+
+        List<MedicineWithReminders> parsedReminders = jsonBackup.parseBackup(result);
+        assertNotNull(parsedReminders);
+        compareListMedicineWithReminders(parsedReminders, medicinesWithReminders);
+    }
+
+    @Test
+    public void test_parse_json_backup_missing_fields() {
+        // Arrange
+        JSONBackup jsonBackup = new JSONBackup();
+        String validJsonBackup = "{\"version\": 1, \"medicinesWithReminders\": [{\"reminders\": [], \"medicine\": {}}]}";
+
+        // Act
+        List<MedicineWithReminders> result = jsonBackup.parseBackup(validJsonBackup);
+
+        // Assert
+        assertNull(result);
+    }
+
+    @Test
+    public void test_parse_invalid_json_backup() {
+        // Arrange
+        JSONBackup jsonBackup = new JSONBackup();
+        String invalidJsonBackup = "invalid_json";
+
+        // Act
+        List<MedicineWithReminders> result = jsonBackup.parseBackup(invalidJsonBackup);
+
+        // Assert
+        assertNull(result);
+    }
+
+    @Test
+    public void test_parse_json_backup_with_missing_fields() {
+        // Arrange
+        JSONBackup jsonBackup = new JSONBackup();
+        String jsonBackupWithMissingFields = "{\"version\": 1}";
+
+        // Act
+        List<MedicineWithReminders> result = jsonBackup.parseBackup(jsonBackupWithMissingFields);
+
+        // Assert
+        assertNull(result);
+    }
+
+}
+