Merge branch 'lineage-21.0' of https://github.com/LineageOS/android_packages_apps_Etar into leaf-3.2

Change-Id: Ie40c0620892967cf2fc01ed6f94ea627855a533c
diff --git a/app/Android.bp b/app/Android.bp
index 57de317..920996d 100644
--- a/app/Android.bp
+++ b/app/Android.bp
@@ -27,6 +27,7 @@
         "kotlinx-coroutines-core",
         "kotlinx-coroutines-android",
         "Etar-Calendar_org.dmfs_lib-recur",
+        "androidx.lifecycle_lifecycle-livedata-ktx",
     ] + [
         "android-common",
         "android-opt-timezonepicker",
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 897ad0d..c171189 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,4 +1,7 @@
 import com.android.build.gradle.internal.tasks.factory.dependsOn
+import org.lineageos.generatebp.GenerateBpPlugin
+import org.lineageos.generatebp.GenerateBpPluginExtension
+import org.lineageos.generatebp.models.Module
 
 plugins {
 	id("com.android.application")
@@ -6,6 +9,20 @@
 	id("org.ec4j.editorconfig")
 }
 
+apply {
+	plugin<GenerateBpPlugin>()
+}
+
+buildscript {
+	repositories {
+		maven("https://raw.githubusercontent.com/lineage-next/gradle-generatebp/v1.9/.m2")
+	}
+
+	dependencies {
+		classpath("org.lineageos:gradle-generatebp:+")
+	}
+}
+
 editorconfig {
 	excludes = listOf("external/**", "metadata/**", "**/*.webp")
 }
@@ -18,8 +35,8 @@
 	defaultConfig {
 		minSdk = 26
 		targetSdk = 34
-		versionCode = 45
-		versionName = "1.0.45"
+		versionCode = 46
+		versionName = "1.0.46"
 		applicationId = "ws.xsoh.etar"
 		testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
 	}
@@ -51,10 +68,6 @@
 		}
 	}
 
