Repository: Futsch1/medTimer (language: kotlin)

Issue:
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 


Discussion hints:
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! :)

Pull request: Json backup
fixes #49 

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

Fix patch:
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


Test patch:
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);
+    }
+
+}
+