Compare commits
1 Commits
feat/inser
...
renovate/m
| Author | SHA1 | Date | |
|---|---|---|---|
| 1770c4ae42 |
@@ -112,17 +112,6 @@
|
|||||||
<data android:mimeType="text/calendar" />
|
<data android:mimeType="text/calendar" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Let another app or widget (e.g. the Todo Agenda widget) launch us
|
|
||||||
to create a new event, the way the AOSP calendar accepts it:
|
|
||||||
ACTION_INSERT on the events dir mime type, carrying the new
|
|
||||||
event's fields as CalendarContract extras
|
|
||||||
(MainActivity.insertFormOrNull, issue #30). -->
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.INSERT" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:mimeType="vnd.android.cursor.dir/event" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<!-- Launcher long-press shortcuts (e.g. "New event"). -->
|
<!-- Launcher long-press shortcuts (e.g. "New event"). -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.CalendarContract
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@@ -26,8 +25,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import de.jeanlucmakiola.calendula.data.crash.CrashReporter
|
import de.jeanlucmakiola.calendula.data.crash.CrashReporter
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
|
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
|
||||||
import de.jeanlucmakiola.calendula.domain.EventForm
|
|
||||||
import de.jeanlucmakiola.calendula.domain.buildInsertEventForm
|
|
||||||
import de.jeanlucmakiola.calendula.ui.RootScreen
|
import de.jeanlucmakiola.calendula.ui.RootScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
@@ -45,7 +42,6 @@ import de.jeanlucmakiola.calendula.ui.theme.resolveFontFamily
|
|||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import kotlin.time.Clock
|
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -64,11 +60,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// by CalendarHost's import flow.
|
// by CalendarHost's import flow.
|
||||||
private var requestedImportUri by mutableStateOf<Uri?>(null)
|
private var requestedImportUri by mutableStateOf<Uri?>(null)
|
||||||
|
|
||||||
// A prefilled new-event form from an external ACTION_INSERT launch (another
|
|
||||||
// app/widget asking us to create an event, issue #30). Consumed once by
|
|
||||||
// CalendarHost, which opens it in the create form for review.
|
|
||||||
private var requestedInsertForm by mutableStateOf<EventForm?>(null)
|
|
||||||
|
|
||||||
// A captured crash report awaiting the user's decision, surfaced as a dialog
|
// A captured crash report awaiting the user's decision, surfaced as a dialog
|
||||||
// over the calendar on the next launch (the single-crash path). A startup
|
// over the calendar on the next launch (the single-crash path). A startup
|
||||||
// crash-loop is handled out of band, before setContent — see below.
|
// crash-loop is handled out of band, before setContent — see below.
|
||||||
@@ -93,7 +84,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
requestedDetailKey = intent.detailKeyOrNull()
|
requestedDetailKey = intent.detailKeyOrNull()
|
||||||
requestedNav = intent.navRequestOrNull()
|
requestedNav = intent.navRequestOrNull()
|
||||||
requestedImportUri = intent.importUriOrNull()
|
requestedImportUri = intent.importUriOrNull()
|
||||||
requestedInsertForm = intent.insertFormOrNull()
|
|
||||||
if (CrashReporter.shouldPrompt(this)) pendingCrashReport = CrashReporter.pendingReport(this)
|
if (CrashReporter.shouldPrompt(this)) pendingCrashReport = CrashReporter.pendingReport(this)
|
||||||
setContent {
|
setContent {
|
||||||
// One activity-scoped SettingsViewModel drives both the theme here
|
// One activity-scoped SettingsViewModel drives both the theme here
|
||||||
@@ -142,8 +132,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
onWidgetNavConsumed = { requestedNav = null },
|
onWidgetNavConsumed = { requestedNav = null },
|
||||||
requestedImportUri = requestedImportUri,
|
requestedImportUri = requestedImportUri,
|
||||||
onImportConsumed = { requestedImportUri = null },
|
onImportConsumed = { requestedImportUri = null },
|
||||||
requestedInsertForm = requestedInsertForm,
|
|
||||||
onInsertConsumed = { requestedInsertForm = null },
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// A persistent corner marker so a debug build is never
|
// A persistent corner marker so a debug build is never
|
||||||
@@ -181,7 +169,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
intent.detailKeyOrNull()?.let { requestedDetailKey = it }
|
intent.detailKeyOrNull()?.let { requestedDetailKey = it }
|
||||||
intent.navRequestOrNull()?.let { requestedNav = it }
|
intent.navRequestOrNull()?.let { requestedNav = it }
|
||||||
intent.importUriOrNull()?.let { requestedImportUri = it }
|
intent.importUriOrNull()?.let { requestedImportUri = it }
|
||||||
intent.insertFormOrNull()?.let { requestedInsertForm = it }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,31 +188,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return uri.takeIf { it.scheme == "content" || it.scheme == "file" }
|
return uri.takeIf { it.scheme == "content" || it.scheme == "file" }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A prefilled new-event form from an external `ACTION_INSERT` launch — another
|
|
||||||
* app or widget (e.g. Todo Agenda) asking us to create an event (issue #30).
|
|
||||||
* The new event's fields ride as CalendarContract extras; anything omitted
|
|
||||||
* falls back to the in-app "new event" defaults in [buildInsertEventForm].
|
|
||||||
*/
|
|
||||||
private fun Intent.insertFormOrNull(): EventForm? {
|
|
||||||
if (action != Intent.ACTION_INSERT) return null
|
|
||||||
return buildInsertEventForm(
|
|
||||||
beginMillis = longExtraOrNull(CalendarContract.EXTRA_EVENT_BEGIN_TIME),
|
|
||||||
endMillis = longExtraOrNull(CalendarContract.EXTRA_EVENT_END_TIME),
|
|
||||||
isAllDay = getBooleanExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, false),
|
|
||||||
title = getStringExtra(CalendarContract.Events.TITLE),
|
|
||||||
description = getStringExtra(CalendarContract.Events.DESCRIPTION),
|
|
||||||
location = getStringExtra(CalendarContract.Events.EVENT_LOCATION),
|
|
||||||
rrule = getStringExtra(CalendarContract.Events.RRULE),
|
|
||||||
zone = TimeZone.currentSystemDefault(),
|
|
||||||
now = Clock.System.now(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A Long extra's value, or null when the extra is absent. */
|
|
||||||
private fun Intent.longExtraOrNull(key: String): Long? =
|
|
||||||
if (hasExtra(key)) getLongExtra(key, 0L) else null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date a launcher/clock date tap points at, parsed from the AOSP calendar
|
* The date a launcher/clock date tap points at, parsed from the AOSP calendar
|
||||||
* "view time" intent: ACTION_VIEW on `content://com.android.calendar/time/
|
* "view time" intent: ACTION_VIEW on `content://com.android.calendar/time/
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
package de.jeanlucmakiola.calendula.domain
|
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
|
||||||
import kotlinx.datetime.LocalTime
|
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.datetime.toInstant
|
|
||||||
import kotlinx.datetime.toLocalDateTime
|
|
||||||
import kotlin.time.Duration.Companion.hours
|
|
||||||
import kotlin.time.Instant
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a prefilled [EventForm] from an `ACTION_INSERT` intent's extras (issue
|
|
||||||
* #30). External apps and widgets (e.g. the Todo Agenda widget) launch the
|
|
||||||
* calendar this way to create a new event, passing the fields as
|
|
||||||
* [android.provider.CalendarContract] extras. Any field the intent omits falls
|
|
||||||
* back to the same defaults the in-app "new event" uses — a timed start at the
|
|
||||||
* next full hour and a one-hour duration. [EventForm.calendarId] is left null so
|
|
||||||
* it resolves to the last-used / first-writable calendar, exactly like the
|
|
||||||
* `.ics` single-event and plain new-event paths.
|
|
||||||
*
|
|
||||||
* Pure (no Android types) so it is unit-testable; the intent parsing that reads
|
|
||||||
* the extras lives in `MainActivity.insertFormOrNull`.
|
|
||||||
*/
|
|
||||||
fun buildInsertEventForm(
|
|
||||||
beginMillis: Long?,
|
|
||||||
endMillis: Long?,
|
|
||||||
isAllDay: Boolean,
|
|
||||||
title: String?,
|
|
||||||
description: String?,
|
|
||||||
location: String?,
|
|
||||||
rrule: String?,
|
|
||||||
zone: TimeZone,
|
|
||||||
now: Instant,
|
|
||||||
): EventForm {
|
|
||||||
val (start, end) = if (isAllDay) {
|
|
||||||
// All-day provider times are UTC midnights with an exclusive end; show
|
|
||||||
// the last covered day and keep placeholder wall-clock times in case the
|
|
||||||
// user switches the event to timed (mirrors EventDetail.toEditForm).
|
|
||||||
val startDate = beginMillis
|
|
||||||
?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.UTC).date }
|
|
||||||
?: now.toLocalDateTime(zone).date
|
|
||||||
val endDate = endMillis
|
|
||||||
?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.UTC).date }
|
|
||||||
?.let { exclusive -> maxOf(startDate, LocalDate.fromEpochDays(exclusive.toEpochDays() - 1)) }
|
|
||||||
?: startDate
|
|
||||||
LocalDateTime(startDate, LocalTime(9, 0)) to LocalDateTime(endDate, LocalTime(10, 0))
|
|
||||||
} else {
|
|
||||||
val startTime = beginMillis
|
|
||||||
?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(zone) }
|
|
||||||
?: nextFullHour(now, zone)
|
|
||||||
val endTime = endMillis
|
|
||||||
?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(zone) }
|
|
||||||
?.takeIf { it >= startTime }
|
|
||||||
?: (startTime.toInstant(zone) + 1.hours).toLocalDateTime(zone)
|
|
||||||
startTime to endTime
|
|
||||||
}
|
|
||||||
return EventForm(
|
|
||||||
calendarId = null,
|
|
||||||
title = title.orEmpty(),
|
|
||||||
isAllDay = isAllDay,
|
|
||||||
start = start,
|
|
||||||
end = end,
|
|
||||||
location = location.orEmpty(),
|
|
||||||
description = description.orEmpty(),
|
|
||||||
// Bare RRULE value (Events.RRULE convention); tolerate a leading "RRULE:"
|
|
||||||
// some callers include.
|
|
||||||
rrule = rrule?.removePrefix("RRULE:")?.takeIf { it.isNotBlank() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun nextFullHour(now: Instant, zone: TimeZone): LocalDateTime {
|
|
||||||
val hourMillis = 3_600_000L
|
|
||||||
val rounded = (now.toEpochMilliseconds() / hourMillis + 1) * hourMillis
|
|
||||||
return Instant.fromEpochMilliseconds(rounded).toLocalDateTime(zone)
|
|
||||||
}
|
|
||||||
@@ -72,8 +72,6 @@ fun CalendarHost(
|
|||||||
onWidgetNavConsumed: () -> Unit = {},
|
onWidgetNavConsumed: () -> Unit = {},
|
||||||
requestedImportUri: android.net.Uri? = null,
|
requestedImportUri: android.net.Uri? = null,
|
||||||
onImportConsumed: () -> Unit = {},
|
onImportConsumed: () -> Unit = {},
|
||||||
requestedInsertForm: EventForm? = null,
|
|
||||||
onInsertConsumed: () -> Unit = {},
|
|
||||||
viewModel: CalendarHostViewModel = hiltViewModel(),
|
viewModel: CalendarHostViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
// Wait for the persisted default view before seeding the stack, so the app
|
// Wait for the persisted default view before seeding the stack, so the app
|
||||||
@@ -178,16 +176,6 @@ fun CalendarHost(
|
|||||||
onImportConsumed()
|
onImportConsumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// An external ACTION_INSERT launch (another app/widget creating an event,
|
|
||||||
// issue #30) arrives already prefilled — open it in the same create form the
|
|
||||||
// single-event .ics path uses. [importForm] is the topmost overlay, so it
|
|
||||||
// reveals on top of whatever was open without extra dismissal.
|
|
||||||
LaunchedEffect(requestedInsertForm) {
|
|
||||||
if (requestedInsertForm != null) {
|
|
||||||
importForm = requestedInsertForm
|
|
||||||
onInsertConsumed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close every overlay that can sit over the calendar, so an externally
|
// Close every overlay that can sit over the calendar, so an externally
|
||||||
// requested destination (a widget/shortcut/QS-tile launch) is revealed on
|
// requested destination (a widget/shortcut/QS-tile launch) is revealed on
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ fun RootScreen(
|
|||||||
onWidgetNavConsumed: () -> Unit = {},
|
onWidgetNavConsumed: () -> Unit = {},
|
||||||
requestedImportUri: android.net.Uri? = null,
|
requestedImportUri: android.net.Uri? = null,
|
||||||
onImportConsumed: () -> Unit = {},
|
onImportConsumed: () -> Unit = {},
|
||||||
requestedInsertForm: de.jeanlucmakiola.calendula.domain.EventForm? = null,
|
|
||||||
onInsertConsumed: () -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var hasPermission by remember {
|
var hasPermission by remember {
|
||||||
@@ -86,8 +84,6 @@ fun RootScreen(
|
|||||||
onWidgetNavConsumed = onWidgetNavConsumed,
|
onWidgetNavConsumed = onWidgetNavConsumed,
|
||||||
requestedImportUri = requestedImportUri,
|
requestedImportUri = requestedImportUri,
|
||||||
onImportConsumed = onImportConsumed,
|
onImportConsumed = onImportConsumed,
|
||||||
requestedInsertForm = requestedInsertForm,
|
|
||||||
onInsertConsumed = onInsertConsumed,
|
|
||||||
)
|
)
|
||||||
false -> ReminderOnboardingScreen(
|
false -> ReminderOnboardingScreen(
|
||||||
onFinished = reminderOnboarding::finish,
|
onFinished = reminderOnboarding::finish,
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
package de.jeanlucmakiola.calendula.domain
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import kotlinx.datetime.LocalDate
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
|
||||||
import kotlinx.datetime.LocalTime
|
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.datetime.atStartOfDayIn
|
|
||||||
import kotlinx.datetime.toInstant
|
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import kotlin.time.Instant
|
|
||||||
|
|
||||||
class InsertEventFormTest {
|
|
||||||
|
|
||||||
private val zone = TimeZone.UTC
|
|
||||||
|
|
||||||
// A fixed "now" at 2026-01-15T10:20:00Z; the next full hour is 11:00.
|
|
||||||
private val now = LocalDateTime(2026, 1, 15, 10, 20).toInstant(zone)
|
|
||||||
|
|
||||||
private fun build(
|
|
||||||
beginMillis: Long? = null,
|
|
||||||
endMillis: Long? = null,
|
|
||||||
isAllDay: Boolean = false,
|
|
||||||
title: String? = null,
|
|
||||||
description: String? = null,
|
|
||||||
location: String? = null,
|
|
||||||
rrule: String? = null,
|
|
||||||
): EventForm = buildInsertEventForm(
|
|
||||||
beginMillis = beginMillis,
|
|
||||||
endMillis = endMillis,
|
|
||||||
isAllDay = isAllDay,
|
|
||||||
title = title,
|
|
||||||
description = description,
|
|
||||||
location = location,
|
|
||||||
rrule = rrule,
|
|
||||||
zone = zone,
|
|
||||||
now = now,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun millis(dateTime: LocalDateTime): Long = dateTime.toInstant(zone).toEpochMilliseconds()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `timed event maps all provided fields`() {
|
|
||||||
val start = LocalDateTime(2026, 3, 1, 14, 30)
|
|
||||||
val end = LocalDateTime(2026, 3, 1, 15, 45)
|
|
||||||
val form = build(
|
|
||||||
beginMillis = millis(start),
|
|
||||||
endMillis = millis(end),
|
|
||||||
title = "Standup",
|
|
||||||
description = "Daily",
|
|
||||||
location = "Room 1",
|
|
||||||
rrule = "FREQ=DAILY",
|
|
||||||
)
|
|
||||||
assertThat(form.calendarId).isNull() // resolves to last-used / first-writable
|
|
||||||
assertThat(form.isAllDay).isFalse()
|
|
||||||
assertThat(form.start).isEqualTo(start)
|
|
||||||
assertThat(form.end).isEqualTo(end)
|
|
||||||
assertThat(form.title).isEqualTo("Standup")
|
|
||||||
assertThat(form.description).isEqualTo("Daily")
|
|
||||||
assertThat(form.location).isEqualTo("Room 1")
|
|
||||||
assertThat(form.rrule).isEqualTo("FREQ=DAILY")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `missing times default to the next full hour and a one-hour duration`() {
|
|
||||||
val form = build(title = "Quick note")
|
|
||||||
assertThat(form.start).isEqualTo(LocalDateTime(2026, 1, 15, 11, 0))
|
|
||||||
assertThat(form.end).isEqualTo(LocalDateTime(2026, 1, 15, 12, 0))
|
|
||||||
assertThat(form.title).isEqualTo("Quick note")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `timed event with only a start gets a one-hour end`() {
|
|
||||||
val start = LocalDateTime(2026, 5, 2, 9, 0)
|
|
||||||
val form = build(beginMillis = millis(start))
|
|
||||||
assertThat(form.start).isEqualTo(start)
|
|
||||||
assertThat(form.end).isEqualTo(LocalDateTime(2026, 5, 2, 10, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `an end before the start is ignored and falls back to plus one hour`() {
|
|
||||||
val start = LocalDateTime(2026, 5, 2, 9, 0)
|
|
||||||
val badEnd = LocalDateTime(2026, 5, 2, 8, 0)
|
|
||||||
val form = build(beginMillis = millis(start), endMillis = millis(badEnd))
|
|
||||||
assertThat(form.end).isEqualTo(LocalDateTime(2026, 5, 2, 10, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `all-day event uses UTC dates and decrements the exclusive end`() {
|
|
||||||
val startMillis = LocalDate(2026, 1, 1).atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
|
||||||
val endExclusive = LocalDate(2026, 1, 3).atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
|
||||||
val form = build(beginMillis = startMillis, endMillis = endExclusive, isAllDay = true, title = "Trip")
|
|
||||||
assertThat(form.isAllDay).isTrue()
|
|
||||||
assertThat(form.start.date).isEqualTo(LocalDate(2026, 1, 1))
|
|
||||||
assertThat(form.end.date).isEqualTo(LocalDate(2026, 1, 2))
|
|
||||||
// Placeholder wall-clock times survive a switch back to a timed event.
|
|
||||||
assertThat(form.start.time).isEqualTo(LocalTime(9, 0))
|
|
||||||
assertThat(form.end.time).isEqualTo(LocalTime(10, 0))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `all-day event without an end is a single day`() {
|
|
||||||
val startMillis = LocalDate(2026, 6, 10).atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
|
||||||
val form = build(beginMillis = startMillis, isAllDay = true)
|
|
||||||
assertThat(form.start.date).isEqualTo(LocalDate(2026, 6, 10))
|
|
||||||
assertThat(form.end.date).isEqualTo(LocalDate(2026, 6, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `a leading RRULE prefix is stripped and a blank rule becomes null`() {
|
|
||||||
assertThat(build(rrule = "RRULE:FREQ=WEEKLY").rrule).isEqualTo("FREQ=WEEKLY")
|
|
||||||
assertThat(build(rrule = " ").rrule).isNull()
|
|
||||||
assertThat(build(rrule = null).rrule).isNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ composeBom = "2026.05.01"
|
|||||||
# Material 3 Expressive APIs currently live only in the 1.5 alpha line.
|
# Material 3 Expressive APIs currently live only in the 1.5 alpha line.
|
||||||
# Pin explicitly to override the BOM (which ships stable 1.4.0).
|
# Pin explicitly to override the BOM (which ships stable 1.4.0).
|
||||||
# Re-evaluate when 1.5.0 stable lands.
|
# Re-evaluate when 1.5.0 stable lands.
|
||||||
material3 = "1.5.0-alpha21"
|
material3 = "1.5.0-alpha23"
|
||||||
datastore = "1.2.1"
|
datastore = "1.2.1"
|
||||||
junit = "6.1.0"
|
junit = "6.1.0"
|
||||||
junitPlatform = "6.1.0"
|
junitPlatform = "6.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user