diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b20a171..72977e7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -90,6 +90,8 @@ android {
buildFeatures {
compose = true
+ // BuildConfig.DEBUG gates the in-app debug ribbon (see DebugRibbon).
+ buildConfig = true
}
// Don't embed AGP's dependency-metadata block in the APK signing block. It's
diff --git a/app/src/debug/res/values/colors.xml b/app/src/debug/res/values/colors.xml
new file mode 100644
index 0000000..87de5a9
--- /dev/null
+++ b/app/src/debug/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+
+ #FFB23B00
+
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
new file mode 100644
index 0000000..5dad449
--- /dev/null
+++ b/app/src/debug/res/values/strings.xml
@@ -0,0 +1,10 @@
+
+
+
+ Calendula Debug
+
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt
index a2de42c..ede492a 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt
@@ -8,6 +8,7 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -29,6 +30,7 @@ import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
import de.jeanlucmakiola.calendula.ui.WidgetNavRequest
import de.jeanlucmakiola.calendula.ui.common.CalendarView
+import de.jeanlucmakiola.calendula.ui.common.DebugRibbon
import de.jeanlucmakiola.calendula.ui.crash.CrashReportActivity
import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog
import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport
@@ -101,19 +103,24 @@ class MainActivity : AppCompatActivity() {
darkTheme = darkTheme,
dynamicColor = settings.dynamicColor,
) {
- CompositionLocalProvider(
- LocalUse24HourFormat provides use24Hour,
- LocalShowHourLines provides settings.showHourLines,
- ) {
- RootScreen(
- modifier = Modifier.fillMaxSize(),
- requestedDetailKey = requestedDetailKey,
- onDetailKeyConsumed = { requestedDetailKey = null },
- widgetNavRequest = requestedNav,
- onWidgetNavConsumed = { requestedNav = null },
- requestedImportUri = requestedImportUri,
- onImportConsumed = { requestedImportUri = null },
- )
+ Box(modifier = Modifier.fillMaxSize()) {
+ CompositionLocalProvider(
+ LocalUse24HourFormat provides use24Hour,
+ LocalShowHourLines provides settings.showHourLines,
+ ) {
+ RootScreen(
+ modifier = Modifier.fillMaxSize(),
+ requestedDetailKey = requestedDetailKey,
+ onDetailKeyConsumed = { requestedDetailKey = null },
+ widgetNavRequest = requestedNav,
+ onWidgetNavConsumed = { requestedNav = null },
+ requestedImportUri = requestedImportUri,
+ onImportConsumed = { requestedImportUri = null },
+ )
+ }
+ // A persistent corner marker so a debug build is never
+ // mistaken for the production app; compiled out of release.
+ if (BuildConfig.DEBUG) DebugRibbon()
}
pendingCrashReport?.let { report ->
CrashReportDialog(
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt
index 0f50930..2c3928c 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt
@@ -118,8 +118,14 @@ internal fun buildEventUpdateValues(
* provider clone the series row and apply these on top. Unlike the series
* update there is no dirty check — the exception is a fresh row, so every
* form-backed column is written (empty optionals as explicit NULLs, since the
- * clone starts from the parent's values). An exception is a single event:
- * DTEND, never RRULE/DURATION.
+ * clone starts from the parent's values).
+ *
+ * The occurrence's length travels as DURATION, never DTEND: the provider
+ * rejects DTEND on an exception outright (`CalendarProvider2`:
+ * "Exceptions can't overwrite dtend") and derives the instance end from
+ * DTSTART + DURATION itself, clearing the inherited RRULE in the process. This
+ * matches how AOSP Calendar/Etar write exceptions; sending DTEND is what made
+ * "only this event" fail on-device (Codeberg #16).
*/
internal fun buildOccurrenceExceptionValues(
form: EventForm,
@@ -131,7 +137,7 @@ internal fun buildOccurrenceExceptionValues(
put(CalendarContract.Events.TITLE, form.title.trim())
put(CalendarContract.Events.ALL_DAY, if (form.isAllDay) 1 else 0)
put(CalendarContract.Events.DTSTART, times.dtStartMillis)
- put(CalendarContract.Events.DTEND, times.dtEndMillis)
+ put(CalendarContract.Events.DURATION, times.toRfc2445Duration(form.isAllDay))
put(CalendarContract.Events.EVENT_TIMEZONE, times.timezone)
put(CalendarContract.Events.AVAILABILITY, form.availability.toProviderValue())
put(CalendarContract.Events.ACCESS_LEVEL, form.accessLevel.toProviderValue())
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DebugRibbon.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DebugRibbon.kt
new file mode 100644
index 0000000..3ca1381
--- /dev/null
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DebugRibbon.kt
@@ -0,0 +1,50 @@
+package de.jeanlucmakiola.calendula.ui.common
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
+
+/**
+ * A Flutter-style "DEBUG" corner ribbon, drawn across the top-right corner of
+ * the app. Deliberately a stark, un-themed marker (not a product component) so a
+ * debug build is unmistakable at a glance — gate it on `BuildConfig.DEBUG` at the
+ * call site so it never reaches a release build. Non-interactive: it's a plain
+ * label with no pointer handler, so taps fall through to whatever is beneath it.
+ *
+ * Drop it in as the last child of a full-screen [androidx.compose.foundation.layout.Box]
+ * so it overlays the UI.
+ */
+@Composable
+fun BoxScope.DebugRibbon() {
+ Text(
+ text = "DEBUG",
+ color = Color.White,
+ fontSize = 10.sp,
+ fontWeight = FontWeight.Bold,
+ letterSpacing = 1.sp,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .align(Alignment.TopEnd)
+ .zIndex(1f)
+ // Push the band out so its midline crosses the very corner, then
+ // rotate it to the classic 45° ribbon.
+ .offset(x = 36.dp, y = 24.dp)
+ .rotate(45f)
+ .background(Color(0xFFB23B00))
+ .width(140.dp)
+ .padding(vertical = 2.dp),
+ )
+}
diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapperTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapperTest.kt
index 40aac33..f1be42b 100644
--- a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapperTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapperTest.kt
@@ -199,11 +199,12 @@ class EventWriteMapperTest {
assertThat(values[CalendarContract.Events.TITLE]).isEqualTo("Moved")
assertThat(values[CalendarContract.Events.EVENT_LOCATION]).isEqualTo("Berlin")
assertThat(values[CalendarContract.Events.DTSTART]).isEqualTo(1_781_164_800_000L)
- assertThat(values[CalendarContract.Events.DTEND])
- .isEqualTo(1_781_164_800_000L + 5_400_000L)
- // A single occurrence never carries its own rule.
+ // The occurrence's length travels as DURATION, never DTEND — the provider
+ // rejects DTEND on an exception ("Exceptions can't overwrite dtend") and
+ // derives the end from DTSTART + DURATION, clearing the inherited RRULE.
+ assertThat(values[CalendarContract.Events.DURATION]).isEqualTo("P5400S")
+ assertThat(values).doesNotContainKey(CalendarContract.Events.DTEND)
assertThat(values).doesNotContainKey(CalendarContract.Events.RRULE)
- assertThat(values).doesNotContainKey(CalendarContract.Events.DURATION)
}
@Test