-	buildFeatures {
-		viewBinding = true
-	}
-
 	/*
 	 * To sign release build, create file gradle.properties in ~/.gradle/ with this content:
 	 *
@@ -137,6 +150,21 @@
 	// https://mvnrepository.com/artifact/org.dmfs/lib-recur
 	implementation("org.dmfs:lib-recur:0.16.0")
 
+	// lifecycle
+	val lifecycle_version = "2.7.0"
+	implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
+}
+
+configure<GenerateBpPluginExtension> {
+	targetSdk.set(android.defaultConfig.targetSdk!!)
+	availableInAOSP.set { module: Module ->
+		when {
+			module.group.startsWith("androidx") -> true
+			module.group.startsWith("com.google") -> true
+			module.group.startsWith("org.jetbrains") -> true
+			else -> false
+		}
+	}
 }
 
 tasks.preBuild.dependsOn(":aarGen")
diff --git a/app/generatebp.gradle.kts b/app/generatebp.gradle.kts
deleted file mode 100644
index f6145f5..0000000
--- a/app/generatebp.gradle.kts
+++ /dev/null
@@ -1,29 +0,0 @@
-import org.lineageos.generatebp.GenerateBpPlugin
-import org.lineageos.generatebp.GenerateBpPluginExtension
-import org.lineageos.generatebp.models.Module
-
-apply {
-    plugin<GenerateBpPlugin>()
-}
-
-buildscript {
-    repositories {
-        maven("https://raw.githubusercontent.com/lineage-next/gradle-generatebp/v1.3/.m2")
-    }
-
-    dependencies {
-        classpath("org.lineageos:gradle-generatebp:+")
-    }
-}
-
-configure<GenerateBpPluginExtension> {
-    targetSdk.set(extra.get("targetSdk") as Int)
-    availableInAOSP.set { module: Module ->
-        when {
-            module.group.startsWith("androidx") -> true
-            module.group.startsWith("com.google") -> true
-            module.group.startsWith("org.jetbrains") -> true
-            else -> false
-        }
-    }
-}
diff --git a/app/libs/Android.bp b/app/libs/Android.bp
index 82e3e8c..7db7f64 100644
--- a/app/libs/Android.bp
+++ b/app/libs/Android.bp
@@ -1,5 +1,5 @@
 //
-// SPDX-FileCopyrightText: 2023 The LineageOS Project
+// SPDX-FileCopyrightText: 2023-2024 The LineageOS Project
 // SPDX-License-Identifier: Apache-2.0
 //
 
@@ -7,7 +7,7 @@
 
 java_import {
     name: "Etar-Calendar_org.dmfs_jems2-nodeps",
-    jars: ["org/dmfs/jems2/2.11.1/jems2-2.11.1.jar"],
+    jars: ["org/dmfs/jems2/2.22.0/jems2-2.22.0.jar"],
     sdk_version: "34",
     min_sdk_version: "14",
     apex_available: [
@@ -32,7 +32,7 @@
 
 java_import {
     name: "Etar-Calendar_org.dmfs_lib-recur-nodeps",
-    jars: ["org/dmfs/lib-recur/0.15.0/lib-recur-0.15.0.jar"],
+    jars: ["org/dmfs/lib-recur/0.16.0/lib-recur-0.16.0.jar"],
     sdk_version: "34",
     min_sdk_version: "14",
     apex_available: [
@@ -51,8 +51,8 @@
     ],
     static_libs: [
         "Etar-Calendar_org.dmfs_lib-recur-nodeps",
-        "Etar-Calendar_org.dmfs_rfc5545-datetime",
         "Etar-Calendar_org.dmfs_jems2",
+        "Etar-Calendar_org.dmfs_rfc5545-datetime",
     ],
     java_version: "1.7",
 }
diff --git a/app/libs/org/dmfs/jems2/2.11.1/jems2-2.11.1.jar.license b/app/libs/org/dmfs/jems2/2.11.1/jems2-2.11.1.jar.license
deleted file mode 100644
index a6d741a..0000000
--- a/app/libs/org/dmfs/jems2/2.11.1/jems2-2.11.1.jar.license
+++ /dev/null
@@ -1,2 +0,0 @@
-SPDX-FileCopyrightText: 2023 Marten Gajda
-
diff --git a/app/libs/org/dmfs/jems2/2.11.1/jems2-2.11.1.jar b/app/libs/org/dmfs/jems2/2.22.0/jems2-2.22.0.jar
similarity index 79%
rename from app/libs/org/dmfs/jems2/2.11.1/jems2-2.11.1.jar
rename to app/libs/org/dmfs/jems2/2.22.0/jems2-2.22.0.jar
index 31f6066..d0584a1 100644
--- a/app/libs/org/dmfs/jems2/2.11.1/jems2-2.11.1.jar
+++ b/app/libs/org/dmfs/jems2/2.22.0/jems2-2.22.0.jar
Binary files differ
diff --git a/app/libs/org/dmfs/jems2/2.22.0/jems2-2.22.0.jar.license b/app/libs/org/dmfs/jems2/2.22.0/jems2-2.22.0.jar.license
new file mode 100644
index 0000000..a685174
--- /dev/null
+++ b/app/libs/org/dmfs/jems2/2.22.0/jems2-2.22.0.jar.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2024 Marten Gajda
+
diff --git a/app/libs/org/dmfs/lib-recur/0.15.0/lib-recur-0.15.0.jar b/app/libs/org/dmfs/lib-recur/0.15.0/lib-recur-0.15.0.jar
deleted file mode 100644
index a931c3e..0000000
--- a/app/libs/org/dmfs/lib-recur/0.15.0/lib-recur-0.15.0.jar
+++ /dev/null
Binary files differ
diff --git a/app/libs/org/dmfs/lib-recur/0.15.0/lib-recur-0.15.0.jar.license b/app/libs/org/dmfs/lib-recur/0.15.0/lib-recur-0.15.0.jar.license
deleted file mode 100644
index a6d741a..0000000
--- a/app/libs/org/dmfs/lib-recur/0.15.0/lib-recur-0.15.0.jar.license
+++ /dev/null
@@ -1,2 +0,0 @@
-SPDX-FileCopyrightText: 2023 Marten Gajda
-
diff --git a/app/libs/org/dmfs/lib-recur/0.16.0/lib-recur-0.16.0.jar b/app/libs/org/dmfs/lib-recur/0.16.0/lib-recur-0.16.0.jar
new file mode 100644
index 0000000..0cb5acb
--- /dev/null
+++ b/app/libs/org/dmfs/lib-recur/0.16.0/lib-recur-0.16.0.jar
Binary files differ
diff --git a/app/libs/org/dmfs/lib-recur/0.16.0/lib-recur-0.16.0.jar.license b/app/libs/org/dmfs/lib-recur/0.16.0/lib-recur-0.16.0.jar.license
new file mode 100644
index 0000000..a685174
--- /dev/null
+++ b/app/libs/org/dmfs/lib-recur/0.16.0/lib-recur-0.16.0.jar.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2024 Marten Gajda
+
diff --git a/app/libs/org/dmfs/rfc5545-datetime/0.3/rfc5545-datetime-0.3.jar.license b/app/libs/org/dmfs/rfc5545-datetime/0.3/rfc5545-datetime-0.3.jar.license
index a6d741a..a685174 100644
--- a/app/libs/org/dmfs/rfc5545-datetime/0.3/rfc5545-datetime-0.3.jar.license
+++ b/app/libs/org/dmfs/rfc5545-datetime/0.3/rfc5545-datetime-0.3.jar.license
@@ -1,2 +1,2 @@
-SPDX-FileCopyrightText: 2023 Marten Gajda
+SPDX-FileCopyrightText: 2024 Marten Gajda
 
diff --git a/app/src/main/java/com/android/calendar/AllInOneActivity.java b/app/src/main/java/com/android/calendar/AllInOneActivity.java
index 888b6c3..fe76fa3 100644
--- a/app/src/main/java/com/android/calendar/AllInOneActivity.java
+++ b/app/src/main/java/com/android/calendar/AllInOneActivity.java
@@ -942,15 +942,6 @@
                     selectedTime.setMonth(monthOfYear);
                     selectedTime.setDay(dayOfMonth);
 
-                    Calendar c = Calendar.getInstance();
-                    c.set(year, monthOfYear, dayOfMonth);
-                    int weekday = c.get(Calendar.DAY_OF_WEEK);
-                    if (weekday == 1) {
-                        selectedTime.setWeekDay(7);
-                    } else {
-                        selectedTime.setWeekDay(weekday - 1);
-                    }
-
                     long extras = CalendarController.EXTRA_GOTO_TIME | CalendarController.EXTRA_GOTO_DATE;
                     mController.sendEvent(this, EventType.GO_TO, selectedTime, null, selectedTime, -1, ViewType.CURRENT, extras, null, null);
                 }
diff --git a/app/src/main/java/com/android/calendar/CalendarEventModel.java b/app/src/main/java/com/android/calendar/CalendarEventModel.java
index 62884d1..466536a 100644
--- a/app/src/main/java/com/android/calendar/CalendarEventModel.java
+++ b/app/src/main/java/com/android/calendar/CalendarEventModel.java
@@ -74,6 +74,7 @@
     public String mDescription = null;
     public String mUrl = null;
     public String mRrule = null;
+    public String mExDate = null;
     public String mOrganizer = null;
     public String mOrganizerDisplayName = null;
     /**
@@ -111,9 +112,6 @@
     public boolean mOrganizerCanRespond = false;
     public int mCalendarAccessLevel = Calendars.CAL_ACCESS_CONTRIBUTOR;
     public int mEventStatus = Events.STATUS_CONFIRMED;
-    // The model can't be updated with a calendar cursor until it has been
-    // updated with an event cursor.
-    public boolean mModelUpdatedWithEventCursor;
     public int mAccessLevel = 0;
     public ArrayList<ReminderEntry> mReminders;
     public ArrayList<ReminderEntry> mDefaultReminders;
@@ -291,7 +289,6 @@
         mEventStatus = Events.STATUS_CONFIRMED;
         mOrganizerCanRespond = false;
         mCalendarAccessLevel = Calendars.CAL_ACCESS_CONTRIBUTOR;
-        mModelUpdatedWithEventCursor = false;
         mCalendarAllowedReminders = null;
         mCalendarAllowedAttendeeTypes = null;
         mCalendarAllowedAvailability = null;
@@ -350,7 +347,6 @@
         result = prime * result + (mGuestsCanModify ? 1231 : 1237);
         result = prime * result + (mGuestsCanSeeGuests ? 1231 : 1237);
         result = prime * result + (mOrganizerCanRespond ? 1231 : 1237);
-        result = prime * result + (mModelUpdatedWithEventCursor ? 1231 : 1237);
         result = prime * result + mCalendarAccessLevel;
         result = prime * result + (mHasAlarm ? 1231 : 1237);
         result = prime * result + (mHasAttendeeData ? 1231 : 1237);
@@ -615,9 +611,6 @@
         if (mCalendarAccessLevel != originalModel.mCalendarAccessLevel) {
             return false;
         }
-        if (mModelUpdatedWithEventCursor != originalModel.mModelUpdatedWithEventCursor) {
-            return false;
-        }
         if (mHasAlarm != originalModel.mHasAlarm) {
             return false;
         }
diff --git a/app/src/main/java/com/android/calendar/datasource/AccountDataSource.kt b/app/src/main/java/com/android/calendar/datasource/AccountDataSource.kt
new file mode 100644
index 0000000..37d39d0
--- /dev/null
+++ b/app/src/main/java/com/android/calendar/datasource/AccountDataSource.kt
@@ -0,0 +1,64 @@
+/*
+ *  Copyright (c) 2024 The Etar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.android.calendar.datasource
+
+import android.accounts.Account
+import android.app.Application
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.provider.CalendarContract
+
+/**
+ * Datasource of Account entities
+ */
+class AccountDataSource(
+    private val application: Application
+) {
+    /**
+     * Convenience to get the content resolver
+     */
+    private val contentResolver: ContentResolver
+        get() = application.contentResolver
+
+    /**
+     * TODO Document
+     */
+    fun queryAccount(calendarId: Long): Account? {
+        val calendarUri =
+            ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarId)
+        contentResolver.query(calendarUri, ACCOUNT_PROJECTION, null, null, null)
+            ?.use {
+                if (it.moveToFirst()) {
+                    val accountName = it.getString(PROJECTION_ACCOUNT_INDEX_NAME)
+                    val accountType = it.getString(PROJECTION_ACCOUNT_INDEX_TYPE)
+                    return Account(accountName, accountType) // TODO Is this the right type?
+                }
+            }
+        return null
+    }
+
+    companion object {
+        private val ACCOUNT_PROJECTION = arrayOf(
+            CalendarContract.Calendars.ACCOUNT_NAME,
+            CalendarContract.Calendars.ACCOUNT_TYPE
+        )
+
+        private const val PROJECTION_ACCOUNT_INDEX_NAME = 0
+        private const val PROJECTION_ACCOUNT_INDEX_TYPE = 1
+    }
+}
diff --git a/app/src/main/java/com/android/calendar/datasource/CalendarDataSource.kt b/app/src/main/java/com/android/calendar/datasource/CalendarDataSource.kt
new file mode 100644
index 0000000..cfe3053
--- /dev/null
+++ b/app/src/main/java/com/android/calendar/datasource/CalendarDataSource.kt
@@ -0,0 +1,286 @@
+/*
+ *  Copyright (c) 2024 The Etar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.android.calendar.datasource
+
+import android.app.Application
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.database.ContentObserver
+import android.net.Uri
+import android.provider.CalendarContract
+import com.android.calendar.Utils
+import com.android.calendar.persistence.Calendar
+import com.android.calendar.persistence.CalendarRepository
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import ws.xsoh.etar.R
+
+/**
+ * Datasource for Calendar entities
+ */
+class CalendarDataSource(
+    private val application: Application
+) {
+    /**
+     * Convenience to get the content resolver
+     */
+    private val contentResolver: ContentResolver
+        get() = application.contentResolver
+
+    /**
+     * Get all calendars
+     */
+    private fun getContentProviderValue(): List<Calendar> {
+        val calendars: MutableList<Calendar> = mutableListOf()
+
+        contentResolver.query(
+            CalendarContract.Calendars.CONTENT_URI,
+            PROJECTION,
+            null,
+            null,
+            CalendarContract.Calendars.ACCOUNT_NAME
+        )?.use {
+            while (it.moveToNext()) {
+                val id = it.getLong(PROJECTION_INDEX_ID)
+                val accountName = it.getString(PROJECTION_INDEX_ACCOUNT_NAME)
+                val accountType = it.getString(PROJECTION_INDEX_ACCOUNT_TYPE)
+                val name = it.getString(PROJECTION_INDEX_NAME)
+                val displayName =
+                    it.getString(PROJECTION_INDEX_CALENDAR_DISPLAY_NAME)
+                val color = it.getInt(PROJECTION_INDEX_CALENDAR_COLOR)
+                val visible = it.getInt(PROJECTION_INDEX_VISIBLE) == 1
+                val syncEvents = it.getInt(PROJECTION_INDEX_SYNC_EVENTS) == 1
+                val isPrimary = it.getInt(PROJECTION_INDEX_IS_PRIMARY) == 1
+                val isLocal = accountType == CalendarContract.ACCOUNT_TYPE_LOCAL
+
+                calendars.add(
+                    Calendar(
+                        id = id,
+                        accountName = accountName,
+                        accountType = accountType,
+                        name = name,
+                        displayName = displayName,
+                        color = color,
+                        visible = visible,
+                        syncEvents = syncEvents,
+                        isPrimary = isPrimary,
+                        isLocal = isLocal
+                    )
+                )
+            }
+        }
+        return calendars
+    }
+
+    /**
+     * Get a flow of all calendars.
+     *
+     * Updates on any changes.
+     */
+    fun getAllCalendars(): Flow<List<Calendar>> =
+        callbackFlow {
+            val observer = object : ContentObserver(null) {
+                override fun onChange(self: Boolean) {
+                    // Notify collectors that data at the uri has changed
+                    trySend(getContentProviderValue())
+                }
+            }
+
+            if (Utils.isCalendarPermissionGranted(application, true)) {
+                contentResolver.registerContentObserver(
+                    CalendarContract.Calendars.CONTENT_URI,
+                    true,
+                    observer
+                )
+                trySend(getContentProviderValue())
+            }
+
+            awaitClose {
+                contentResolver.unregisterContentObserver(observer)
+            }
+        }
+
+    /**
+     * Creates the content values needed to insert a local account.
+     */
+    private fun buildLocalCalendarContentValues(
+        accountName: String,
+        displayName: String
+    ): ContentValues {
+        val internalName = "calendar_local_" + displayName.replace("[^a-zA-Z0-9]".toRegex(), "")
+
+        return ContentValues().apply {
+            put(CalendarContract.Calendars.ACCOUNT_NAME, accountName)
+            put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
+            put(CalendarContract.Calendars.OWNER_ACCOUNT, accountName)
+            put(CalendarContract.Calendars.NAME, internalName)
+            put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, displayName)
+            put(CalendarContract.Calendars.CALENDAR_COLOR_KEY, DEFAULT_COLOR_KEY)
+            put(
+                CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL,
+                CalendarContract.Calendars.CAL_ACCESS_ROOT
+            )
+            put(CalendarContract.Calendars.VISIBLE, 1)
+            put(CalendarContract.Calendars.SYNC_EVENTS, 1)
+            put(CalendarContract.Calendars.IS_PRIMARY, 0)
+            put(CalendarContract.Calendars.CAN_ORGANIZER_RESPOND, 0)
+            put(CalendarContract.Calendars.CAN_MODIFY_TIME_ZONE, 1)
+            // from Android docs: "the device will only process METHOD_DEFAULT and METHOD_ALERT reminders"
+            put(
+                CalendarContract.Calendars.ALLOWED_REMINDERS,
+                CalendarContract.Reminders.METHOD_ALERT.toString()
+            )
+            put(
+                CalendarContract.Calendars.ALLOWED_ATTENDEE_TYPES,
+                CalendarContract.Attendees.TYPE_NONE.toString()
+            )
+        }
+    }
+
+    /**
+     * Build content values needed to insert calendar colors
+     */
+    private fun buildLocalCalendarColorsContentValues(
+        accountName: String,
+        colorType: Int,
+        colorKey: String,
+        color: Int
+    ): ContentValues {
+        return ContentValues().apply {
+            put(CalendarContract.Colors.ACCOUNT_NAME, accountName)
+            put(CalendarContract.Colors.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
+            put(CalendarContract.Colors.COLOR_TYPE, colorType)
+            put(CalendarContract.Colors.COLOR_KEY, colorKey)
+            put(CalendarContract.Colors.COLOR, color)
+        }
+    }
+
+    /**
+     * TODO Figure out exactly what this does
+     */
+    private fun areCalendarColorsExisting(accountName: String): Boolean {
+        contentResolver.query(
+            CalendarContract.Colors.CONTENT_URI,
+            null,
+            CalendarContract.Colors.ACCOUNT_NAME + "=? AND " + CalendarContract.Colors.ACCOUNT_TYPE + "=?",
+            arrayOf(accountName, CalendarContract.ACCOUNT_TYPE_LOCAL),
+            null
+        ).use {
+            if (it!!.moveToFirst()) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * TODO Figure out why is this a maybe?
+     */
+    private fun maybeAddCalendarAndEventColors(accountName: String) {
+        if (areCalendarColorsExisting(accountName)) {
+            return
+        }
+
+        val defaultColors: IntArray =
+            application.resources.getIntArray(R.array.defaultCalendarColors)
+
+        val insertBulk = mutableListOf<ContentValues>()
+        for ((i, color) in defaultColors.withIndex()) {
+            val colorKey = i.toString()
+            val colorCvCalendar = buildLocalCalendarColorsContentValues(
+                accountName,
+                CalendarContract.Colors.TYPE_CALENDAR,
+                colorKey,
+                color
+            )
+            val colorCvEvent = buildLocalCalendarColorsContentValues(
+                accountName,
+                CalendarContract.Colors.TYPE_EVENT,
+                colorKey,
+                color
+            )
+            insertBulk.add(colorCvCalendar)
+            insertBulk.add(colorCvEvent)
+        }
+        contentResolver.bulkInsert(
+            CalendarRepository.asLocalCalendarSyncAdapter(
+                accountName,
+                CalendarContract.Colors.CONTENT_URI
+            ), insertBulk.toTypedArray()
+        )
+    }
+
+    /**
+     * TODO Document
+     */
+    fun addLocalCalendar(accountName: String, displayName: String): Uri {
+
+        maybeAddCalendarAndEventColors(accountName)
+
+        val cv = buildLocalCalendarContentValues(accountName, displayName)
+        return contentResolver.insert(
+            CalendarRepository.asLocalCalendarSyncAdapter(
+                accountName,
+                CalendarContract.Calendars.CONTENT_URI
+            ), cv
+        )
+            ?: throw IllegalArgumentException()
+    }
+
+    /**
+     * TODO Document
+     */
+    fun deleteLocalCalendar(accountName: String, id: Long): Boolean {
+        val calUri = ContentUris.withAppendedId(
+            CalendarRepository.asLocalCalendarSyncAdapter(
+                accountName,
+                CalendarContract.Calendars.CONTENT_URI
+            ), id
+        )
+        return contentResolver.delete(calUri, null, null) == 1
+    }
+
+    companion object {
+        private val PROJECTION = arrayOf(
+            CalendarContract.Calendars._ID,
+            CalendarContract.Calendars.ACCOUNT_NAME,
+            CalendarContract.Calendars.ACCOUNT_TYPE,
+            CalendarContract.Calendars.OWNER_ACCOUNT,
+            CalendarContract.Calendars.NAME,
+            CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
+            CalendarContract.Calendars.CALENDAR_COLOR,
+            CalendarContract.Calendars.VISIBLE,
+            CalendarContract.Calendars.SYNC_EVENTS,
+            CalendarContract.Calendars.IS_PRIMARY
+        )
+        private const val PROJECTION_INDEX_ID = 0
+        private const val PROJECTION_INDEX_ACCOUNT_NAME = 1
+        private const val PROJECTION_INDEX_ACCOUNT_TYPE = 2
+        private const val PROJECTION_INDEX_OWNER_ACCOUNT = 3
+        private const val PROJECTION_INDEX_NAME = 4
+        private const val PROJECTION_INDEX_CALENDAR_DISPLAY_NAME = 5
+        private const val PROJECTION_INDEX_CALENDAR_COLOR = 6
+        private const val PROJECTION_INDEX_VISIBLE = 7
+        private const val PROJECTION_INDEX_SYNC_EVENTS = 8
+        private const val PROJECTION_INDEX_IS_PRIMARY = 9
+
+        private const val DEFAULT_COLOR_KEY = "1"
+    }
+}
diff --git a/app/src/main/java/com/android/calendar/datasource/EventDataSource.kt b/app/src/main/java/com/android/calendar/datasource/EventDataSource.kt
new file mode 100644
index 0000000..5b5ee07
--- /dev/null
+++ b/app/src/main/java/com/android/calendar/datasource/EventDataSource.kt
@@ -0,0 +1,53 @@
+/*
+ *  Copyright (c) 2024 The Etar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.android.calendar.datasource
+
+import android.app.Application
+import android.provider.CalendarContract
+
+/**
+ * Datasource of Event entities
+ */
+class EventDataSource(
+    private val application: Application
+) {
+    /**
+     * TODO Document
+     */
+    fun queryNumberOfEvents(calendarId: Long): Long? {
+        val args = arrayOf(calendarId.toString())
+        application.contentResolver.query(
+            CalendarContract.Events.CONTENT_URI,
+            PROJECTION_COUNT_EVENTS,
+            WHERE_COUNT_EVENTS, args, null
+        )?.use {
+            if (it.moveToFirst()) {
+                return it.getLong(PROJECTION_COUNT_EVENTS_INDEX_COUNT)
+            }
+        }
+        return null
+    }
+
+    companion object {
+        private val PROJECTION_COUNT_EVENTS = arrayOf(
+            CalendarContract.Events._COUNT
+        )
+        private const val PROJECTION_COUNT_EVENTS_INDEX_COUNT = 0
+        private const val WHERE_COUNT_EVENTS = CalendarContract.Events.CALENDAR_ID + "=?"
+    }
+}
diff --git a/app/src/main/java/com/android/calendar/event/EditEventFragment.java b/app/src/main/java/com/android/calendar/event/EditEventFragment.java
index d226abb..302a817 100644
--- a/app/src/main/java/com/android/calendar/event/EditEventFragment.java
+++ b/app/src/main/java/com/android/calendar/event/EditEventFragment.java
@@ -308,7 +308,7 @@
         super.onAttach(activity);
         mActivity = (AppCompatActivity) activity;
 
-        mHelper = new EditEventHelper(activity, null);
+        mHelper = new EditEventHelper(activity);
         mHandler = new QueryHandler(activity.getContentResolver());
         mModel = new CalendarEventModel(activity, mIntent);
         mInputMethodManager = (InputMethodManager)
@@ -688,7 +688,7 @@
             long eventId;
             switch (token) {
                 case TOKEN_EVENT:
-                    if (cursor.getCount() == 0) {
+                    if (!cursor.moveToFirst()) {
                         // The cursor is empty. This can happen if the event
                         // was deleted.
                         cursor.close();
@@ -752,13 +752,32 @@
                         setModelIfDone(TOKEN_REMINDERS);
                     }
 
+                    final String selection;
+                    final String[] selectionArgs;
+                    final boolean isRecurring = !TextUtils.isEmpty(mModel.mRrule);
+                    if (isRecurring && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+                        // recurring event AND api level < 30. disable changing calendars as moving
+                        // recurrences is currently only possible for api level 30+
+                        selection = EditEventHelper.CALENDARS_WHERE;
+                        selectionArgs = new String[] { Long.toString(mModel.mCalendarId) };
+                    } else if (isRecurring) {
+                        // recurring event AND api level >= 30. enable changing calendars to synced
+                        // calendars only, as we currently don't allow recurrence exceptions in local calendars
+                        // and would lose them when moving the recurrence between calendars
+                        selection = EditEventHelper.CALENDARS_WHERE_SYNCED_WRITEABLE_VISIBLE;
+                        selectionArgs = null;
+                    } else {
+                        // non recurring event. enable changing calendars to all calendars.
+                        selection = EditEventHelper.CALENDARS_WHERE_WRITEABLE_VISIBLE;
+                        selectionArgs = null;
+                    }
+
                     // TOKEN_CALENDARS
-                    String[] selArgs = {
-                            Long.toString(mModel.mCalendarId)
-                    };
                     mHandler.startQuery(TOKEN_CALENDARS, null, Calendars.CONTENT_URI,
-                            EditEventHelper.CALENDARS_PROJECTION, EditEventHelper.CALENDARS_WHERE,
-                            selArgs /* selection args */, null /* sort order */);
+                            EditEventHelper.CALENDARS_PROJECTION,
+                            selection,
+                            selectionArgs,
+                            null /* sort order */);
 
                     // TOKEN_COLORS
                     mHandler.startQuery(TOKEN_COLORS, null, Colors.CONTENT_URI,
@@ -770,9 +789,7 @@
                             mModel.mCalendarAccountName,
                             mModel.mCalendarAccountType
                     );
-                    selArgs = new String[]{
-                            Long.toString(eventId)
-                    };
+                    String[] selArgs = new String[]{ Long.toString(eventId) };
                     mHandler.startQuery(TOKEN_EXTENDED, null, extendedPropUri,
                             EditEventHelper.EXTENDED_PROJECTION,
                             EditEventHelper.EXTENDED_WHERE_EVENT, selArgs, null);
@@ -780,7 +797,7 @@
                     setModelIfDone(TOKEN_EVENT);
                     break;
                 case TOKEN_ATTENDEES:
-                    try {
+                    try (cursor) {
                         while (cursor.moveToNext()) {
                             String name = cursor.getString(EditEventHelper.ATTENDEES_INDEX_NAME);
                             String email = cursor.getString(EditEventHelper.ATTENDEES_INDEX_EMAIL);
@@ -824,14 +841,12 @@
                             mModel.addAttendee(attendee);
                             mOriginalModel.addAttendee(attendee);
                         }
-                    } finally {
-                        cursor.close();
                     }
 
                     setModelIfDone(TOKEN_ATTENDEES);
                     break;
                 case TOKEN_REMINDERS:
-                    try {
+                    try (cursor) {
                         // Add all reminders to the models
                         while (cursor.moveToNext()) {
                             int minutes = cursor.getInt(EditEventHelper.REMINDERS_INDEX_MINUTES);
@@ -844,55 +859,45 @@
                         // Sort appropriately for display
                         Collections.sort(mModel.mReminders);
                         Collections.sort(mOriginalModel.mReminders);
-                    } finally {
-                        cursor.close();
                     }
 
                     setModelIfDone(TOKEN_REMINDERS);
                     break;
                 case TOKEN_CALENDARS:
-                    try {
-                        if (mModel.mId == -1) {
-                            // Populate Calendar spinner only if no event id is set.
-                            MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor);
-                            if (DEBUG) {
-                                Log.d(TAG, "onQueryComplete: setting cursor with "
-                                        + matrixCursor.getCount() + " calendars");
-                            }
-                            mView.setCalendarsCursor(matrixCursor, isAdded() && isResumed(),
-                                    mCalendarId);
-                        } else {
+                    try (cursor) {
+                        MatrixCursor matrixCursor = Utils.matrixCursorFromCursor(cursor);
+                        if (DEBUG) {
+                            Log.d(TAG, "onQueryComplete: setting cursor with " + matrixCursor.getCount() + " calendars");
+                        }
+                        if (mModel.mId != -1) {
                             // Populate model for an existing event
                             EditEventHelper.setModelFromCalendarCursor(mModel, cursor, activity);
                             EditEventHelper.setModelFromCalendarCursor(mOriginalModel, cursor, activity);
                         }
-                    } finally {
-                        cursor.close();
+                        mView.setCalendarsCursor(matrixCursor, isAdded() && isResumed(), mModel.mCalendarId);
                     }
                     setModelIfDone(TOKEN_CALENDARS);
                     break;
                 case TOKEN_COLORS:
-                    if (cursor.moveToFirst()) {
-                        EventColorCache cache = new EventColorCache();
-                        do {
-                            String colorKey = cursor.getString(EditEventHelper.COLORS_INDEX_COLOR_KEY);
-                            int rawColor = cursor.getInt(EditEventHelper.COLORS_INDEX_COLOR);
-                            int displayColor = Utils.getDisplayColorFromColor(activity, rawColor);
-                            String accountName = cursor
-                                    .getString(EditEventHelper.COLORS_INDEX_ACCOUNT_NAME);
-                            String accountType = cursor
-                                    .getString(EditEventHelper.COLORS_INDEX_ACCOUNT_TYPE);
-                            cache.insertColor(accountName, accountType,
-                                    displayColor, colorKey);
-                        } while (cursor.moveToNext());
-                        cache.sortPalettes(new HsvColorComparator());
+                    try (cursor) {
+                        if (cursor.moveToFirst()) {
+                            EventColorCache cache = new EventColorCache();
+                            do {
+                                String colorKey = cursor.getString(EditEventHelper.COLORS_INDEX_COLOR_KEY);
+                                int rawColor = cursor.getInt(EditEventHelper.COLORS_INDEX_COLOR);
+                                int displayColor = Utils.getDisplayColorFromColor(activity, rawColor);
+                                String accountName = cursor
+                                        .getString(EditEventHelper.COLORS_INDEX_ACCOUNT_NAME);
+                                String accountType = cursor
+                                        .getString(EditEventHelper.COLORS_INDEX_ACCOUNT_TYPE);
+                                cache.insertColor(accountName, accountType,
+                                        displayColor, colorKey);
+                            } while (cursor.moveToNext());
+                            cache.sortPalettes(new HsvColorComparator());
 
-                        mModel.mEventColorCache = cache;
-                        mView.mColorPickerNewEvent.setOnClickListener(mOnColorPickerClicked);
-                        mView.mColorPickerExistingEvent.setOnClickListener(mOnColorPickerClicked);
-                    }
-                    if (cursor != null) {
-                        cursor.close();
+                            mModel.mEventColorCache = cache;
+                            mView.mColorPicker.setOnClickListener(mOnColorPickerClicked);
+                        }
                     }
 
                     // If the account name/type is null, the calendar event colors cannot be
diff --git a/app/src/main/java/com/android/calendar/event/EditEventHelper.java b/app/src/main/java/com/android/calendar/event/EditEventHelper.java
index b7126a8..7607203 100644
--- a/app/src/main/java/com/android/calendar/event/EditEventHelper.java
+++ b/app/src/main/java/com/android/calendar/event/EditEventHelper.java
@@ -17,12 +17,15 @@
 package com.android.calendar.event;
 
 import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Build;
+import android.provider.CalendarContract;
 import android.provider.CalendarContract.Attendees;
 import android.provider.CalendarContract.Calendars;
 import android.provider.CalendarContract.Colors;
@@ -54,6 +57,7 @@
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
+import java.util.List;
 import java.util.TimeZone;
 
 public class EditEventHelper {
@@ -93,7 +97,9 @@
             Events.EVENT_COLOR, // 23
             Events.EVENT_COLOR_KEY, // 24
             Events.ACCOUNT_NAME, // 25
-            Events.ACCOUNT_TYPE // 26
+            Events.ACCOUNT_TYPE, // 26
+            Events.EXDATE, // 27
+            Events.ORIGINAL_INSTANCE_TIME // 28
     };
     protected static final int EVENT_INDEX_ID = 0;
     protected static final int EVENT_INDEX_TITLE = 1;
@@ -122,6 +128,8 @@
     protected static final int EVENT_INDEX_EVENT_COLOR_KEY = 24;
     protected static final int EVENT_INDEX_ACCOUNT_NAME = 25;
     protected static final int EVENT_INDEX_ACCOUNT_TYPE = 26;
+    protected static final int EVENT_INDEX_EXDATE = 27;
+    protected static final int EVENT_INDEX_ORIGINAL_INSTANCE_TIME = 28;
 
     public static final String[] REMINDERS_PROJECTION = new String[] {
             Reminders._ID, // 0
@@ -157,6 +165,9 @@
 
     private final AsyncQueryService mService;
 
+    private final ContentResolver mContextResolver;
+    private final Context mContext;
+
     // This allows us to flag the event if something is wrong with it, right now
     // if an uri is provided for an event that doesn't exist in the db.
     protected boolean mEventOk = true;
@@ -209,6 +220,11 @@
     static final String CALENDARS_WHERE_WRITEABLE_VISIBLE = Calendars.CALENDAR_ACCESS_LEVEL + ">="
             + Calendars.CAL_ACCESS_CONTRIBUTOR + " AND " + Calendars.VISIBLE + "=1";
 
+    static final String CALENDARS_WHERE_SYNCED_WRITEABLE_VISIBLE =
+            Calendars.CALENDAR_ACCESS_LEVEL + ">=" + Calendars.CAL_ACCESS_CONTRIBUTOR
+                    + " AND " + Calendars.VISIBLE + "=1"
+                    + " AND " + Calendars.ACCOUNT_TYPE + "!='" + CalendarContract.ACCOUNT_TYPE_LOCAL + "'";
+
     static final String CALENDARS_WHERE = Calendars._ID + "=?";
 
     static final String[] COLORS_PROJECTION = new String[] {
@@ -266,11 +282,8 @@
 
     public EditEventHelper(Context context) {
         mService = ((AbstractCalendarActivity)context).getAsyncQueryService();
-    }
-
-    public EditEventHelper(Context context, CalendarEventModel model) {
-        this(context);
-        // TODO: Remove unnecessary constructor.
+        mContextResolver = context.getContentResolver();
+        this.mContext = context;
     }
 
     /**
@@ -307,7 +320,7 @@
             Log.e(TAG, "Attempted to save invalid model.");
             return false;
         }
-        if (originalModel != null && !isSameEvent(model, originalModel)) {
+        if (originalModel != null && originalModel.mId != model.mId) {
             Log.e(TAG, "Attempted to update existing event but models didn't refer to the same "
                     + "event.");
             return false;
@@ -345,6 +358,21 @@
             ops.add(b.build());
             forceSaveReminders = true;
 
+        } else if (originalModel.mCalendarId != model.mCalendarId) {
+            // event calendar has changed
+            eventIdIndex = ops.size();
+
+            // when moving recurrences between calendars, we must use the start time of the whole
+            // recurrence and not the start time of the currently opened instance.
+            if (!TextUtils.isEmpty(model.mRrule)) {
+                long recurrenceStartTime = getStartTimeForRecurrence(
+                    model.mOriginalStart, model.mStart, originalModel.mStart, model.mAllDay);
+                values.put(Events.DTSTART, recurrenceStartTime);
+            }
+
+            ops.addAll(moveEventToCalendar(model.mId, model.mSyncId, values));
+
+            forceSaveReminders = true;
         } else if (TextUtils.isEmpty(model.mRrule) && TextUtils.isEmpty(originalModel.mRrule)) {
             // Simple update to a non-recurring event
             checkTimeDependentFields(originalModel, model, values, modifyWhich);
@@ -435,8 +463,9 @@
             }
         }
 
-        // New Event or New Exception to an existing event
+        // New event or new exception to an existing event or event moved to different calendar
         boolean newEvent = (eventIdIndex != -1);
+
         ArrayList<ReminderEntry> originalReminders;
         if (originalModel != null) {
             originalReminders = originalModel.mReminders;
@@ -447,7 +476,7 @@
         if (newEvent) {
             saveRemindersWithBackRef(ops, eventIdIndex, reminders, originalReminders,
                     forceSaveReminders);
-        } else if (uri != null) {
+        } else {
             long eventId = ContentUris.parseId(uri);
             saveReminders(ops, eventId, reminders, originalReminders, forceSaveReminders);
         }
@@ -518,9 +547,7 @@
             ops.add(b.build());
         }
 
-        // TODO: is this the right test? this currently checks if this is
-        // a new event or an existing event. or is this a paranoia check?
-        if (hasAttendeeData && (newEvent || uri != null)) {
+        if (hasAttendeeData) {
             String attendees = model.getAttendeesString();
             String originalAttendeesString;
             if (originalModel != null) {
@@ -604,13 +631,83 @@
             }
         }
 
-
         mService.startBatch(mService.getNextToken(), null, android.provider.CalendarContract.AUTHORITY, ops,
                 Utils.UNDO_DELAY);
 
         return true;
     }
 
+    /**
+     * Moves an existing event to a different calendar provider by deleting the event from its old
+     * calendar and creating the same event in the new calendar.
+     *
+     * @param eventId    the id of the event in the old calendar which should be moved. this is used
+     *                   to delete the event from the old calendars, and in case of a recurrence,
+     *                   its exceptions.
+     * @param syncId     sync-id of the event to be moved. used the find recurrence exceptions if
+     *                   provided.
+     * @param eventValues the new values of the event. the id of the new calendar should be set here
+     *                   via {@link Events#CALENDAR_ID}. if this is a recurrence, please see
+     *                   {@link #getStartTimeForRecurrence(long, long, long, boolean)} for getting
+     *                   the DTSTART value correct.
+     * @return a list of content provider operations which have to be performed in order to move the
+     * event to the new calendar.
+     */
+    private List<ContentProviderOperation> moveEventToCalendar(long eventId, String syncId,
+        ContentValues eventValues) {
+
+        final List<ContentProviderOperation> ops = new ArrayList<>();
+
+        // set eventIdIndex for back referencing when inserting recurrence exceptions later
+        int eventIdIndex = ops.size();
+
+        // create event in new calendar and delete the event from the old calendar
+        // insert of the new event must always be the first operation, as calling code may depend on this!
+        ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI)
+            .withValues(eventValues)
+            .build());
+
+        ops.add(ContentProviderOperation.newDelete(
+            ContentUris.withAppendedId(Events.CONTENT_URI, eventId)).build());
+
+        // if syncId is not provided, or api level is < 30, return here before trying to move exceptions
+        if (TextUtils.isEmpty(syncId) || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+            return ops;
+        }
+
+        // retrieve events exceptions, create them in target calendar, delete them from source calendar
+        try (Cursor cursor = mContextResolver.query(
+            Events.CONTENT_URI,
+            EVENT_PROJECTION,
+            Events.ORIGINAL_SYNC_ID + "= ? AND " + Events._SYNC_ID + " IS NULL",
+            new String[]{syncId},
+            null
+        )) {
+            while (cursor != null && cursor.moveToNext()) {
+                final CalendarEventModel model = new CalendarEventModel();
+                setModelFromCursor(model, cursor, mContext);
+                final ContentValues values = getContentValuesFromModel(model);
+                values.put(Events.CALENDAR_ID, eventValues.getAsString(Events.CALENDAR_ID));
+                values.put(Events.ORIGINAL_INSTANCE_TIME, model.mOriginalTime);
+                values.remove(Events.ORGANIZER);
+
+                ops.add(ContentProviderOperation.newInsert(Events.CONTENT_URI)
+                    .withValues(values)
+                    // note: this call requires API level 30
+                    .withValueBackReference(Events.ORIGINAL_ID, eventIdIndex, Events._ID)
+                    .build());
+
+                Uri.Builder exceptionUriBuilder = Events.CONTENT_EXCEPTION_URI.buildUpon();
+                ContentUris.appendId(exceptionUriBuilder, eventId); // original event id
+                ContentUris.appendId(exceptionUriBuilder, model.mId); // exception event id
+
+                ops.add(ContentProviderOperation.newDelete(exceptionUriBuilder.build()).build());
+            }
+        }
+
+        return ops;
+    }
+
     public static LinkedHashSet<Rfc822Token> getAddressesFromList(String list,
             Rfc822Validator validator) {
         LinkedHashSet<Rfc822Token> addresses = new LinkedHashSet<Rfc822Token>();
@@ -696,33 +793,53 @@
             return;
         }
 
-        // If we are modifying all events then we need to set DTSTART to the
-        // start time of the first event in the series, not the current
-        // date and time. If the start time of the event was changed
-        // (from, say, 3pm to 4pm), then we want to add the time difference
-        // to the start time of the first event in the series (the DTSTART
-        // value). If we are modifying one instance or all following instances,
-        // then we leave the DTSTART field alone.
+        // If we are modifying all events in then we need to set DTSTART to the
+        // start time of the first event in the series, not the current date and time.
         if (modifyWhich == MODIFY_ALL) {
-            long oldStartMillis = originalModel.mStart;
-            if (oldBegin != newBegin) {
-                // The user changed the start time of this event
-                long offset = newBegin - oldBegin;
-                oldStartMillis += offset;
-            }
-            if (newAllDay) {
-                Time time = new Time(Time.TIMEZONE_UTC);
-                time.set(oldStartMillis);
-                time.setHour(0);
-                time.setMinute(0);
-                time.setSecond(0);
-                oldStartMillis = time.toMillis();
-            }
-            values.put(Events.DTSTART, oldStartMillis);
+            values.put(Events.DTSTART,
+                    getStartTimeForRecurrence(oldBegin, newBegin, originalModel.mStart, newAllDay));
         }
     }
 
     /**
+     * If we are modifying all events in a recurrence, then we need to set DTSTART to the start time
+     * of the first event in the recurrence, not to the date and time of the event currently being
+     * modified. If the start time of the event was changed (from, say, 3pm to 4pm), then we need to
+     * add the time difference to the start time of the first event in the recurrence (the DTSTART
+     * value).
+     * <p>
+     * If we are modifying one instance or if we are modifying all following instances of a
+     * recurrence then we leave the DTSTART field alone. This method should not be used to get the
+     * start time for these cases!
+     *
+     * @param oldStartTime    the start time of the currently opened event, before user
+     *                        modification
+     * @param newStartTime    the start time of the currently opened event, after user modification
+     * @param seriesStartTime the start time of the recurring series
+     * @param isAllDay        whether the event is an all-day event, after user modification
+     */
+    private long getStartTimeForRecurrence(long oldStartTime, long newStartTime,
+        long seriesStartTime, boolean isAllDay) {
+
+        if (oldStartTime != newStartTime) {
+            // The user changed the start time of this event
+            long offset = newStartTime - oldStartTime;
+            seriesStartTime += offset;
+        }
+
+        if (isAllDay) {
+            Time time = new Time(Time.TIMEZONE_UTC);
+            time.set(seriesStartTime);
+            time.setHour(0);
+            time.setMinute(0);
+            time.setSecond(0);
+            seriesStartTime = time.toMillis();
+        }
+
+        return seriesStartTime;
+    }
+
+    /**
      * Prepares an update to the original event so it stops where the new series
      * begins. When we update 'this and all following' events we need to change
      * the original event to end before a new series starts. This creates an
@@ -827,30 +944,6 @@
     }
 
     /**
-     * Compares two models to ensure that they refer to the same event. This is
-     * a safety check to make sure an updated event model refers to the same
-     * event as the original model. If the original model is null then this is a
-     * new event or we're forcing an overwrite so we return true in that case.
-     * The important identifiers are the Calendar Id and the Event Id.
-     *
-     * @return
-     */
-    public static boolean isSameEvent(CalendarEventModel model, CalendarEventModel originalModel) {
-        if (originalModel == null) {
-            return true;
-        }
-
-        if (model.mCalendarId != originalModel.mCalendarId) {
-            return false;
-        }
-        if (model.mId != originalModel.mId) {
-            return false;
-        }
-
-        return true;
-    }
-
-    /**
      * Saves the reminders, if they changed. Returns true if operations to
      * update the database were added.
      *
@@ -1075,18 +1168,18 @@
      * Uses an event cursor to fill in the given model This method assumes the
      * cursor used {@link #EVENT_PROJECTION} as it's query projection. It uses
      * the cursor to fill in the given model with all the information available.
+     * Only the row the cursor currently points to is used.
      *
      * @param model The model to fill in
      * @param cursor An event cursor that used {@link #EVENT_PROJECTION} for the query
      */
     public static void setModelFromCursor(CalendarEventModel model, Cursor cursor, Context context) {
-        if (model == null || cursor == null || cursor.getCount() != 1) {
+        if (model == null || cursor == null || cursor.getCount() < 1) {
             Log.wtf(TAG, "Attempted to build non-existent model or from an incorrect query.");
             return;
         }
 
         model.clear();
-        cursor.moveToFirst();
 
         model.mId = cursor.getInt(EVENT_INDEX_ID);
         model.mTitle = cursor.getString(EVENT_INDEX_TITLE);
@@ -1105,6 +1198,7 @@
         }
         String rRule = cursor.getString(EVENT_INDEX_RRULE);
         model.mRrule = rRule;
+        model.mExDate = cursor.getString(EVENT_INDEX_EXDATE);
         model.mSyncId = cursor.getString(EVENT_INDEX_SYNC_ID);
         model.mSyncAccountName = cursor.getString(EVENT_INDEX_ACCOUNT_NAME);
         model.mSyncAccountType = cursor.getString(EVENT_INDEX_ACCOUNT_TYPE);
@@ -1114,6 +1208,7 @@
         model.mHasAttendeeData = cursor.getInt(EVENT_INDEX_HAS_ATTENDEE_DATA) != 0;
         model.mOriginalSyncId = cursor.getString(EVENT_INDEX_ORIGINAL_SYNC_ID);
         model.mOriginalId = cursor.getLong(EVENT_INDEX_ORIGINAL_ID);
+        model.mOriginalTime = cursor.getLong(EVENT_INDEX_ORIGINAL_INSTANCE_TIME);
         model.mOrganizer = cursor.getString(EVENT_INDEX_ORGANIZER);
         model.mIsOrganizer = model.mOwnerAccount.equalsIgnoreCase(model.mOrganizer);
         model.mGuestsCanModify = cursor.getInt(EVENT_INDEX_GUESTS_CAN_MODIFY) != 0;
@@ -1137,8 +1232,6 @@
         } else {
             model.mEnd = cursor.getLong(EVENT_INDEX_DTEND);
         }
-
-        model.mModelUpdatedWithEventCursor = true;
     }
 
     /**
@@ -1160,12 +1253,6 @@
             return false;
         }
 
-        if (!model.mModelUpdatedWithEventCursor) {
-            Log.wtf(TAG,
-                    "Can't update model with a Calendar cursor until it has seen an Event cursor.");
-            return false;
-        }
-
         cursor.moveToPosition(-1);
         while (cursor.moveToNext()) {
             if (model.mCalendarId != cursor.getInt(CALENDARS_INDEX_ID)) {
@@ -1301,6 +1388,7 @@
         values.put(Events.TITLE, title);
         values.put(Events.ALL_DAY, isAllDay ? 1 : 0);
         values.put(Events.DTSTART, startMillis);
+        values.put(Events.EXDATE, model.mExDate);
         values.put(Events.RRULE, rrule);
         if (!TextUtils.isEmpty(rrule)) {
             addRecurrenceRule(values, model);
diff --git a/app/src/main/java/com/android/calendar/event/EditEventView.java b/app/src/main/java/com/android/calendar/event/EditEventView.java
index 8c83296..84304e9 100644
--- a/app/src/main/java/com/android/calendar/event/EditEventView.java
+++ b/app/src/main/java/com/android/calendar/event/EditEventView.java
@@ -135,8 +135,7 @@
     Button mStartTimeButton;
     Button mEndTimeButton;
     Button mTimezoneButton;
-    View mColorPickerNewEvent;
-    View mColorPickerExistingEvent;
+    View mColorPicker;
     View mTimezoneRow;
     TextView mStartTimeHome;
     TextView mStartDateHome;
@@ -156,9 +155,7 @@
     TextView mWhenView;
     TextView mTimezoneTextView;
     MultiAutoCompleteTextView mAttendeesList;
-    View mCalendarSelectorGroup;
     View mCalendarSelectorGroupBackground;
-    View mCalendarStaticGroup;
     View mLocationGroup;
     View mDescriptionGroup;
     View mUrlGroup;
@@ -260,9 +257,7 @@
         mRruleButton = (Button) view.findViewById(R.id.rrule);
         mAvailabilitySpinner = (Spinner) view.findViewById(R.id.availability);
         mAccessLevelSpinner = (Spinner) view.findViewById(R.id.visibility);
-        mCalendarSelectorGroup = view.findViewById(R.id.calendar_selector_group);
         mCalendarSelectorGroupBackground = view.findViewById(R.id.calendar_selector_group_background);
-        mCalendarStaticGroup = view.findViewById(R.id.calendar_group);
         mRemindersGroup = view.findViewById(R.id.reminder_items_container);
         mResponseGroup = view.findViewById(R.id.response_group);
         mOrganizerGroup = view.findViewById(R.id.organizer_row);
@@ -274,8 +269,7 @@
         mEndHomeGroup = view.findViewById(R.id.to_row_home_tz);
         mAttendeesList = (MultiAutoCompleteTextView) view.findViewById(R.id.attendees);
 
-        mColorPickerNewEvent = view.findViewById(R.id.change_color_new_event);
-        mColorPickerExistingEvent = view.findViewById(R.id.change_color_new_event);
+        mColorPicker = view.findViewById(R.id.change_color);
 
         mTitleTextView.setTag(mTitleTextView.getBackground());
         mLocationTextView.setTag(mLocationTextView.getBackground());
@@ -637,22 +631,19 @@
             mEmailValidator.setRemoveInvalid(false);
         }
 
-        // If this was a new event we need to fill in the Calendar information
-        if (mModel.mUri == null) {
-            mModel.mCalendarId = mCalendarsSpinner.getSelectedItemId();
-            int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
-            if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
-                String calendarOwner = mCalendarsCursor.getString(
-                        EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT);
-                String calendarName = mCalendarsCursor.getString(
-                        EditEventHelper.CALENDARS_INDEX_DISPLAY_NAME);
-                String defaultCalendar = calendarOwner + "/" + calendarName;
-                Utils.setSharedPreference(
-                        mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, defaultCalendar);
-                mModel.mOwnerAccount = calendarOwner;
-                mModel.mOrganizer = calendarOwner;
-                mModel.mCalendarId = mCalendarsCursor.getLong(EditEventHelper.CALENDARS_INDEX_ID);
-            }
+        mModel.mCalendarId = mCalendarsSpinner.getSelectedItemId();
+        int calendarCursorPosition = mCalendarsSpinner.getSelectedItemPosition();
+        if (mCalendarsCursor.moveToPosition(calendarCursorPosition)) {
+            String calendarOwner = mCalendarsCursor.getString(
+                    EditEventHelper.CALENDARS_INDEX_OWNER_ACCOUNT);
+            String calendarName = mCalendarsCursor.getString(
+                    EditEventHelper.CALENDARS_INDEX_DISPLAY_NAME);
+            String defaultCalendar = calendarOwner + "/" + calendarName;
+            Utils.setSharedPreference(
+                    mActivity, GeneralPreferences.KEY_DEFAULT_CALENDAR, defaultCalendar);
+            mModel.mOwnerAccount = calendarOwner;
+            mModel.mOrganizer = calendarOwner;
+            mModel.mCalendarId = mCalendarsCursor.getLong(EditEventHelper.CALENDARS_INDEX_ID);
         }
 
         if (mModel.mAllDay) {
@@ -1000,21 +991,6 @@
             mResponseGroup.setVisibility(View.GONE);
         }
 
-        if (model.mUri != null) {
-            // This is an existing event so hide the calendar spinner
-            // since we can't change the calendar.
-            View calendarGroup = mView.findViewById(R.id.calendar_selector_group);
-            calendarGroup.setVisibility(View.VISIBLE);
-            TextView tv = (TextView) mView.findViewById(R.id.calendar_textview);
-            tv.setText(model.mCalendarDisplayName);
-            tv = (TextView) mView.findViewById(R.id.calendar_textview_secondary);
-            if (tv != null) {
-                tv.setText(model.mOwnerAccount);
-            }
-        } else {
-            View calendarGroup = mView.findViewById(R.id.calendar_group);
-            calendarGroup.setVisibility(View.GONE);
-        }
         if (model.isEventColorInitialized()) {
             updateHeadlineColor(model.getEventColor());
         }
@@ -1119,9 +1095,8 @@
     }
 
     /**
-     * Configures the Calendars spinner.  This is only done for new events, because only new
-     * events allow you to select a calendar while editing an event.
-     * <p>
+     * Configures the Calendars spinner.
+     *
      * We tuck a reference to a Cursor with calendar database data into the spinner, so that
      * we can easily extract calendar-specific values when the value changes (the spinner's
      * onItemSelected callback is configured).
@@ -1175,7 +1150,6 @@
             } else if (Log.isLoggable(TAG, Log.DEBUG)) {
                 Log.d(TAG, "SetCalendarsCursor:Save failed and unable to exit view");
             }
-            return;
         }
     }
 
@@ -1208,8 +1182,6 @@
                 v.setEnabled(false);
                 v.setBackgroundDrawable(null);
             }
-            mCalendarSelectorGroup.setVisibility(View.GONE);
-            mCalendarStaticGroup.setVisibility(View.VISIBLE);
             mRruleButton.setEnabled(false);
             if (EditEventHelper.canAddReminders(mModel)) {
                 mRemindersGroup.setVisibility(View.VISIBLE);
@@ -1225,6 +1197,7 @@
             if (TextUtils.isEmpty(mUrlTextView.getText())) {
                 mUrlGroup.setVisibility(View.GONE);
             }
+            mCalendarsSpinner.setEnabled(false);
         } else {
             for (View v : mViewOnlyList) {
                 v.setVisibility(View.GONE);
@@ -1240,13 +1213,6 @@
                             mOriginalPadding[3]);
                 }
             }
-            if (mModel.mUri == null) {
-                mCalendarSelectorGroup.setVisibility(View.VISIBLE);
-                mCalendarStaticGroup.setVisibility(View.GONE);
-            } else {
-                mCalendarSelectorGroup.setVisibility(View.GONE);
-                mCalendarStaticGroup.setVisibility(View.VISIBLE);
-            }
             if (mModel.mOriginalSyncId == null) {
                 mRruleButton.setEnabled(true);
             } else {
@@ -1258,6 +1224,9 @@
             mLocationGroup.setVisibility(View.VISIBLE);
             mDescriptionGroup.setVisibility(View.VISIBLE);
             mUrlGroup.setVisibility(View.VISIBLE);
+
+            // disallow changing calendar for recurrences when not modifying all instances
+            mCalendarsSpinner.setEnabled(mode == Utils.MODIFY_ALL);
         }
         setAllDayViewsVisibility(mAllDayCheckBox.isChecked());
     }
@@ -1511,18 +1480,11 @@
     }
 
     public void setColorPickerButtonStates(boolean showColorPalette) {
-        if (showColorPalette) {
-            mColorPickerNewEvent.setVisibility(View.VISIBLE);
-            mColorPickerExistingEvent.setVisibility(View.VISIBLE);
-        } else {
-            mColorPickerNewEvent.setVisibility(View.INVISIBLE);
-            mColorPickerExistingEvent.setVisibility(View.GONE);
-        }
+        mColorPicker.setVisibility(showColorPalette ? View.VISIBLE : View.GONE);
     }
 
     public boolean isColorPaletteVisible() {
-        return mColorPickerNewEvent.getVisibility() == View.VISIBLE ||
-                mColorPickerExistingEvent.getVisibility() == View.VISIBLE;
+        return mColorPicker.getVisibility() == View.VISIBLE;
     }
 
     @Override
@@ -1536,26 +1498,11 @@
             return;
         }
 
-        int idColumn = c.getColumnIndexOrThrow(Calendars._ID);
-        long calendarId = c.getLong(idColumn);
-        int colorColumn = c.getColumnIndexOrThrow(Calendars.CALENDAR_COLOR);
-        int calendarColor = c.getInt(colorColumn);
-        int displayCalendarColor = Utils.getDisplayColorFromColor(mActivity, calendarColor);
-
-        // Prevents resetting of data (reminders, etc.) on orientation change.
-        if (calendarId == mModel.mCalendarId && mModel.isCalendarColorInitialized() &&
-                displayCalendarColor == mModel.getCalendarColor()) {
-            return;
-        }
-
+        mModel.mCalendarId = c.getLong(EditEventHelper.CALENDARS_INDEX_ID);
+        EditEventHelper.setModelFromCalendarCursor(mModel, c, mActivity);
         // ensure model is up to date so that reminders don't get lost on calendar change
         fillModelFromUI();
 
-        mModel.mCalendarId = calendarId;
-        mModel.setCalendarColor(displayCalendarColor);
-        mModel.mCalendarAccountName = c.getString(EditEventHelper.CALENDARS_INDEX_ACCOUNT_NAME);
-        mModel.mCalendarAccountType = c.getString(EditEventHelper.CALENDARS_INDEX_ACCOUNT_TYPE);
-
         // try to find the event color in the new calendar, remove it otherwise
         if (mModel.isEventColorInitialized() && mModel.getCalendarEventColors() != null) {
             Arrays.stream(mModel.getCalendarEventColors())
@@ -1569,16 +1516,6 @@
                 ? mModel.getEventColor() : mModel.getCalendarColor());
         setColorPickerButtonStates(mModel.getCalendarEventColors());
 
-        // Update the max/allowed reminders with the new calendar properties.
-        int maxRemindersColumn = c.getColumnIndexOrThrow(Calendars.MAX_REMINDERS);
-        mModel.mCalendarMaxReminders = c.getInt(maxRemindersColumn);
-        int allowedRemindersColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_REMINDERS);
-        mModel.mCalendarAllowedReminders = c.getString(allowedRemindersColumn);
-        int allowedAttendeeTypesColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_ATTENDEE_TYPES);
-        mModel.mCalendarAllowedAttendeeTypes = c.getString(allowedAttendeeTypesColumn);
-        int allowedAvailabilityColumn = c.getColumnIndexOrThrow(Calendars.ALLOWED_AVAILABILITY);
-        mModel.mCalendarAllowedAvailability = c.getString(allowedAvailabilityColumn);
-
         // Update the UI elements.
         mReminderItems.clear();
         LinearLayout reminderLayout =
diff --git a/app/src/main/java/com/android/calendar/persistence/CalendarRepository.kt b/app/src/main/java/com/android/calendar/persistence/CalendarRepository.kt
index 144de8e..7bf153a 100644
--- a/app/src/main/java/com/android/calendar/persistence/CalendarRepository.kt
+++ b/app/src/main/java/com/android/calendar/persistence/CalendarRepository.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 Dominik Schürmann <dominik@schuermann.eu>
+ *  Copyright (c) 2024 The Etar Project, Dominik Schürmann <dominik@schuermann.eu>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -12,7 +12,7 @@
  * GNU General Public License for more details.
  *
  * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
 package com.android.calendar.persistence
@@ -20,212 +20,76 @@
 import android.accounts.Account
 import android.annotation.SuppressLint
 import android.app.Application
-import android.content.ContentUris
-import android.content.ContentValues
-import android.content.Context
 import android.net.Uri
 import android.provider.CalendarContract
-import androidx.lifecycle.LiveData
-import ws.xsoh.etar.R
+import com.android.calendar.datasource.AccountDataSource
+import com.android.calendar.datasource.CalendarDataSource
+import com.android.calendar.datasource.EventDataSource
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOn
 
 
 /**
+ * Source of truth for everything related to Calendars.
+ *
  * Repository as in
  * https://developer.android.com/jetpack/docs/guide#recommended-app-arch
  *
  * TODO:
  * Replace usages of AsyncQueryService in Etar with repositories
  * Currently CalendarRepository is only used for settings
+ *
+ * TODO Move to a proper /repository folder
  */
 @SuppressLint("MissingPermission")
-internal class CalendarRepository(val application: Application) {
-
-    private var contentResolver = application.contentResolver
-
-    private var allCalendars: CalendarLiveData = CalendarLiveData(application.applicationContext)
-
-    fun getCalendarsOrderedByAccount(): LiveData<List<Calendar>> {
-        return allCalendars
-    }
-
-    class CalendarLiveData(val context: Context) : ContentProviderLiveData<List<Calendar>>(context, uri) {
-
-        override fun getContentProviderValue(): List<Calendar> {
-            val calendars: MutableList<Calendar> = mutableListOf()
-
-            context.contentResolver.query(uri, PROJECTION, null, null, CalendarContract.Calendars.ACCOUNT_NAME)?.use {
-                while (it.moveToNext()) {
-                    val id = it.getLong(PROJECTION_INDEX_ID)
-                    val accountName = it.getString(PROJECTION_INDEX_ACCOUNT_NAME)
-                    val accountType = it.getString(PROJECTION_INDEX_ACCOUNT_TYPE)
-                    val name = it.getString(PROJECTION_INDEX_NAME)
-                    val displayName = it.getString(PROJECTION_INDEX_CALENDAR_DISPLAY_NAME)
-                    val color = it.getInt(PROJECTION_INDEX_CALENDAR_COLOR)
-                    val visible = it.getInt(PROJECTION_INDEX_VISIBLE) == 1
-                    val syncEvents = it.getInt(PROJECTION_INDEX_SYNC_EVENTS) == 1
-                    val isPrimary = it.getInt(PROJECTION_INDEX_IS_PRIMARY) == 1
-                    val isLocal = accountType == CalendarContract.ACCOUNT_TYPE_LOCAL
-
-                    calendars.add(Calendar(id, accountName, accountType, name, displayName, color, visible, syncEvents, isPrimary, isLocal))
-                }
-            }
-            return calendars
-        }
-
-        companion object {
-            private val uri = CalendarContract.Calendars.CONTENT_URI
-
-            private val PROJECTION = arrayOf(
-                    CalendarContract.Calendars._ID,
-                    CalendarContract.Calendars.ACCOUNT_NAME,
-                    CalendarContract.Calendars.ACCOUNT_TYPE,
-                    CalendarContract.Calendars.OWNER_ACCOUNT,
-                    CalendarContract.Calendars.NAME,
-                    CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
-                    CalendarContract.Calendars.CALENDAR_COLOR,
-                    CalendarContract.Calendars.VISIBLE,
-                    CalendarContract.Calendars.SYNC_EVENTS,
-                    CalendarContract.Calendars.IS_PRIMARY
-            )
-            const val PROJECTION_INDEX_ID = 0
-            const val PROJECTION_INDEX_ACCOUNT_NAME = 1
-            const val PROJECTION_INDEX_ACCOUNT_TYPE = 2
-            const val PROJECTION_INDEX_OWNER_ACCOUNT = 3
-            const val PROJECTION_INDEX_NAME = 4
-            const val PROJECTION_INDEX_CALENDAR_DISPLAY_NAME = 5
-            const val PROJECTION_INDEX_CALENDAR_COLOR = 6
-            const val PROJECTION_INDEX_VISIBLE = 7
-            const val PROJECTION_INDEX_SYNC_EVENTS = 8
-            const val PROJECTION_INDEX_IS_PRIMARY = 9
-        }
-    }
-
-    private fun buildLocalCalendarContentValues(accountName: String, displayName: String): ContentValues {
-        val internalName = "calendar_local_" + displayName.replace("[^a-zA-Z0-9]".toRegex(), "")
-        return ContentValues().apply {
-            put(CalendarContract.Calendars.ACCOUNT_NAME, accountName)
-            put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
-            put(CalendarContract.Calendars.OWNER_ACCOUNT, accountName)
-            put(CalendarContract.Calendars.NAME, internalName)
-            put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, displayName)
-            put(CalendarContract.Calendars.CALENDAR_COLOR_KEY, DEFAULT_COLOR_KEY)
-            put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_ROOT)
-            put(CalendarContract.Calendars.VISIBLE, 1)
-            put(CalendarContract.Calendars.SYNC_EVENTS, 1)
-            put(CalendarContract.Calendars.IS_PRIMARY, 0)
-            put(CalendarContract.Calendars.CAN_ORGANIZER_RESPOND, 0)
-            put(CalendarContract.Calendars.CAN_MODIFY_TIME_ZONE, 1)
-            // from Android docs: "the device will only process METHOD_DEFAULT and METHOD_ALERT reminders"
-            put(CalendarContract.Calendars.ALLOWED_REMINDERS, CalendarContract.Reminders.METHOD_ALERT.toString())
-            put(CalendarContract.Calendars.ALLOWED_ATTENDEE_TYPES, CalendarContract.Attendees.TYPE_NONE.toString())
-        }
-    }
-
-    private fun buildLocalCalendarColorsContentValues(accountName: String, colorType: Int, colorKey: String, color: Int): ContentValues {
-        return ContentValues().apply {
-            put(CalendarContract.Colors.ACCOUNT_NAME, accountName)
-            put(CalendarContract.Colors.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
-            put(CalendarContract.Colors.COLOR_TYPE, colorType)
-            put(CalendarContract.Colors.COLOR_KEY, colorKey)
-            put(CalendarContract.Colors.COLOR, color)
-        }
-    }
-
-    fun addLocalCalendar(accountName: String, displayName: String): Uri {
-        maybeAddCalendarAndEventColors(accountName)
-
-        val cv = buildLocalCalendarContentValues(accountName, displayName)
-        return contentResolver.insert(asLocalCalendarSyncAdapter(accountName, CalendarContract.Calendars.CONTENT_URI), cv)
-                ?: throw IllegalArgumentException()
-    }
-
-    private fun maybeAddCalendarAndEventColors(accountName: String) {
-        if (areCalendarColorsExisting(accountName)) {
-            return
-        }
-
-        val defaultColors: IntArray = application.resources.getIntArray(R.array.defaultCalendarColors)
-
-        val insertBulk = mutableListOf<ContentValues>()
-        for ((i, color) in defaultColors.withIndex()) {
-            val colorKey = i.toString()
-            val colorCvCalendar = buildLocalCalendarColorsContentValues(accountName, CalendarContract.Colors.TYPE_CALENDAR, colorKey, color)
-            val colorCvEvent = buildLocalCalendarColorsContentValues(accountName, CalendarContract.Colors.TYPE_EVENT, colorKey, color)
-            insertBulk.add(colorCvCalendar)
-            insertBulk.add(colorCvEvent)
-        }
-        contentResolver.bulkInsert(asLocalCalendarSyncAdapter(accountName, CalendarContract.Colors.CONTENT_URI), insertBulk.toTypedArray())
-    }
-
-    private fun areCalendarColorsExisting(accountName: String): Boolean {
-        contentResolver.query(CalendarContract.Colors.CONTENT_URI,
-                null,
-                CalendarContract.Colors.ACCOUNT_NAME + "=? AND " + CalendarContract.Colors.ACCOUNT_TYPE + "=?",
-                arrayOf(accountName, CalendarContract.ACCOUNT_TYPE_LOCAL),
-                null).use {
-            if (it!!.moveToFirst()) {
-                return true
-            }
-        }
-        return false
-    }
+internal class CalendarRepository(val application: Application) : ICalendarRepository {
+    /**
+     * Source of calendar entities
+     */
+    private val calendarDataSource = CalendarDataSource(application)
 
     /**
-     * @return true iff exactly one row is deleted
+     * Source of event entities
      */
-    fun deleteLocalCalendar(accountName: String, id: Long): Boolean {
-        val calUri = ContentUris.withAppendedId(asLocalCalendarSyncAdapter(accountName, CalendarContract.Calendars.CONTENT_URI), id)
-        return contentResolver.delete(calUri, null, null) == 1
-    }
+    private val eventDataSource = EventDataSource(application)
 
-    fun queryAccount(calendarId: Long): Account? {
-        val calendarUri = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarId)
-        contentResolver.query(calendarUri, ACCOUNT_PROJECTION, null, null, null)?.use {
-            if (it.moveToFirst()) {
-                val accountName = it.getString(PROJECTION_ACCOUNT_INDEX_NAME)
-                val accountType = it.getString(PROJECTION_ACCOUNT_INDEX_TYPE)
-                return Account(accountName, accountType)
-            }
-        }
-        return null
-    }
+    /**
+     * Source of account entities
+     */
+    private val accountDataSource = AccountDataSource(application)
 
-    fun queryNumberOfEvents(calendarId: Long): Long? {
-        val args = arrayOf(calendarId.toString())
-        contentResolver.query(CalendarContract.Events.CONTENT_URI, PROJECTION_COUNT_EVENTS, WHERE_COUNT_EVENTS, args, null)?.use {
-            if (it.moveToFirst()) {
-                return it.getLong(PROJECTION_COUNT_EVENTS_INDEX_COUNT)
-            }
-        }
-        return null
-    }
+    override fun getCalendarsOrderedByAccount(): Flow<List<Calendar>> =
+        calendarDataSource.getAllCalendars().flowOn(Dispatchers.IO)
+
+    override fun addLocalCalendar(accountName: String, displayName: String): Uri =
+        calendarDataSource.addLocalCalendar(accountName, displayName)
+
+    override fun deleteLocalCalendar(accountName: String, id: Long): Boolean =
+        calendarDataSource.deleteLocalCalendar(accountName, id)
+
+    override fun queryAccount(calendarId: Long): Account? =
+        accountDataSource.queryAccount(calendarId)
+
+    override fun queryNumberOfEvents(calendarId: Long): Long? =
+        eventDataSource.queryNumberOfEvents(calendarId)
 
     companion object {
-        private val ACCOUNT_PROJECTION = arrayOf(
-                CalendarContract.Calendars.ACCOUNT_NAME,
-                CalendarContract.Calendars.ACCOUNT_TYPE
-        )
-        const val PROJECTION_ACCOUNT_INDEX_NAME = 0
-        const val PROJECTION_ACCOUNT_INDEX_TYPE = 1
-
-        private val PROJECTION_COUNT_EVENTS = arrayOf(
-                CalendarContract.Events._COUNT
-        )
-        const val PROJECTION_COUNT_EVENTS_INDEX_COUNT = 0
-        const val WHERE_COUNT_EVENTS = CalendarContract.Events.CALENDAR_ID + "=?"
-
-        const val DEFAULT_COLOR_KEY = "1"
 
         /**
          * Operations only work if they are made "under" the correct account
+         *
+         * TODO Find a better place for this function
          */
         @JvmStatic
         fun asLocalCalendarSyncAdapter(accountName: String, uri: Uri): Uri {
             return uri.buildUpon()
-                    .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
-                    .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, accountName)
-                    .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL).build()
+                .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
+                .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, accountName)
+                .appendQueryParameter(
+                    CalendarContract.Calendars.ACCOUNT_TYPE,
+                    CalendarContract.ACCOUNT_TYPE_LOCAL
+                ).build()
         }
     }
-
 }
diff --git a/app/src/main/java/com/android/calendar/persistence/ContentProviderLiveData.kt b/app/src/main/java/com/android/calendar/persistence/ContentProviderLiveData.kt
deleted file mode 100644
index 448168f..0000000
--- a/app/src/main/java/com/android/calendar/persistence/ContentProviderLiveData.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2020 Dominik Schürmann <dominik@schuermann.eu>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-package com.android.calendar.persistence
-
-import android.content.Context
-import android.database.ContentObserver
-import android.net.Uri
-import androidx.lifecycle.MutableLiveData
-import com.android.calendar.Utils
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.async
-import kotlinx.coroutines.launch
-
-/**
- * Based on https://medium.com/@jmcassis/android-livedata-and-content-provider-updates-5f8fd3b2b3a4
- *
- * Abstract [LiveData] to observe Android's Content Provider changes.
- * Provide a [uri] to observe changes and implement [getContentProviderValue]
- * to provide data to post when content provider notifies a change.
- */
-abstract class ContentProviderLiveData<T>(
-        private val context: Context,
-        private val uri: Uri
-) : MutableLiveData<T>() {
-
-    private var observer = object : ContentObserver(null) {
-        override fun onChange(self: Boolean) {
-            // Notify LiveData listeners that data at the uri has changed
-            getContentProviderValueAsync()
-        }
-    }
-
-    override fun onActive() {
-        if (Utils.isCalendarPermissionGranted(context, true)) {
-            context.contentResolver.registerContentObserver(uri, true, observer)
-            getContentProviderValueAsync()
-        }
-    }
-
-    override fun onInactive() {
-        context.contentResolver.unregisterContentObserver(observer)
-    }
-
-    private fun getContentProviderValueAsync() {
-        GlobalScope.launch(Dispatchers.Main) {
-            val accounts = async {
-                getContentProviderValue()
-            }
-
-            postValue(accounts.await())
-        }
-    }
-
-    /**
-     * Implement if you need to provide [T] value to be posted
-     * when observed content is changed.
-     */
-    abstract fun getContentProviderValue(): T
-}
diff --git a/app/src/main/java/com/android/calendar/persistence/ICalendarRepository.kt b/app/src/main/java/com/android/calendar/persistence/ICalendarRepository.kt
new file mode 100644
index 0000000..cb009b7
--- /dev/null
+++ b/app/src/main/java/com/android/calendar/persistence/ICalendarRepository.kt
@@ -0,0 +1,82 @@
+/*
+ *  Copyright (c) 2024 The Etar Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.android.calendar.persistence
+
+import android.accounts.Account
+import android.app.Application
+import android.net.Uri
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Interface for a Source of truth of all things Calendars.
+ *
+ * TODO Move to proper /repository folder
+ */
+interface ICalendarRepository {
+
+    /**
+     * Get flow of calendars order by their account.
+     *
+     * Automatically updates when collected, disconnects afterwards.
+     *
+     * TODO Better documentation, idk if this actually orders or not.
+     */
+    fun getCalendarsOrderedByAccount(): Flow<List<Calendar>>
+
+    /**
+     * TODO document
+     */
+    fun addLocalCalendar(accountName: String, displayName: String): Uri
+
+    /**
+     * TODO document better
+     *
+     * @return true iff exactly one row is deleted
+     */
+    fun deleteLocalCalendar(accountName: String, id: Long): Boolean
+
+    /**
+     * Query the owning account of a given calendar
+     */
+    fun queryAccount(calendarId: Long): Account?
+
+    /**
+     * TODO document
+     */
+    fun queryNumberOfEvents(calendarId: Long): Long?
+
+    companion object {
+        /**
+         * Static repository holder.
+         *
+         * We hold this, since repositories are meant to be singletons.
+         */
+        private var static: ICalendarRepository? = null
+
+        /**
+         * TODO Replace with proper dependency injection
+         */
+        fun get(application: Application): ICalendarRepository {
+            if (static == null) {
+                static = CalendarRepository(application)
+            }
+
+            return static!!
+        }
+    }
+}
diff --git a/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt b/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt
index 4743d60..a5f71eb 100644
--- a/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt
+++ b/app/src/main/java/com/android/calendar/settings/CalendarPreferences.kt
@@ -36,20 +36,20 @@
 import androidx.preference.SwitchPreference
 import com.android.calendar.Utils
 import com.android.calendar.alerts.channelId
-import com.android.calendar.persistence.CalendarRepository
+import com.android.calendar.persistence.ICalendarRepository
 import ws.xsoh.etar.R
 
 
 class CalendarPreferences : PreferenceFragmentCompat() {
 
     private var calendarId: Long = -1
-    private lateinit var calendarRepository: CalendarRepository
+    private lateinit var calendarRepository: ICalendarRepository
     private lateinit var account: Account
     private var numberOfEvents: Long = -1
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
         calendarId = requireArguments().getLong(ARG_CALENDAR_ID)
-        calendarRepository = CalendarRepository(requireActivity().application)
+        calendarRepository = ICalendarRepository.get(requireActivity().application)
         account = calendarRepository.queryAccount(calendarId)!!
         numberOfEvents = calendarRepository.queryNumberOfEvents(calendarId)!!
 
diff --git a/app/src/main/java/com/android/calendar/settings/MainListViewModel.kt b/app/src/main/java/com/android/calendar/settings/MainListViewModel.kt
index b44161c..93d8ca2 100644
--- a/app/src/main/java/com/android/calendar/settings/MainListViewModel.kt
+++ b/app/src/main/java/com/android/calendar/settings/MainListViewModel.kt
@@ -20,13 +20,14 @@
 import android.app.Application
 import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.LiveData
+import androidx.lifecycle.asLiveData
 import com.android.calendar.persistence.Calendar
-import com.android.calendar.persistence.CalendarRepository
+import com.android.calendar.persistence.ICalendarRepository
 
 
 class MainListViewModel(application: Application) : AndroidViewModel(application) {
 
-    private var repository: CalendarRepository = CalendarRepository(application)
+    private var repository = ICalendarRepository.get(application)
 
     // Using LiveData and caching what fetchCalendarsByAccountName returns has several benefits:
     // - We can put an observer on the data (instead of polling for changes) and only update the
@@ -39,7 +40,7 @@
     }
 
     private fun loadCalendars() {
-        allCalendars = repository.getCalendarsOrderedByAccount()
+        allCalendars = repository.getCalendarsOrderedByAccount().asLiveData()
     }
 
     fun getCalendarsOrderedByAccount(): LiveData<List<Calendar>> {
diff --git a/app/src/main/res/color/calendar_spinner_item_colors.xml b/app/src/main/res/color/calendar_spinner_item_colors.xml
new file mode 100644
index 0000000..ae66cac
--- /dev/null
+++ b/app/src/main/res/color/calendar_spinner_item_colors.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/calendarSpinnerDisabledTextColor" android:state_enabled="false" />
+    <item android:color="?android:attr/textColorPrimary" />
+</selector>
diff --git a/app/src/main/res/layout/calendars_spinner_item.xml b/app/src/main/res/layout/calendars_spinner_item.xml
index 2467a80..0277926 100644
--- a/app/src/main/res/layout/calendars_spinner_item.xml
+++ b/app/src/main/res/layout/calendars_spinner_item.xml
@@ -15,27 +15,28 @@
 -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
+    android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_width="match_parent">
+    android:orientation="vertical">
 
-    <TextView android:id="@+id/calendar_name"
-        style="?android:attr/spinnerItemStyle"
+    <TextView
+        android:id="@+id/calendar_name"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:singleLine="true"
+        android:duplicateParentState="true"
         android:ellipsize="end"
-        android:textColor="?attr/light_dark"
+        android:singleLine="true"
+        android:textColor="@color/calendar_spinner_item_colors"
         android:textSize="18sp" />
 
     <TextView
         android:id="@+id/account_name"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
+        android:duplicateParentState="true"
         android:ellipsize="marquee"
         android:singleLine="true"
-        style="?android:attr/spinnerItemStyle"
-        android:textColor="?attr/light_dark"
+        android:textColor="@color/calendar_spinner_item_colors"
         android:textSize="14sp" />
 
 </LinearLayout>
diff --git a/app/src/main/res/layout/edit_event_all.xml b/app/src/main/res/layout/edit_event_all.xml
index 37fd425..db2cf21 100644
--- a/app/src/main/res/layout/edit_event_all.xml
+++ b/app/src/main/res/layout/edit_event_all.xml
@@ -26,19 +26,6 @@
         android:layout_height="wrap_content"
         android:gravity="center_vertical">
 
-        <!-- CALENDAR DISPLAY for existing events -->
-        <androidx.constraintlayout.widget.Group
-            android:id="@+id/calendar_group"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            app:constraint_referenced_ids="calendar_textview,calendar_textview_secondary,change_color_new_event" />
-
-        <androidx.constraintlayout.widget.Group
-            android:id="@+id/calendar_selector_group"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            app:constraint_referenced_ids="calendars_spinner,change_color_new_event" />
-
         <androidx.constraintlayout.widget.Group
             android:id="@+id/timezone_button_row"
             android:layout_width="wrap_content"
@@ -93,61 +80,34 @@
 
         <Spinner
             android:id="@+id/calendars_spinner"
-            android:layout_width="0dp"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginStart="4dp"
+            android:layout_marginStart="12dp"
+            android:layout_marginEnd="32dp"
             android:gravity="center_vertical"
-            android:prompt="@string/edit_event_calendar_label"
             android:paddingStart="0dp"
             android:paddingEnd="2dp"
+            android:prompt="@string/edit_event_calendar_label"
             app:layout_constraintBottom_toBottomOf="@+id/calendar_selector_group_icon"
+            app:layout_constraintEnd_toStartOf="@+id/change_color"
+            app:layout_constraintHorizontal_bias="0.0"
             app:layout_constraintStart_toEndOf="@+id/calendar_selector_group_icon"
             app:layout_constraintTop_toTopOf="@+id/calendar_selector_group_icon" />
 
-        <TextView
-            android:id="@+id/calendar_textview"
-            style="@style/TextAppearance.EditEvent"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="12dp"
-            android:layout_marginTop="8dp"
-            android:textColor="?attr/light_dark"
-            app:layout_constraintStart_toEndOf="@+id/calendar_selector_group_icon"
-            app:layout_constraintTop_toBottomOf="@id/view"
-            tools:layout_conversion_absoluteHeight="24dp"
-            tools:layout_conversion_absoluteWidth="363dp" />
-
-        <TextView
-            android:id="@+id/calendar_textview_secondary"
-            style="@style/TextAppearance.EditEvent"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="12dp"
-            android:layout_marginTop="2dp"
-            android:layout_marginBottom="4dp"
-            android:textColor="?attr/light_dark"
-            android:textSize="14sp"
-            app:layout_constraintBottom_toTopOf="@+id/view1"
-            app:layout_constraintStart_toEndOf="@+id/calendar_selector_group_icon"
-            app:layout_constraintTop_toBottomOf="@+id/calendar_textview"
-            tools:layout_conversion_absoluteHeight="19dp"
-            tools:layout_conversion_absoluteWidth="363dp" />
-
         <ImageButton
-            android:id="@+id/change_color_new_event"
-            android:layout_width="0dp"
+            android:id="@+id/change_color"
+            android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginEnd="24dp"
-            android:layout_marginTop="20dp"
-            android:background="?android:attr/selectableItemBackground"
+            android:layout_marginEnd="32dp"
+            android:background="?android:attr/selectableItemBackgroundBorderless"
             android:contentDescription="@string/choose_event_color_label"
             android:enabled="false"
             android:scaleType="center"
             android:src="@drawable/ic_colorpicker"
-            android:visibility="invisible"
-            app:layout_constraintBottom_toBottomOf="@+id/calendar_selector_group_icon"
+            app:layout_constraintBottom_toBottomOf="@+id/calendars_spinner"
             app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintTop_toTopOf="@+id/view" />
+            app:layout_constraintTop_toTopOf="@+id/calendars_spinner"
+            tools:visibility="visible" />
 
         <View
             android:id="@+id/view1"
diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml
index 738ce68..f9bf1a0 100644
--- a/app/src/main/res/values-cy/strings.xml
+++ b/app/src/main/res/values-cy/strings.xml
@@ -414,5 +414,6 @@
     <string name="preferences_menu_about">Ynghylch Calendr</string>
     <string name="offline_account_name">Calendr All-lein</string>
     <string name="after_start_of_event">ar ôl dechrau digwyddiad</string>
+    <string name="discard_event_changes"/>
     <string name="cancel">Diddymu</string>
 </resources>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 51e0415..64fba22 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -352,5 +352,7 @@
     <string name="preferences_do_not_check_battery_optimization">Akku-Optimierung nicht überprüfen</string>
     <string name="preferences_do_not_check_battery_optimization_summary">Verwendung auf eigene Gefahr! Benachrichtigungen werden nicht mehr funktionieren, Widgets werden nicht mehr aktualisiert und Hintergrunddienste werden abstürzen. Aber du wirst nicht mehr gefragt werden, ob du den Hintergrunddienst aktivieren möchtest.</string>
     <string name="calendars">Kalender</string>
+    <string name="discard_event_changes">Änderungen verwerfen?</string>
+    <string name="discard">Verwerfen</string>
     <string name="cancel">Abbrechen</string>
 </resources>
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index e6faec0..b4a313f 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -341,5 +341,7 @@
     <string name="preferences_list_add_offline_cancel">Peruuta</string>
     <string name="preferences_menu_about">Tietoja kalenterista</string>
     <string name="offline_account_name">Offline-kalenteri</string>
+    <string name="discard_event_changes">Haluatko hylätä muutokset?</string>
+    <string name="discard">Hylkää</string>
     <string name="cancel">Peruuta</string>
 </resources>
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 5f35b40..7d79ab2 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -352,5 +352,7 @@
     <string name="preferences_do_not_check_battery_optimization">Ignora l\'ottimizzazione della batteria</string>
     <string name="preferences_do_not_check_battery_optimization_summary">Usa questa funzione a tuo rischio! Le notifiche non funzioneranno più, il widget non si aggiornerà e i servizi in background si bloccheranno. Non ti verrà chiesto di nuovo se vuoi abilitare il servizio in background.</string>
     <string name="calendars">Calendari</string>
+    <string name="discard_event_changes">Vuoi scartare le modifiche?</string>
+    <string name="discard">Annulla</string>
     <string name="cancel">Annulla</string>
 </resources>
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 71d3cb2..0aa2e95 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -112,7 +112,7 @@
     <string name="email_picker_label">Enviar e-mail para</string>
     <string name="event_not_found">"Evento não encontrado."</string>
     <string name="map_label">Mapa</string>
-    <string name="no_map">Não há aplicativos de mapas instalados</string>
+    <string name="no_map">Não há apps de mapas instalados</string>
     <string name="call_label">Ligar</string>
     <string name="quick_response_settings">Respostas rápidas</string>
     <string name="quick_response_settings_summary">Editar respostas padrão quando enviar e-mails para os convidados</string>
diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml
index 0e2cee5..47287ac 100644
--- a/app/src/main/res/values-sq/strings.xml
+++ b/app/src/main/res/values-sq/strings.xml
@@ -352,5 +352,7 @@
     <string name="preferences_do_not_check_battery_optimization">Mos e kontrolloni optimizimin e baterisë</string>
     <string name="preferences_do_not_check_battery_optimization_summary">Përdoreni në rrezikun tuaj! Njoftimet nuk do të funksionojnë më, miniaplikacioni nuk do të përditësohet më dhe shërbimet e sfondit do të prishen. Por nuk do të pyeteni përsëri nëse dëshironi të aktivizoni shërbimin e sfondit.</string>
     <string name="calendars">Kalendarët</string>
+    <string name="discard_event_changes">Dëshiron të refuzosh ndryshimet?</string>
+    <string name="discard">Injoro</string>
     <string name="cancel">Anullo</string>
 </resources>
diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml
index dbdce3d..eb7e8b1 100644
--- a/app/src/main/res/values-v31/themes.xml
+++ b/app/src/main/res/values-v31/themes.xml
@@ -40,6 +40,7 @@
         <item name="iconSettingsGeneral">@android:color/black</item>
         <item name="iconAddCalendar">@android:color/black</item>
         <item name="iconCalendarAccount">@android:color/black</item>
+       <item name="calendarSpinnerDisabledTextColor">@color/calendar_item_disabled</item>
     </style>
     <style name="CalendarAppThemeDarkMonet" parent="Theme.AppCompat.NoActionBar">
         <item name="colorPrimary">@android:color/system_accent1_500</item>
@@ -80,6 +81,7 @@
         <item name="iconSettingsGeneral">@android:color/white</item>
         <item name="iconAddCalendar">@android:color/white</item>
         <item name="iconCalendarAccount">@android:color/white</item>
+        <item name="calendarSpinnerDisabledTextColor">@color/calendar_item_disabled_dark</item>
     </style>
     <style name="CalendarAppThemeBlackMonet" parent="Theme.AppCompat.NoActionBar">
         <item name="colorPrimary">@android:color/system_accent1_500</item>
@@ -120,5 +122,6 @@
         <item name="iconSettingsGeneral">@android:color/white</item>
         <item name="iconAddCalendar">@android:color/white</item>
         <item name="iconCalendarAccount">@android:color/white</item>
+        <item name="calendarSpinnerDisabledTextColor">@color/calendar_item_disabled_black</item>
     </style>
 </resources>
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index f27d5ff..3ce51df 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -49,5 +49,6 @@
         <attr name="iconAddCalendar" format="reference" />
         <attr name="iconCalendarAccount" format="reference" />
         <attr name="divider_select_calendars" format="color"/>
+        <attr name="calendarSpinnerDisabledTextColor" format="color" />
     </declare-styleable>
 </resources>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index c37403b..4b4952b 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -109,6 +109,7 @@
     <color name="calendar_hidden">#ff808080</color>
     <color name="calendar_secondary_visible">#ff323232</color>
     <color name="calendar_secondary_hidden">#ff808080</color>
+    <color name="calendar_item_disabled">#61000000</color>
 
     <!-- Text color of the date in the Calendar widget -->
 
@@ -294,6 +295,7 @@
     <color name="calendar_hidden_black">#757575</color>
     <color name="calendar_secondary_visible_black">#ffffff</color>
     <color name="calendar_secondary_hidden_black">#757575</color>
+    <color name="calendar_item_disabled_black">#61ffffff</color>
 
     <!-- timezone_GMT_background_color -->
     <color name="calendar_date_banner_background_black">#212121</color>
@@ -399,6 +401,7 @@
     <color name="calendar_hidden_dark">#757575</color>
     <color name="calendar_secondary_visible_dark">#ffffff</color>
     <color name="calendar_secondary_hidden_dark">#757575</color>
+    <color name="calendar_item_disabled_dark">#61ffffff</color>
 
     <!-- timezone_GMT_background_color -->
     <color name="calendar_date_banner_background_dark">#212121</color>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 6eca518..a88456b 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -40,6 +40,7 @@
         <item name="iconAddCalendar">@android:color/black</item>
         <item name="iconCalendarAccount">@android:color/black</item>
         <item name="divider_select_calendars">@color/divider_select_calendars</item>
+        <item name="calendarSpinnerDisabledTextColor">@color/calendar_item_disabled</item>
     </style>
 
     <style name="CalendarAppThemeDark" parent="Theme.AppCompat.NoActionBar">
@@ -82,6 +83,7 @@
         <item name="iconAddCalendar">@android:color/white</item>
         <item name="iconCalendarAccount">@android:color/white</item>
         <item name="divider_select_calendars">@color/divider_select_calendars_dark</item>
+        <item name="calendarSpinnerDisabledTextColor">@color/calendar_item_disabled_dark</item>
     </style>
 
     <style name="CalendarAppThemeBlack" parent="Theme.AppCompat.NoActionBar">
@@ -124,6 +126,7 @@
         <item name="iconAddCalendar">@android:color/white</item>
         <item name="iconCalendarAccount">@android:color/white</item>
         <item name="divider_select_calendars">@color/divider_select_calendars_black</item>
+        <item name="calendarSpinnerDisabledTextColor">@color/calendar_item_disabled_black</item>
     </style>
 
 
diff --git a/metadata/de-DE/changelogs/46.txt b/metadata/de-DE/changelogs/46.txt
new file mode 100644
index 0000000..dcb10e0
--- /dev/null
+++ b/metadata/de-DE/changelogs/46.txt
@@ -0,0 +1,3 @@
+- Kalender eines bestehenden Termins ist nun änderbar
+- Fehler in der Gehe zu Funktion behoben
+- Fehlerbehebungen
diff --git a/metadata/en-US/changelogs/46.txt b/metadata/en-US/changelogs/46.txt
new file mode 100644
index 0000000..da765fe
--- /dev/null
+++ b/metadata/en-US/changelogs/46.txt
@@ -0,0 +1,3 @@
+- Allow editing calendar on existing events
+- Fix go-to date function
+- Small bugfixes