device_calendar_plus_android 0.10.0 copy "device_calendar_plus_android: ^0.10.0" to clipboard
device_calendar_plus_android: ^0.10.0 copied to clipboard

PlatformAndroid

Android implementation of the device_calendar_plus plugin.

0.10.0 - 2026-10-18 #

Changed #

  • Breaking: implements the platform interface's new android: / ios: options. CreateCalendarOptionsAndroid moves to the platform interface as CalendarOptionsAndroid.

Added #

  • createEvent, updateEvent and updateRecurring write EventOptionsAndroid / EventUpdateOptionsAndroid.status to CalendarContract.Events.STATUS: on a one-off event, a detached occurrence's exception row, the whole series (allEvents), and the new series of a thisAndFollowing split (which carries the original's when the call sets none). Carried occurrences keep their own (#70).

Fixed #

  • updateEvent on one occurrence wrote its exception row as STATUS_CONFIRMED whatever the series' status; it now keeps the series'.

0.9.3 - 2026-10-18 #

Fixed #

  • updateRecurring with EventSpan.thisAndFollowing that keeps a COUNT rule counts the occurrences before the split from the rule, deleted ones included, as RFC 5545 and EventKit do. It counted the provider's Instances, which leave deleted occurrences out, so the new series ran past the original's end by one occurrence per deletion. The Instances count is now only the fallback for a rule the plugin can't expand itself, such as one with BYHOUR (#220).
  • updateRecurring with EventSpan.thisAndFollowing moves a detached occurrence past the split by the whole 24h days in the anchor's shift (read on the wall clock, truncated toward zero), as EventKit does, not by the calendar days the series moved. A sub-day shift across the series zone's midnight (23:30 to 00:30) moved it a day; it now stays put. Whole day moves, all-day series and all-day toggles count days as before (#224).
  • updateEvent with isAllDay: false and no startDate or endDate keeps an all-day one-off event, or a detached occurrence of an all-day series, on its own days: local midnight of the first through the last second of the last, as on iOS. The stored UTC midnights were left under ALL_DAY=0, so west of UTC it read back as the evening before (#205, #216).
  • updateRecurring with isAllDay: false and no start does the same for an all-day series, on EventSpan.allEvents and on a EventSpan.thisAndFollowing split: each occurrence runs from local midnight through the last second of its days, as on iOS, from the master's span. It kept the UTC-midnight DTSTART under ALL_DAY=0, so west of UTC the series read back the evening before. The DURATION is fixed seconds, so on a DST change day an occurrence ends an hour off iOS's 23:59:59 (#219).
  • updateEvent that toggles isAllDay with only one of startDate or endDate moves the other bound into the new frame too, as iOS does, rather than leaving it in the old one, where it read back a day off (#204).
  • updateRecurring with isAllDay: true without a duration, named at an occurrence, spans the series' first occurrence's local days, as on iOS, not the named occurrence's, on EventSpan.allEvents and on a EventSpan.thisAndFollowing split alike. The two differ when a DST change falls between them (#208, #221).
  • updateRecurring with EventSpan.allEvents that toggles a timed series all-day and moves it counts the move in local days, so a DST change between the series start and the named occurrence no longer lands the start a day off (#186). A toggle back to timed counts it the same way, so it no longer lands a day off in a zone whose DST offsets straddle UTC, such as the Azores.
  • updateRecurring with EventSpan.thisAndFollowing that toggles a timed series all-day carries the detached occurrences past the split onto the new series' slots on their own local dates. One past a DST change could land a day off its slot, and show beside a generated occurrence (#186).
  • updateRecurring with EventSpan.thisAndFollowing and a new CountEnd rule counts it from the series' start, as iOS does: the new series drops the occurrences the new rule would have had before the split, and is a one-off when that leaves none. It kept the count as given, so it ran longer than on iOS (#124).

0.9.2 - 2026-10-14 #

Fixed #

  • updateRecurring with thisAndFollowing and a start on another day the kept rule generates splits the series there even when the rule pins days, instead of refusing the move with INVALID_ARGUMENTS. The refusal only stood in for iOS, which couldn't split such a series; the split itself was always right here (#194).
  • hasPermissions without an Activity (a background context) reports denied when the plugin has recorded a denial, instead of a notDetermined that promised a dialog requestPermissions can't show from there (#127).
  • updateRecurring thisAndFollowing on a COUNT series counts the occurrences before the split from the series' own start. It looked back only five years, so an older series under-counted them and the new series ran past the original end (#124).
  • updateRecurring with isAllDay: true and no start or duration moves a timed series into the all-day frame: each occurrence lands on its own local date, spanning every day it touched, as on iOS. The timed start and duration were left in place, and the series read back as one occurrence or on the wrong date (#124).
  • A thisAndFollowing split that fails part-way removes the new series it created through the same sync-adapter-aware delete as every other plugin delete, rather than leaving a DELETED=1 tombstone on a local calendar (#132, #124).
  • updateEvent with isAllDay: true and no startDate or endDate moves a timed one-off event, or a detached occurrence of a timed series, into the all-day frame: its own local date, spanning every day it touched, as on iOS. The timed DTSTART and DTEND (or duration) were left under ALL_DAY=1, so the provider read them as a UTC date, a day off where that isn't the local one.

0.9.1 - 2026-10-01 #

Fixed #

  • updateRecurring refuses a start move with no new rule unless the existing rule generates the series' new start. It compared only the weekday, day of month and month the rule pins, so an ordinal BYDAY or a BYSETPOS series (the 4th Thursday of November, say) could move to another occurrence of its weekday (the 3rd Thursday), leaving DTSTART on a day the rule never generates (#189). A move onto another day the rule generates (Monday to Wednesday of a Mon/Wed/Fri rule) is now allowed through allEvents; with thisAndFollowing a move to another day still throws while the rule pins days, until iOS can split such a series (#194).

0.9.0 - 2026-09-30 #

Changed #

  • Breaking: updateEvent and deleteEvent without a timestamp refuse a recurring series with INVALID_ARGUMENTS and write nothing; series-wide changes go through updateRecurring / deleteRecurring. This also closes the path that wrote DTEND onto a recurring master (#175, #125). deleteEvent on a bare ID that is gone or only a DELETED=1 tombstone reports NOT_FOUND; collecting a tombstoned series is deleteRecurring(allEvents)'s job.
  • updateRecurring forwards a reminders patch (#175).

Fixed #

  • updateRecurring on an all-day series converts the new start to the stored UTC midnight before comparing it, as updateEvent already did. West of UTC a same-day start was refused and a day-earlier move onto a day the rule doesn't generate got through; east of UTC a one-day move counted as zero days and the series stayed put (#144).
  • Modal plumbing (#123): a second showEventModal/showCreateEventModal while one is showing fails OPERATION_FAILED instead of orphaning the first reply; a pending modal survives a configuration change (rotation) and resolves when the activity goes away for good; no Activity replies OPERATION_FAILED rather than an unconverted error; and showEventModal looks the event up first and fails NOT_FOUND for one that doesn't exist, matching iOS.
  • updateEvent or deleteEvent on a single occurrence of a recurring event on a synced calendar (Google, Exchange) no longer hides the whole series when the series hasn't synced yet. Until the calendar's first sync of the series, it lists as it was, and the per-occurrence change shows once the calendar syncs (#163, the synced-calendar side of #153).
  • createEvent and updateRecurring refuse a recurrence rule whose FREQ isn't DAILY/WEEKLY/MONTHLY/YEARLY (or that is malformed) with INVALID_ARGUMENTS before writing, matching iOS. FREQ=HOURLY used to be stored as an hourly series, and malformed input failed as OPERATION_FAILED (#125).

0.8.0 - 2026-09-26 #

Fixed #

  • Event times are now written at whole seconds, as on iOS. A sub-second start was stored with its milliseconds, and some providers (Samsung, Android 11) expand a series' occurrences at whole seconds, so getEvent on the series and its listed occurrences disagreed by up to a second. createEvent, updateEvent and updateRecurring now floor start and end to the second, and a series re-anchored by updateRecurring drops any milliseconds it was stored with (#165).
  • updateRecurring with thisAndFollowing now carries an occurrence that was edited on its own, and falls on or after the split, into the new series, as iOS does. It keeps its own title, time, reminders and other edits, and the new series no longer lists a second copy beside it. When the split moves the start, the occurrence's slot moves by the same number of days. An occurrence deleted on its own stays deleted if the start stays put, and comes back if the start moves, matching iOS. On a synced calendar the occurrence is re-created on the new series and the old one is deleted, since the server ties it to the old series (#158).
  • Deletes and recurring edits on a synced calendar (Google, Exchange, any account but local) now reach the server. The plugin wrote them as the calendar's own sync adapter, which the provider takes as the server's word: the row was changed or removed locally, never marked for upload, and the next sync brought the event back — one duplicate per delete-and-sync cycle. deleteEvent, deleteRecurring (whole series, one occurrence, or thisAndFollowing, including the detached occurrences it sweeps) and updateRecurring now write as the app they are, so the adapter finds a DELETED/DIRTY row to upload; local calendars, which have no adapter, keep the direct deletes (#132, #161).
  • deleteRecurring with thisAndFollowing now removes a detached occurrence that falls on or after the split. Truncating the series' rule left an occurrence that had been edited on its own behind as an orphan row, which came back in listEvents once the provider next rebuilt its Instances cache; iOS removes it, so Android does too.
  • updateCalendar and deleteCalendar refuse a calendar whose access level is below contributor — the same calendars listCalendars reports as readOnly — with READ_ONLY before writing, instead of renaming or deleting any row they were handed (#126).
  • createCalendar refuses a non-local accountType with READ_ONLY, as listSources already reports, instead of sync-adapter-inserting a phantom calendar into another account's namespace (#126).
  • listCalendars reads a NULL display name as empty instead of handing Dart a null it casts (#126).
  • getEvent resolves the instance ID of an all-day recurring occurrence. The lookup went through the all-day date filter with a two-second window, which collapses to an empty date range in every timezone, so it always returned null; it now matches the Instances row on EVENT_ID and BEGIN (#122).
  • getEvent with a bare recurring ID returns the master's real end date. A recurring row stores DURATION with no DTEND, and the end used to fall back to the start (#122).
  • listEvents sorts on the start date it reports, so an all-day event lands at its local midnight among timed events in non-UTC zones instead of at its stored UTC-midnight instant. Matches iOS (#122).
  • Editing or deleting a single occurrence of a recurring event on a local calendar no longer makes the rest of the series disappear. The Calendar Provider keys a series' exceptions by _sync_id, which a local calendar never gets, so the exception insert wiped the master's occurrences from its Instances cache — the earlier ones for good. A local series is now given a _sync_id before its first exception is written, and deleting the series removes its detached occurrences with it (#153).
  • getEvent, updateEvent, updateRecurring, and deleteEvent / deleteRecurring for anything short of the whole series no longer see an event that another app has deleted but the provider only tombstoned (DELETED=1, the fate of any event with a _sync_id deleted outside a sync adapter, which now includes a local series edited per occurrence). Such an event reads as not found instead of accepting edits against a row that never shows in listEvents. On a local calendar a whole-series delete still collects the tombstone.
  • updateRecurring with a new rule anchors the series on the first day that rule generates, as iOS does. Changing a weekly series to another weekday left its start on the old day, so the first occurrence was stranded there. A rule that generates no occurrence at all is refused with INVALID_ARGUMENTS and the series is left untouched (#140).
  • listEvents returns an all-day event when the window is a sub-day slice of its date (e.g. 10:00–11:00). The all-day date filter mapped both window edges to the same UTC midnight, so the range collapsed to nothing; the end edge now rounds up to the next UTC midnight when it isn't on a local midnight. Matches iOS.

Changed #

  • Migrated to Flutter's built-in Kotlin: the plugin no longer applies the Kotlin Gradle Plugin itself, which silences the KGP deprecation warning on every flutter build and keeps the plugin building once Flutter rejects plugins that apply KGP themselves (#133).
  • Minimum supported SDK is now Flutter 3.44 / Dart 3.12, as the migration requires.

0.7.2 - 2026-09-21 #

Fixed #

  • showCreateEventModal no longer requires READ_CALENDAR. ACTION_INSERT needs no permission, so the gate only blocked the one path that still works after a denial (#121, #141).
  • listCalendars, listSources, listEvents and getEvent throw permissionDenied when READ_CALENDAR isn't held instead of returning an empty result (#121).
  • Full-tier operations (calendar mutations, event update/delete, recurring update/delete) require READ_CALENDAR and WRITE_CALENDAR; WRITE_CALENDAR alone is the write-only tier and could previously mutate calendars that iOS rejects. The error message names the tier that's missing (#121).
  • showCreateEventModal writes EXTRA_EVENT_ALL_DAY as a boolean extra, so the all-day prefill is honoured (#121).

Changed #

  • Permission gates are consolidated in PermissionGates.kt; event cursor projections go through EventColumns presets (#119). No behaviour change.

0.7.1 - 2026-07-22 #

Added #

  • Event maps include colorHex read from Events.EVENT_COLOR when the event has a custom color; absent otherwise (#117).

0.7.0 - 2026-06-17 #

Added #

  • Write-only access: a writeOnly request asks for WRITE_CALENDAR only. READ_CALENDAR and WRITE_CALENDAR share the CALENDAR group, so a later full request escalates to read access with no dialog (#89).
  • createEvent with no calendarId resolves a default calendar — the primary writable calendar, falling back to the first writable one (#88).
  • Event reminders via CalendarContract.Reminders rows and Events.HAS_ALARM (#87).

Fixed #

  • Permanent-denial detection keys off WRITE_CALENDAR, and a cancelled permission dialog no longer reports denied (#108).
  • createEvent with no calendarId reports permissionDenied rather than "no writable calendar" when it can't read the calendar list to resolve a default (#112).

0.6.0 - 2026-06-16 #

Changed #

  • Breaking: updateRecurring takes the anchored occurrence's new start (newStartMillis) instead of startMinuteOfDay. shiftDate translates the series anchor by the wall-clock delta (calendar-day count + time-of-day) in the event's EVENT_TIMEZONE, so a move can change the day and time together and stays correct across DST (#103).

Fixed #

  • A time-only allEvents edit now re-writes the unchanged RRULE so the CalendarProvider re-expands the Instances cache; without it a moved DTSTART could read back as a single occurrence.

Behaviour #

  • updateRecurring rejects a start that moves the day of a series whose RRULE pins it (BYDAY / BYMONTHDAY / BYMONTH) unless a new rule is also supplied (dayMoveConflictsWithRule).

0.5.2 - 2026-06-15 #

Fixed #

  • listEvents now returns a zero-duration (instantaneous) event that sits exactly on the query's start time; the half-open overlap check previously excluded it (#416)

0.5.1 - 2026-06-15 #

  • No functional changes; version aligned with the rest of the suite for the 0.5.1 release

0.5.0 - 2026-06-11 #

Changed #

  • Breaking: recurring-edit split — updateRecurring / deleteRecurring accept only allEvents and thisAndFollowing; single occurrences go through updateEvent / deleteEvent with an occurrence timestamp (detached exception rows; deletes via STATUS_CANCELED exceptions)
  • updateRecurring time changes preserve each occurrence's date, replacing only the time-of-day; thisAndFollowing truncates the master with UNTIL to match iOS's EKSpan.futureEvents split

Fixed #

  • Calendar Provider work now runs on a background thread. Method-channel handlers were doing blocking provider queries on the main thread, which could ANR on large calendars (#73). Thanks @mauriziopinotti for the report and a working proof-of-fix.
  • A NULL STATUS column reads back as none; it was defaulted to 0, which is STATUS_TENTATIVE, so status-less events came back tentative (#70) — thanks @mauriziopinotti

0.4.0 - 2026-05-25 #

Added #

  • updateRecurring() — series-level recurring-event edits with EventSpan (allEvents / thisAndFollowing / thisInstance). thisAndFollowing truncates the master with UNTIL and starts a new series; thisInstance writes a detached exception event.
  • deleteRecurring() — allEvents deletes the master; thisAndFollowing truncates via UNTIL; thisInstance appends to the master's EXDATE column (no separate exception event needed).
  • url field on events via Events.CUSTOM_APP_URI
  • Patch<T> support in updateEvent() — null leaves a field unchanged, Patch.set writes, Patch.clear writes the empty string to remove
  • edit flag on showEvent() — fires Intent.ACTION_EDIT instead of ACTION_VIEW

Fixed #

  • Event deletion now uses sync-adapter context so EventKit-equivalent listEvents calls stop returning the deleted row immediately

Changed #

  • Extracted all-day date-conversion helpers; no behaviour change

0.3.5 - 2026-04-20 #

Fixed #

  • All-day events appearing in wrong day's query in non-UTC timezones (#20)
  • PermissionService accepts Context — hasPermissions() works without an Activity (#31)

0.3.4 - 2026-02-08 #

Version sync with other packages. No functional changes.

0.3.3 - 2025-12-21 #

Fixed #

  • Fixed parsing of instanceId for events with @ in their event ID (e.g., Google Calendar IDs like abc123@google.com)

0.3.2 - 2025-12-19 #

Added #

  • CreateCalendarOptionsAndroid for specifying custom account name when creating calendars
  • createCalendar() now accepts optional accountName parameter via platform options

0.3.1 - 2025-11-07 #

Fixed #

  • showEvent() now uses startActivityForResult() to properly await until the calendar activity is dismissed

0.3.0 - 2024-11-05 #

Changed #

  • BREAKING: deleteEvent() now always deletes entire series for recurring events (removed deleteAllInstances parameter)
  • BREAKING: updateEvent() now always updates entire series for recurring events (removed updateAllInstances parameter)
  • Native code now extracts event ID from instance ID format automatically

Removed #

  • BREAKING: NOT_SUPPORTED error code (no longer needed as single-instance operations are not attempted)

0.2.0 - 2024-11-05 #

Added #

  • openAppSettings() implementation to open Android app settings via Intent

Removed #

  • BREAKING: getPlatformVersion() implementation (unused boilerplate)

0.1.1 - 2024-11-04 #

Added #

  • ProGuard/R8 rules to prevent code stripping in release builds
  • Automatic consumer ProGuard rules configuration

0.1.0 - 2024-11-04 #

Initial release.