diff --git a/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml b/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml
index 8c38e08a0..61b1737c9 100644
--- a/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml
+++ b/DeviceTests/DeviceTests.Android/Properties/AndroidManifest.xml
@@ -5,7 +5,8 @@
-
+
+
diff --git a/DeviceTests/DeviceTests.Shared/Calendar_Tests.cs b/DeviceTests/DeviceTests.Shared/Calendar_Tests.cs
index c60511927..54671ca8d 100644
--- a/DeviceTests/DeviceTests.Shared/Calendar_Tests.cs
+++ b/DeviceTests/DeviceTests.Shared/Calendar_Tests.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
using Xamarin.Essentials;
using Xunit;
@@ -99,5 +102,392 @@ public Task Get_Event_By_Bad_Text_Id(string eventId)
await Assert.ThrowsAsync(() => Calendars.GetEventAsync(eventId));
});
}
+
+ [Fact]
+ public Task Full_Calendar_Edit_Test()
+ {
+ return Utils.OnMainThread(async () =>
+ {
+ // Create Calendar
+ var calendars = await Calendars.GetCalendarsAsync();
+ var calendar = calendars.FirstOrDefault(x => x.Name == "Test_Calendar");
+ var calendarId = string.Empty;
+ if (calendar == null)
+ {
+ var newCalendar = new Calendar() { Name = "Test_Calendar" };
+ calendarId = await Calendars.CreateCalendar(newCalendar);
+ }
+ else
+ {
+ calendarId = calendar.Id;
+ }
+
+ var startDate = TimeZoneInfo.ConvertTime(new DateTimeOffset(2019, 4, 1, 10, 30, 0, TimeZoneInfo.Local.BaseUtcOffset), TimeZoneInfo.Local);
+ var events = await Calendars.GetEventsAsync(calendarId, startDate, startDate.AddHours(10));
+ var newEvent = events.FirstOrDefault(x => x.Title == "Test_Event");
+ var eventId = string.Empty;
+ if (newEvent == null)
+ {
+ newEvent = new CalendarEvent()
+ {
+ Title = "Test_Event",
+ CalendarId = calendarId,
+ StartDate = startDate,
+ EndDate = startDate.AddHours(10)
+ };
+ eventId = await Calendars.CreateCalendarEvent(newEvent);
+ }
+ else
+ {
+ eventId = newEvent.Id;
+ }
+ Assert.NotEmpty(eventId);
+ var createdEvent = await Calendars.GetEventByIdAsync(eventId);
+ newEvent.Id = createdEvent.Id;
+ newEvent.Attendees = createdEvent.Attendees;
+
+ Assert.Equal(newEvent.Id, createdEvent.Id);
+ Assert.Equal(newEvent.CalendarId, createdEvent.CalendarId);
+ Assert.Equal(newEvent.Title, createdEvent.Title);
+ Assert.Equal(string.Empty, createdEvent.Description);
+ Assert.Equal(string.Empty, createdEvent.Location);
+ Assert.Equal(string.Empty, createdEvent.Url);
+ Assert.Equal(newEvent.AllDay, createdEvent.AllDay);
+ Assert.Equal(newEvent.StartDate, createdEvent.StartDate);
+ Assert.Equal(newEvent.Duration, createdEvent.Duration);
+ Assert.Equal(newEvent.EndDate, createdEvent.EndDate);
+ Assert.Equal(newEvent.Attendees, createdEvent.Attendees);
+ Assert.Equal(newEvent.Reminders, createdEvent.Reminders);
+ Assert.Equal(newEvent.RecurrancePattern, createdEvent.RecurrancePattern);
+
+ createdEvent.RecurrancePattern = new RecurrenceRule()
+ {
+ Frequency = RecurrenceFrequency.YearlyOnDay,
+ Interval = 1,
+ WeekOfMonth = IterationOffset.Second,
+ DaysOfTheWeek = new List() { CalendarDayOfWeek.Thursday },
+ MonthOfTheYear = MonthOfYear.April,
+ TotalOccurrences = 4
+ };
+ createdEvent.AllDay = true;
+
+ var updateSuccessful = await Calendars.UpdateCalendarEvent(createdEvent);
+ var updatedEvent = await Calendars.GetEventByIdAsync(createdEvent.Id);
+
+ // Updated Successfuly
+ Assert.True(updateSuccessful);
+ Assert.Equal(createdEvent.Id, updatedEvent.Id);
+ Assert.Equal(createdEvent.CalendarId, updatedEvent.CalendarId);
+ Assert.Equal(createdEvent.Title, updatedEvent.Title);
+ Assert.Equal(createdEvent.Description, updatedEvent.Description);
+ Assert.Equal(createdEvent.Location, updatedEvent.Location);
+ Assert.Equal(createdEvent.Url, updatedEvent.Url);
+ Assert.Equal(createdEvent.AllDay, updatedEvent.AllDay);
+ Assert.NotEqual(createdEvent.StartDate, updatedEvent.StartDate);
+ Assert.Equal(createdEvent.Attendees, updatedEvent.Attendees);
+ Assert.Equal(createdEvent.Reminders, updatedEvent.Reminders);
+
+ var attendeeToAddAndRemove = new CalendarEventAttendee() { Email = "fake@email.com", Name = "Fake Email", Type = AttendeeType.Resource };
+
+ // Added Attendee to event successfully
+ var attendeeAddedSuccessfully = await Calendars.AddAttendeeToEvent(attendeeToAddAndRemove, updatedEvent.Id);
+ Assert.True(attendeeAddedSuccessfully);
+
+ // Verify Attendee added to event
+ updatedEvent = await Calendars.GetEventByIdAsync(createdEvent.Id);
+ var expectedAttendeeCount = createdEvent.Attendees != null ? createdEvent.Attendees.Count() + 1 : 1;
+ Assert.Equal(updatedEvent.Attendees.Count(), expectedAttendeeCount);
+
+ // Remove Attendee from event
+ var removedAttendeeSuccessfully = await Calendars.RemoveAttendeeFromEvent(attendeeToAddAndRemove, updatedEvent.Id);
+ Assert.True(removedAttendeeSuccessfully);
+
+ var dateOfSecondOccurence = TimeZoneInfo.ConvertTime(new DateTimeOffset(2020, 4, 9, 0, 0, 0, TimeZoneInfo.Local.BaseUtcOffset), TimeZoneInfo.Local);
+ var eventInstance = await Calendars.GetEventInstanceByIdAsync(updatedEvent.Id, dateOfSecondOccurence);
+
+ // Retrieve instance of event
+ Assert.Equal(eventInstance.Id, updatedEvent.Id);
+ Assert.Equal(eventInstance.StartDate.Date, dateOfSecondOccurence.Date);
+
+ // Delete instance of event
+ var canDeleteInstance = await Calendars.DeleteCalendarEventInstanceByDate(eventInstance.Id, calendarId, eventInstance.StartDate);
+ Assert.True(canDeleteInstance);
+
+ // Get whole event
+ var eventStillExists = await Calendars.GetEventByIdAsync(eventInstance.Id);
+ Assert.NotNull(eventStillExists);
+
+ // Delete whole event
+ var deleteEvent = await Calendars.DeleteCalendarEventById(eventInstance.Id, calendarId);
+ Assert.True(deleteEvent);
+ });
+ }
+
+ [Fact]
+ public Task Basic_Calendar_Creation()
+ {
+ return Utils.OnMainThread(async () =>
+ {
+ var newCalendar = new Calendar() { Name = "Test_Calendar" };
+ var calendarId = await Calendars.CreateCalendar(newCalendar);
+ Assert.NotEmpty(calendarId);
+ });
+ }
+
+ [Fact]
+ public Task Basic_Calendar_Event_Creation()
+ {
+ return Utils.OnMainThread(async () =>
+ {
+ var calendars = await Calendars.GetCalendarsAsync();
+ var calendar = calendars.FirstOrDefault(x => x.Name == "Test_Calendar");
+ var calendarId = string.Empty;
+ if (calendar == null)
+ {
+ var newCalendar = new Calendar() { Name = "Test_Calendar" };
+ calendarId = await Calendars.CreateCalendar(newCalendar);
+ }
+ else
+ {
+ calendarId = calendar.Id;
+ }
+
+ var startDate = TimeZoneInfo.ConvertTime(new DateTimeOffset(2019, 4, 1, 10, 30, 0, TimeZoneInfo.Local.BaseUtcOffset), TimeZoneInfo.Local);
+ var newEvent = new CalendarEvent()
+ {
+ Title = "Test_Event",
+ CalendarId = calendarId,
+ StartDate = startDate,
+ EndDate = startDate.AddHours(10)
+ };
+ var eventId = await Calendars.CreateCalendarEvent(newEvent);
+ Assert.NotEmpty(eventId);
+ });
+ }
+
+ [Fact]
+ public Task Basic_Calendar_Event_Attendee_Add()
+ {
+ return Utils.OnMainThread(async () =>
+ {
+ var calendars = await Calendars.GetCalendarsAsync();
+ var calendar = calendars.FirstOrDefault(x => x.Name == "Test_Calendar");
+ var calendarId = string.Empty;
+ if (calendar == null)
+ {
+ var newCalendar = new Calendar() { Name = "Test_Calendar" };
+ calendarId = await Calendars.CreateCalendar(newCalendar);
+ }
+ else
+ {
+ calendarId = calendar.Id;
+ }
+
+ var startDate = TimeZoneInfo.ConvertTime(new DateTimeOffset(2019, 4, 1, 10, 30, 0, TimeZoneInfo.Local.BaseUtcOffset), TimeZoneInfo.Local);
+ var events = await Calendars.GetEventsAsync(calendarId, startDate, startDate.AddHours(10));
+ var newEvent = events.FirstOrDefault(x => x.Title == "Test_Event");
+ var eventId = string.Empty;
+ if (newEvent == null)
+ {
+ newEvent = new CalendarEvent()
+ {
+ Title = "Test_Event",
+ CalendarId = calendarId,
+ StartDate = startDate,
+ EndDate = startDate.AddHours(10)
+ };
+ eventId = await Calendars.CreateCalendarEvent(newEvent);
+ }
+ else
+ {
+ eventId = newEvent.Id;
+ }
+ var attendeeToAdd = new CalendarEventAttendee() { Email = "fake@email.com", Name = "Fake Out", Type = AttendeeType.Required };
+ Assert.True(await Calendars.AddAttendeeToEvent(attendeeToAdd, eventId));
+
+ newEvent = await Calendars.GetEventByIdAsync(eventId);
+ var attendee = newEvent.Attendees.FirstOrDefault(x => x.Email == "fake@email.com");
+
+ Assert.Equal(attendee.Email, attendeeToAdd.Email);
+ Assert.Equal(attendee.Name, attendeeToAdd.Name);
+ Assert.Equal(attendee.IsOrganizer, attendeeToAdd.IsOrganizer);
+ Assert.Equal(attendee.Type, attendeeToAdd.Type);
+ });
+ }
+
+ [Fact]
+ public Task Basic_Calendar_Event_Attendee_Remove()
+ {
+ return Utils.OnMainThread(async () =>
+ {
+ var calendars = await Calendars.GetCalendarsAsync();
+ var calendar = calendars.FirstOrDefault(x => x.Name == "Test_Calendar");
+ var calendarId = string.Empty;
+ if (calendar == null)
+ {
+ var newCalendar = new Calendar() { Name = "Test_Calendar" };
+ calendarId = await Calendars.CreateCalendar(newCalendar);
+ }
+ else
+ {
+ calendarId = calendar.Id;
+ }
+
+ var startDate = TimeZoneInfo.ConvertTime(new DateTimeOffset(2019, 4, 1, 10, 30, 0, TimeZoneInfo.Local.BaseUtcOffset), TimeZoneInfo.Local);
+ var events = await Calendars.GetEventsAsync(calendarId, startDate, startDate.AddHours(10));
+ var newEvent = events.FirstOrDefault(x => x.Title == "Test_Event");
+ var eventId = string.Empty;
+ if (newEvent == null)
+ {
+ newEvent = new CalendarEvent()
+ {
+ Title = "Test_Event",
+ CalendarId = calendarId,
+ StartDate = startDate,
+ EndDate = startDate.AddHours(10)
+ };
+ eventId = await Calendars.CreateCalendarEvent(newEvent);
+ }
+ else
+ {
+ eventId = newEvent.Id;
+ }
+
+ var attendeeCount = 0;
+ CalendarEventAttendee attendeeToAdd = null;
+ CalendarEventAttendee attendee = null;
+ if (newEvent.Attendees != null)
+ {
+ if (newEvent.Attendees.Count() > 0)
+ {
+ attendeeToAdd = newEvent.Attendees.FirstOrDefault(x => x.Email == "fake@email.com");
+ if (attendeeToAdd != null)
+ {
+ attendeeCount = newEvent.Attendees.Count();
+ attendee = attendeeToAdd;
+ }
+ else
+ {
+ attendeeToAdd = new CalendarEventAttendee() { Email = "fake@email.com", Name = "Fake Out", Type = AttendeeType.Required };
+ Assert.True(await Calendars.AddAttendeeToEvent(attendeeToAdd, eventId));
+ newEvent = await Calendars.GetEventByIdAsync(eventId);
+ attendeeCount = newEvent.Attendees.Count();
+ attendee = newEvent.Attendees.FirstOrDefault(x => x.Email == "fake@email.com");
+
+ Assert.Equal(attendee.Email, attendeeToAdd.Email);
+ Assert.Equal(attendee.Name, attendeeToAdd.Name);
+ Assert.Equal(attendee.IsOrganizer, attendeeToAdd.IsOrganizer);
+ Assert.Equal(attendee.Type, attendeeToAdd.Type);
+ }
+ }
+ }
+ else
+ {
+ attendeeToAdd = new CalendarEventAttendee() { Email = "fake@email.com", Name = "Fake Out", Type = AttendeeType.Required };
+ Assert.True(await Calendars.AddAttendeeToEvent(attendeeToAdd, eventId));
+ newEvent = await Calendars.GetEventByIdAsync(eventId);
+ attendeeCount = newEvent.Attendees.Count();
+ attendee = newEvent.Attendees.FirstOrDefault(x => x.Email == "fake@email.com");
+
+ Assert.Equal(attendee.Email, attendeeToAdd.Email);
+ Assert.Equal(attendee.Name, attendeeToAdd.Name);
+ Assert.Equal(attendee.IsOrganizer, attendeeToAdd.IsOrganizer);
+ Assert.Equal(attendee.Type, attendeeToAdd.Type);
+ }
+ Assert.True(await Calendars.RemoveAttendeeFromEvent(attendee, eventId));
+ newEvent = await Calendars.GetEventByIdAsync(eventId);
+ var newAttendeeCount = newEvent.Attendees.Count();
+
+ Assert.Equal(attendeeCount - 1, newAttendeeCount);
+ });
+ }
+
+ [Fact]
+ public Task Basic_Calendar_Event_Update()
+ {
+ return Utils.OnMainThread(async () =>
+ {
+ var calendars = await Calendars.GetCalendarsAsync();
+ var calendar = calendars.FirstOrDefault(x => x.Name == "Test_Calendar");
+ var calendarId = string.Empty;
+ if (calendar == null)
+ {
+ var newCalendar = new Calendar() { Name = "Test_Calendar" };
+ calendarId = await Calendars.CreateCalendar(newCalendar);
+ }
+ else
+ {
+ calendarId = calendar.Id;
+ }
+
+ var startDate = TimeZoneInfo.ConvertTime(new DateTimeOffset(2019, 4, 1, 10, 30, 0, TimeZoneInfo.Local.BaseUtcOffset), TimeZoneInfo.Local);
+ var events = await Calendars.GetEventsAsync(calendarId, startDate, startDate.AddHours(10));
+ var newEvent = events.FirstOrDefault(x => x.Title == "Test_Event");
+ if (newEvent == null)
+ {
+ newEvent = new CalendarEvent()
+ {
+ Title = "Test_Event",
+ CalendarId = calendarId,
+ StartDate = startDate,
+ EndDate = startDate.AddHours(10)
+ };
+ var eventId = await Calendars.CreateCalendarEvent(newEvent);
+ newEvent = await Calendars.GetEventByIdAsync(eventId);
+ }
+ else
+ {
+ newEvent = await Calendars.GetEventByIdAsync(newEvent.Id);
+ }
+
+ newEvent.AllDay = true;
+
+ var result = await Calendars.UpdateCalendarEvent(newEvent);
+ Assert.True(result);
+ });
+ }
+
+ [Fact]
+ public Task Basic_Calendar_Event_Deletion()
+ {
+ return Utils.OnMainThread(async () =>
+ {
+ var calendars = await Calendars.GetCalendarsAsync();
+ var calendar = calendars.FirstOrDefault(x => x.Name == "Test_Calendar");
+ var calendarId = string.Empty;
+ if (calendar == null)
+ {
+ var newCalendar = new Calendar() { Name = "Test_Calendar" };
+ calendarId = await Calendars.CreateCalendar(newCalendar);
+ }
+ else
+ {
+ calendarId = calendar.Id;
+ }
+
+ var startDate = TimeZoneInfo.ConvertTime(new DateTimeOffset(2019, 4, 1, 10, 30, 0, TimeZoneInfo.Local.BaseUtcOffset), TimeZoneInfo.Local);
+ var events = await Calendars.GetEventsAsync(calendarId, startDate, startDate.AddHours(10));
+ var newEvent = events.FirstOrDefault(x => x.Title == "Test_Event");
+ var eventId = string.Empty;
+ if (newEvent == null)
+ {
+ newEvent = new CalendarEvent()
+ {
+ Title = "Test_Event",
+ CalendarId = calendarId,
+ StartDate = startDate,
+ EndDate = startDate.AddHours(10)
+ };
+ eventId = await Calendars.CreateCalendarEvent(newEvent);
+ }
+ else
+ {
+ eventId = newEvent.Id;
+ }
+ var result = await Calendars.DeleteCalendarEventById(eventId, calendarId);
+
+ Assert.True(result);
+ });
+ }
}
}
diff --git a/Samples/Samples.Android/Properties/AndroidManifest.xml b/Samples/Samples.Android/Properties/AndroidManifest.xml
index b5127354e..9966edeff 100644
--- a/Samples/Samples.Android/Properties/AndroidManifest.xml
+++ b/Samples/Samples.Android/Properties/AndroidManifest.xml
@@ -12,7 +12,7 @@
-
+
diff --git a/Samples/Samples/Converters/AttendeeRequiredColorConverter.cs b/Samples/Samples/Converters/AttendeeRequiredColorConverter.cs
new file mode 100644
index 000000000..2031479b8
--- /dev/null
+++ b/Samples/Samples/Converters/AttendeeRequiredColorConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.Converters
+{
+ public class AttendeeRequiredColorConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (!(value is AttendeeType attendeeType))
+ {
+ return Color.PaleVioletRed;
+ }
+
+ switch (attendeeType)
+ {
+ case AttendeeType.Required:
+ return Color.LightGoldenrodYellow;
+ case AttendeeType.Resource:
+ return Color.PaleGreen;
+ default:
+ return Color.LightGray;
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
+}
diff --git a/Samples/Samples/Converters/RecurrenceDayToBoolConverter.cs b/Samples/Samples/Converters/RecurrenceDayToBoolConverter.cs
new file mode 100644
index 000000000..8e53f2548
--- /dev/null
+++ b/Samples/Samples/Converters/RecurrenceDayToBoolConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using Samples.ViewModel;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.Converters
+{
+ public class RecurrenceDayToBoolConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || !(value is CalendarDayOfWeek recurrenceDays) || !(parameter is CalendarDayOfWeek expectedResult))
+ {
+ return false;
+ }
+
+ return recurrenceDays == expectedResult;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || !(value is bool switchValue) || !(parameter is CalendarDayOfWeek expectedResult) || switchValue == false)
+ {
+ return null;
+ }
+
+ return expectedResult;
+ }
+ }
+}
diff --git a/Samples/Samples/Converters/RecurrenceEndTypeToBoolConverter.cs b/Samples/Samples/Converters/RecurrenceEndTypeToBoolConverter.cs
new file mode 100644
index 000000000..abb1611e3
--- /dev/null
+++ b/Samples/Samples/Converters/RecurrenceEndTypeToBoolConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Globalization;
+using Samples.ViewModel;
+using Xamarin.Forms;
+
+namespace Samples.Converters
+{
+ public class RecurrenceEndTypeToBoolConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || !(value is string type))
+ {
+ return false;
+ }
+ switch (type)
+ {
+ case RecurrenceEndType.AfterOccurences:
+ return true;
+ case RecurrenceEndType.UntilEndDate:
+ case RecurrenceEndType.Indefinitely:
+ default:
+ return false;
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Samples/Samples/Converters/RecurrenceRuleTextConverter.cs b/Samples/Samples/Converters/RecurrenceRuleTextConverter.cs
new file mode 100644
index 000000000..140069843
--- /dev/null
+++ b/Samples/Samples/Converters/RecurrenceRuleTextConverter.cs
@@ -0,0 +1,86 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.Converters
+{
+ public class RecurrenceRuleTextConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || !(value is RecurrenceRule rule))
+ return null;
+
+ var toReturn = $"Occurs ";
+
+ if (rule.Interval > 0)
+ {
+ if (rule.Interval == 1)
+ {
+ toReturn += $"Every ";
+ }
+ else
+ {
+ toReturn += $"Every {((int)rule.Interval).ToOrdinal()} ";
+ }
+ switch (rule.Frequency)
+ {
+ case RecurrenceFrequency.Daily:
+ toReturn += "Day ";
+ break;
+ case RecurrenceFrequency.Weekly:
+ toReturn += "Week ";
+ break;
+ case RecurrenceFrequency.Monthly:
+ case RecurrenceFrequency.MonthlyOnDay:
+ toReturn += "Month ";
+ break;
+ case RecurrenceFrequency.Yearly:
+ case RecurrenceFrequency.YearlyOnDay:
+ toReturn += "Year ";
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ }
+
+ if (rule.WeekOfMonth != null && (rule.Frequency == RecurrenceFrequency.MonthlyOnDay || rule.Frequency == RecurrenceFrequency.YearlyOnDay))
+ {
+ toReturn += $"on the {rule.WeekOfMonth} ";
+ if (rule.DaysOfTheWeek?.Count > 0)
+ {
+ toReturn += $"[";
+ toReturn = rule.DaysOfTheWeek.Aggregate(toReturn, (current, d) => current + $"{d}, ");
+ toReturn = toReturn.Substring(0, toReturn.Length - 2) + "] ";
+ }
+ if (rule.Frequency == RecurrenceFrequency.YearlyOnDay)
+ {
+ toReturn += $"in {rule.MonthOfTheYear.ToString()} ";
+ }
+ }
+ else if (rule.DaysOfTheWeek?.Count > 0)
+ {
+ toReturn += $"On: [";
+ toReturn = rule.DaysOfTheWeek.Aggregate(toReturn, (current, d) => current + $"{d}, ");
+ toReturn = toReturn.Substring(0, toReturn.Length - 2) + "] ";
+ }
+
+ if (rule.TotalOccurrences > 0)
+ {
+ toReturn += $"For the next {rule.TotalOccurrences} occurrences ";
+ }
+
+ if (rule.EndDate.HasValue)
+ {
+ toReturn += $"Until {rule.EndDate.Value.DateTime.ToShortDateString()} ";
+ }
+
+ return toReturn;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
+}
diff --git a/Samples/Samples/Converters/RecurrenceTypeToBoolConverter.cs b/Samples/Samples/Converters/RecurrenceTypeToBoolConverter.cs
new file mode 100644
index 000000000..04350fe96
--- /dev/null
+++ b/Samples/Samples/Converters/RecurrenceTypeToBoolConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.Linq;
+using Samples.ViewModel;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.Converters
+{
+ public class RecurrenceTypeToBoolConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || !(value is RecurrenceFrequency recurrenceType) || !(parameter is RecurrenceFrequency expectedResult))
+ {
+ return false;
+ }
+
+ switch (expectedResult)
+ {
+ case RecurrenceFrequency.Monthly:
+ return recurrenceType == RecurrenceFrequency.MonthlyOnDay ||
+ recurrenceType == RecurrenceFrequency.Monthly ||
+ recurrenceType == RecurrenceFrequency.YearlyOnDay ||
+ recurrenceType == RecurrenceFrequency.Yearly;
+ case RecurrenceFrequency.Yearly:
+ return recurrenceType == RecurrenceFrequency.Yearly ||
+ recurrenceType == RecurrenceFrequency.YearlyOnDay;
+ default:
+ return recurrenceType == expectedResult;
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || !(value is bool switchValue) || !(parameter is RecurrenceFrequency expectedResult) || switchValue == false)
+ {
+ return false;
+ }
+
+ return expectedResult;
+ }
+ }
+}
diff --git a/Samples/Samples/Converters/RecurrenceUntilTypeToBoolConverter.cs b/Samples/Samples/Converters/RecurrenceUntilTypeToBoolConverter.cs
new file mode 100644
index 000000000..acb1fff4b
--- /dev/null
+++ b/Samples/Samples/Converters/RecurrenceUntilTypeToBoolConverter.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Globalization;
+using Samples.ViewModel;
+using Xamarin.Forms;
+
+namespace Samples.Converters
+{
+ public class RecurrenceUntilTypeToBoolConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || !(value is string type))
+ {
+ return false;
+ }
+ switch (type)
+ {
+ case RecurrenceEndType.UntilEndDate:
+ return true;
+ case RecurrenceEndType.Indefinitely:
+ case RecurrenceEndType.AfterOccurences:
+ default:
+ return false;
+ }
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/Samples/Samples/Converters/ReminderTextConverter.cs b/Samples/Samples/Converters/ReminderTextConverter.cs
new file mode 100644
index 000000000..57b7cb0c5
--- /dev/null
+++ b/Samples/Samples/Converters/ReminderTextConverter.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Globalization;
+using Xamarin.Forms;
+
+namespace Samples.Converters
+{
+ public class ReminderTextConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || !(value is int minutes))
+ return null;
+
+ return minutes > 0 ? $"{minutes} minutes prior" : "No Reminders";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
+}
diff --git a/Samples/Samples/Converters/StartWidthDisplayConverter.cs b/Samples/Samples/Converters/StartWidthDisplayConverter.cs
new file mode 100644
index 000000000..1c2f3a92e
--- /dev/null
+++ b/Samples/Samples/Converters/StartWidthDisplayConverter.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+using Xamarin.Forms;
+
+namespace Samples.Converters
+{
+ public class StartWidthDisplayConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value == null || !(value is bool b))
+ {
+ return 1;
+ }
+ return b ? 3 : 1;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();
+ }
+}
diff --git a/Samples/Samples/View/CalendarAddPage.xaml b/Samples/Samples/View/CalendarAddPage.xaml
new file mode 100644
index 000000000..0fab192ce
--- /dev/null
+++ b/Samples/Samples/View/CalendarAddPage.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/View/CalendarAddPage.xaml.cs b/Samples/Samples/View/CalendarAddPage.xaml.cs
new file mode 100644
index 000000000..88b9e27f4
--- /dev/null
+++ b/Samples/Samples/View/CalendarAddPage.xaml.cs
@@ -0,0 +1,10 @@
+namespace Samples.View
+{
+ public partial class CalendarAddPage : BasePage
+ {
+ public CalendarAddPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/Samples/View/CalendarEventAddPage.xaml b/Samples/Samples/View/CalendarEventAddPage.xaml
new file mode 100644
index 000000000..59c075826
--- /dev/null
+++ b/Samples/Samples/View/CalendarEventAddPage.xaml
@@ -0,0 +1,323 @@
+
+
+
+
+
+
+
+
+ None
+ Weekday
+ Weekend
+ AllDays
+ Daily
+ Weekly
+ Monthly
+ Yearly
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/View/CalendarEventAddPage.xaml.cs b/Samples/Samples/View/CalendarEventAddPage.xaml.cs
new file mode 100644
index 000000000..98c1fe834
--- /dev/null
+++ b/Samples/Samples/View/CalendarEventAddPage.xaml.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.ObjectModel;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.View
+{
+ public partial class CalendarEventAddPage : BasePage
+ {
+ public CalendarEventAddPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/Samples/View/CalendarEventAttendeeAddPage.xaml b/Samples/Samples/View/CalendarEventAttendeeAddPage.xaml
new file mode 100644
index 000000000..5b0389671
--- /dev/null
+++ b/Samples/Samples/View/CalendarEventAttendeeAddPage.xaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/View/CalendarEventAttendeeAddPage.xaml.cs b/Samples/Samples/View/CalendarEventAttendeeAddPage.xaml.cs
new file mode 100644
index 000000000..0e1314470
--- /dev/null
+++ b/Samples/Samples/View/CalendarEventAttendeeAddPage.xaml.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.ObjectModel;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.View
+{
+ public partial class CalendarEventAttendeeAddPage : BasePage
+ {
+ public CalendarEventAttendeeAddPage()
+ {
+ InitializeComponent();
+ }
+ }
+}
diff --git a/Samples/Samples/View/CalendarEventPage.xaml b/Samples/Samples/View/CalendarEventPage.xaml
index 920f973f4..939bc97c0 100644
--- a/Samples/Samples/View/CalendarEventPage.xaml
+++ b/Samples/Samples/View/CalendarEventPage.xaml
@@ -1,4 +1,5 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Title="Event"
+ Padding="20,20,20,20">
+
+
+ {0:dd/MM/yy hh:mm tt}
+ {0:h\:mm}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/View/CalendarEventPage.xaml.cs b/Samples/Samples/View/CalendarEventPage.xaml.cs
index a16229aa1..6a1a98efc 100644
--- a/Samples/Samples/View/CalendarEventPage.xaml.cs
+++ b/Samples/Samples/View/CalendarEventPage.xaml.cs
@@ -1,5 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Linq;
+using Samples.ViewModel;
using Xamarin.Essentials;
using Xamarin.Forms;
@@ -7,9 +10,139 @@ namespace Samples.View
{
public partial class CalendarEventPage : BasePage
{
+ const string actionResponseDeleteAll = "Yes to All";
+ const string actionResponseDeleteOne = "Just this one";
+ const string actionResponseDeleteForward = "From this date forward";
+ const string actionResponseOk = "Ok";
+ const string actionResponseYes = "Yes";
+ const string actionResponseCancel = "Cancel";
+ const string actionTitleInfo = "Info";
+ const string actionTitleWarning = "Warning!";
+
+ CalendarEvent ViewModel => BindingContext as CalendarEvent;
+
public CalendarEventPage()
{
InitializeComponent();
}
+
+ async void OnDeleteEventButtonClicked(object sender, EventArgs e)
+ {
+ if (!(EventId.Text is string eventId) || string.IsNullOrEmpty(eventId))
+ return;
+
+ var viewModel = BindingContext as CalendarEvent;
+
+ var calendarEvent = await Calendars.GetEventInstanceByIdAsync(eventId, viewModel.StartDate);
+
+ if (!(calendarEvent is CalendarEvent))
+ return;
+
+ var answer = await DisplayAlert(actionTitleWarning, $"Are you sure you want to delete {calendarEvent.Title}? (this cannot be undone)", actionResponseYes, actionResponseCancel);
+ if (answer)
+ {
+ if (calendarEvent.RecurrancePattern != null)
+ {
+ var action = await DisplayActionSheet("Do you want to delete all instances of this event?", actionResponseCancel, null, actionResponseDeleteAll, actionResponseDeleteOne, actionResponseDeleteForward);
+ var deletionConfirmed = false;
+ var deletionMessage = string.Empty;
+ switch (action)
+ {
+ case actionResponseDeleteAll:
+ deletionConfirmed = await Calendars.DeleteCalendarEventById(eventId, CalendarId.Text);
+ deletionMessage = $"Deleted event id: {eventId}";
+ break;
+ case actionResponseDeleteOne:
+ deletionConfirmed = await Calendars.DeleteCalendarEventInstanceByDate(eventId, CalendarId.Text, calendarEvent.StartDate);
+ deletionMessage = $"Deleted instance of event id: {eventId}";
+ break;
+ case actionResponseDeleteForward:
+ deletionConfirmed = await Calendars.SetEventRecurrenceEndDate(eventId, calendarEvent.StartDate.AddDays(-1));
+ deletionMessage = $"Deleted all future instances of event id: {eventId}";
+ break;
+ }
+
+ if (deletionConfirmed)
+ {
+ await DisplayAlert(actionTitleInfo, deletionMessage, actionResponseOk);
+ await Navigation.PopAsync();
+ }
+ else
+ {
+ await DisplayAlert(actionTitleInfo, "Unable to delete event: " + eventId, actionResponseOk);
+ }
+ }
+ else if (await Calendars.DeleteCalendarEventById(eventId, CalendarId.Text))
+ {
+ await DisplayAlert(actionTitleInfo, "Deleted event id: " + eventId, actionResponseOk);
+ await Navigation.PopAsync();
+ }
+ }
+ }
+
+ async void OnAddAttendeeButtonClicked(object sender, EventArgs e)
+ {
+ var modal = new CalendarEventAttendeeAddPage();
+
+ if (!(EventId.Text is string eventId) || string.IsNullOrEmpty(eventId) || !(EventName.Text is string eventName) || string.IsNullOrEmpty(eventName))
+ return;
+
+ modal.BindingContext = new CalendarEventAddAttendeeViewModel(eventId, eventName);
+ await Navigation.PushAsync(modal);
+ }
+
+ async void OnRemoveAttendeeFromEventButtonClicked(object sender, EventArgs e)
+ {
+ if (!(sender is Button btn) || !(EventId.Text is string eventId) || string.IsNullOrEmpty(eventId))
+ return;
+
+ var attendee = btn?.BindingContext as CalendarEventAttendee;
+
+ if (attendee is CalendarEventAttendee)
+ {
+ var success = await Calendars.RemoveAttendeeFromEvent(attendee, eventId);
+
+ if (success)
+ {
+ var lst = ViewModel.Attendees.ToList();
+ var attendeeToRemove = lst.FirstOrDefault(x => x.Email == attendee.Email && x.Name == attendee.Name);
+ if (attendeeToRemove != null)
+ {
+ lst.Remove(attendeeToRemove);
+ }
+ BindingContext = new CalendarEvent()
+ {
+ AllDay = ViewModel.AllDay,
+ Attendees = lst,
+ CalendarId = ViewModel.CalendarId,
+ Description = ViewModel.Description,
+ Duration = ViewModel.Duration,
+ EndDate = ViewModel.EndDate,
+ Id = ViewModel.Id,
+ Location = ViewModel.Location,
+ StartDate = ViewModel.StartDate,
+ Title = ViewModel.Title
+ };
+ }
+ }
+ }
+
+ void OnRemoveReminderFromEventButtonClicked(object sender, EventArgs e)
+ {
+ if (!(sender is Button btn) || !(EventId.Text is string eventId) || string.IsNullOrEmpty(eventId))
+ return;
+
+ var attendee = btn?.BindingContext as CalendarEventReminder;
+ }
+
+ async void OnEditEventButtonClicked(object sender, EventArgs e)
+ {
+ var modal = new CalendarEventAddPage();
+
+ var calendarName = (await Calendars.GetCalendarsAsync()).FirstOrDefault(x => x.Id == ViewModel.CalendarId)?.Name;
+
+ modal.BindingContext = new CalendarEventAddViewModel(ViewModel.CalendarId, calendarName, ViewModel);
+ await Navigation.PushAsync(modal);
+ }
}
}
diff --git a/Samples/Samples/View/CalendarPage.xaml b/Samples/Samples/View/CalendarPage.xaml
index 708487c74..950587398 100644
--- a/Samples/Samples/View/CalendarPage.xaml
+++ b/Samples/Samples/View/CalendarPage.xaml
@@ -1,83 +1,180 @@
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ SelectedItem="{Binding SelectedCalendar, Mode=TwoWay}"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Samples/Samples/View/CalendarPage.xaml.cs b/Samples/Samples/View/CalendarPage.xaml.cs
index 96c9ca592..96aef679c 100644
--- a/Samples/Samples/View/CalendarPage.xaml.cs
+++ b/Samples/Samples/View/CalendarPage.xaml.cs
@@ -1,4 +1,8 @@
-using Xamarin.Essentials;
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using Samples.ViewModel;
+using Xamarin.Essentials;
using Xamarin.Forms;
namespace Samples.View
@@ -10,19 +14,37 @@ public CalendarPage()
InitializeComponent();
}
+ async void OnAddCalendarButtonClicked(object sender, EventArgs e)
+ {
+ var modal = new CalendarAddPage();
+
+ modal.BindingContext = new CalendarAddViewModel();
+ await Navigation.PushAsync(modal);
+ }
+
+ async void OnAddEventButtonClicked(object sender, EventArgs e)
+ {
+ var modal = new CalendarEventAddPage();
+
+ if (!(SelectedCalendar.SelectedItem is Calendar calendar) || string.IsNullOrEmpty(calendar.Id))
+ return;
+
+ modal.BindingContext = new CalendarEventAddViewModel(calendar.Id, calendar.Name);
+ await Navigation.PushAsync(modal);
+ }
+
async void OnEventTapped(object sender, ItemTappedEventArgs e)
{
- if (e.Item is CalendarEvent evt)
- {
- var calendarEvent = await Calendars.GetEventAsync(evt.Id);
+ if (e.Item == null || !(e.Item is CalendarEvent calendarEvent))
+ return;
- var page = new CalendarEventPage
- {
- BindingContext = calendarEvent
- };
+ calendarEvent = await Calendars.GetEventInstanceByIdAsync(calendarEvent.Id, calendarEvent.StartDate);
- await Navigation.PushAsync(page);
- }
+ var modal = new CalendarEventPage
+ {
+ BindingContext = calendarEvent
+ };
+ await Navigation.PushAsync(modal);
}
}
}
diff --git a/Samples/Samples/ViewModel/CalendarAddViewModel.cs b/Samples/Samples/ViewModel/CalendarAddViewModel.cs
new file mode 100644
index 000000000..3791fea97
--- /dev/null
+++ b/Samples/Samples/ViewModel/CalendarAddViewModel.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows.Input;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.ViewModel
+{
+ public class CalendarAddViewModel : BaseViewModel
+ {
+ public CalendarAddViewModel()
+ {
+ CreateCalendar = new Command(CreateCalendarCommand);
+ }
+
+ public string CalendarId { get; set; }
+
+ string calendarName;
+
+ public string CalendarName
+ {
+ get => calendarName;
+ set
+ {
+ if (SetProperty(ref calendarName, value))
+ {
+ OnPropertyChanged(nameof(CanCreateCalendar));
+ }
+ }
+ }
+
+ public bool CanCreateCalendar => !string.IsNullOrWhiteSpace(CalendarName);
+
+ public ICommand CreateCalendar { get; }
+
+ async void CreateCalendarCommand()
+ {
+ var newCalendar = new Calendar()
+ {
+ Name = CalendarName
+ };
+
+ var calendarId = await Calendars.CreateCalendar(newCalendar);
+
+ await DisplayAlertAsync("Created calendar id: " + calendarId);
+ }
+ }
+}
diff --git a/Samples/Samples/ViewModel/CalendarEventAddAttendeeViewModel.cs b/Samples/Samples/ViewModel/CalendarEventAddAttendeeViewModel.cs
new file mode 100644
index 000000000..2f3ba3b2b
--- /dev/null
+++ b/Samples/Samples/ViewModel/CalendarEventAddAttendeeViewModel.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Net.Mail;
+using System.Windows.Input;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.ViewModel
+{
+ public class CalendarEventAddAttendeeViewModel : BaseViewModel
+ {
+ public CalendarEventAddAttendeeViewModel(string eventId, string eventName)
+ {
+ EventId = eventId;
+ EventName = eventName;
+ CreateCalendarEventAttendee = new Command(CreateCalendarEventAttendeeCommand);
+ }
+
+ public string EventName { get; set; }
+
+ public string EventId { get; set; }
+
+ string name;
+
+ public string Name
+ {
+ get => name;
+ set
+ {
+ if (SetProperty(ref name, value))
+ {
+ OnPropertyChanged(nameof(CanCreateAttendee));
+ }
+ }
+ }
+
+ string emailAddress;
+
+ public string EmailAddress
+ {
+ get => emailAddress;
+ set
+ {
+ if (SetProperty(ref emailAddress, value))
+ {
+ OnPropertyChanged(nameof(CanCreateAttendee));
+ }
+ }
+ }
+
+ public List AttendeeTypes { get; } = new List()
+ {
+ AttendeeType.Optional.ToString(),
+ AttendeeType.Required.ToString(),
+ AttendeeType.Resource.ToString(),
+ };
+
+ string selectedAttendeeType = AttendeeType.Optional.ToString();
+
+ public string SelectedAttendeeType
+ {
+ get => selectedAttendeeType;
+ set => SetProperty(ref selectedAttendeeType, value);
+ }
+
+ public bool IsValidEmail(string emailaddress)
+ {
+ try
+ {
+ return EmailAddress == new MailAddress(emailAddress).Address;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public bool CanCreateAttendee => IsValidEmail(EmailAddress) && !string.IsNullOrWhiteSpace(Name);
+
+ public ICommand CreateCalendarEventAttendee { get; }
+
+ async void CreateCalendarEventAttendeeCommand()
+ {
+ var newAttendee = new CalendarEventAttendee()
+ {
+ Name = Name,
+ Email = EmailAddress,
+ Type = (AttendeeType)Enum.Parse(typeof(AttendeeType), SelectedAttendeeType)
+ };
+
+ var result = await Calendars.AddAttendeeToEvent(newAttendee, EventId);
+
+ await DisplayAlertAsync("Added event attendee: " + newAttendee.Name);
+ }
+ }
+}
diff --git a/Samples/Samples/ViewModel/CalendarEventAddViewModel.cs b/Samples/Samples/ViewModel/CalendarEventAddViewModel.cs
new file mode 100644
index 000000000..b1a407cb6
--- /dev/null
+++ b/Samples/Samples/ViewModel/CalendarEventAddViewModel.cs
@@ -0,0 +1,615 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using System.Windows.Input;
+using Xamarin.Essentials;
+using Xamarin.Forms;
+
+namespace Samples.ViewModel
+{
+ public static class RecurrenceEndType
+ {
+ public const string AfterOccurences = "After a set number of times";
+ public const string Indefinitely = "Indefinitely";
+ public const string UntilEndDate = "Continues until a specified date";
+ }
+
+ public class CalendarDayOfWeekSwitch : INotifyPropertyChanged
+ {
+ bool isChecked;
+
+ public CalendarDayOfWeekSwitch(CalendarDayOfWeek val, PropertyChangedEventHandler propertyChangedCallBack)
+ {
+ Day = val;
+ PropertyChanged += propertyChangedCallBack;
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public CalendarDayOfWeek Day { get; set; }
+
+ public bool IsChecked
+ {
+ get => isChecked;
+ set
+ {
+ if (value != isChecked)
+ {
+ isChecked = value;
+ OnPropertyChanged(nameof(IsChecked));
+ }
+ }
+ }
+
+ public override string ToString() => Day.ToString();
+
+ protected void OnPropertyChanged([CallerMemberName]string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ public class CalendarEventAddViewModel : BaseViewModel
+ {
+ static TimeSpan RoundToNearestMinutes(TimeSpan input, int minutes)
+ {
+ var totalMinutes = (int)(input + new TimeSpan(0, minutes / 2, 0)).TotalMinutes;
+
+ return new TimeSpan(0, totalMinutes - (totalMinutes % minutes), 0);
+ }
+
+ bool allDay;
+ string description;
+ DateTime endDate = DateTime.Now.Date;
+ TimeSpan endTime = RoundToNearestMinutes(DateTime.Now.AddHours(2).TimeOfDay, 30);
+ string eventLocation;
+ string eventTitle;
+ bool isMonthDaySpecific = true;
+ DateTime? recurrenceEndDate;
+ uint? recurrenceEndInterval;
+ uint recurrenceInterval = 1;
+ CalendarDayOfWeek? selectedMonthWeekRecurrenceDay = null;
+ CalendarDayOfWeek selectedRecurrenceDay;
+ string selectedRecurrenceEndType = "Indefinitely";
+ uint selectedRecurrenceMonthDay = 1;
+ IterationOffset? selectedRecurrenceMonthWeek = null;
+ RecurrenceFrequency? selectedRecurrenceType = null;
+ MonthOfYear selectedRecurrenceYearlyMonth = MonthOfYear.January;
+ DateTime startDate = DateTime.Now.Date;
+ TimeSpan startTime = RoundToNearestMinutes(DateTime.Now.AddHours(1).TimeOfDay, 30);
+ string url;
+
+ public CalendarEventAddViewModel(string calendarId, string calendarName, CalendarEvent existingEvent = null)
+ {
+ CalendarId = calendarId;
+ CalendarName = calendarName;
+ RecurrenceDays = new ObservableCollection()
+ {
+ new CalendarDayOfWeekSwitch(CalendarDayOfWeek.Sunday, OnChildCheckBoxChangedEvent),
+ new CalendarDayOfWeekSwitch(CalendarDayOfWeek.Monday, OnChildCheckBoxChangedEvent),
+ new CalendarDayOfWeekSwitch(CalendarDayOfWeek.Tuesday, OnChildCheckBoxChangedEvent),
+ new CalendarDayOfWeekSwitch(CalendarDayOfWeek.Wednesday, OnChildCheckBoxChangedEvent),
+ new CalendarDayOfWeekSwitch(CalendarDayOfWeek.Thursday, OnChildCheckBoxChangedEvent),
+ new CalendarDayOfWeekSwitch(CalendarDayOfWeek.Friday, OnChildCheckBoxChangedEvent),
+ new CalendarDayOfWeekSwitch(CalendarDayOfWeek.Saturday, OnChildCheckBoxChangedEvent)
+ };
+
+ if (existingEvent != null)
+ {
+ EventId = existingEvent.Id;
+ EventTitle = existingEvent.Title;
+ Description = existingEvent.Description;
+ EventLocation = existingEvent.Location;
+ Url = existingEvent.Url;
+ AllDay = existingEvent.AllDay;
+ StartDate = existingEvent.StartDate.Date;
+ EndDate = existingEvent.EndDate?.Date ?? existingEvent.StartDate.Date;
+ StartTime = existingEvent.StartDate.TimeOfDay;
+ EndTime = existingEvent.EndDate?.TimeOfDay ?? existingEvent.StartDate.TimeOfDay;
+ if (existingEvent.RecurrancePattern != null)
+ {
+ SelectedRecurrenceType = existingEvent.RecurrancePattern.Frequency;
+ RecurrenceInterval = existingEvent.RecurrancePattern.Interval;
+
+ var selectedDays = existingEvent.RecurrancePattern.DaysOfTheWeek != null && existingEvent.RecurrancePattern.DaysOfTheWeek.Any() ? new ObservableCollection(existingEvent.RecurrancePattern.DaysOfTheWeek.ConvertAll(x => new CalendarDayOfWeekSwitch(x, OnChildCheckBoxChangedEvent)).ToList()) : new ObservableCollection();
+ foreach (var r in selectedDays)
+ {
+ var recurrenceDay = RecurrenceDays.FirstOrDefault(x => x.Day == r.Day);
+ if (recurrenceDay != null)
+ recurrenceDay.IsChecked = true;
+ }
+ switch (existingEvent.RecurrancePattern.Frequency)
+ {
+ case RecurrenceFrequency.MonthlyOnDay:
+ case RecurrenceFrequency.YearlyOnDay:
+ SelectedRecurrenceType = SelectedRecurrenceType == RecurrenceFrequency.MonthlyOnDay ? RecurrenceFrequency.Monthly : RecurrenceFrequency.Yearly;
+ IsMonthDaySpecific = false;
+ SelectedRecurrenceMonthWeek = existingEvent.RecurrancePattern.WeekOfMonth;
+ SelectedMonthWeekRecurrenceDay = existingEvent.RecurrancePattern.DaysOfTheWeek.FirstOrDefault();
+ break;
+ case RecurrenceFrequency.Monthly:
+ case RecurrenceFrequency.Yearly:
+ SelectedRecurrenceMonthDay = existingEvent.RecurrancePattern.DayOfTheMonth;
+ break;
+ }
+ if (existingEvent.RecurrancePattern.MonthOfTheYear.HasValue)
+ {
+ SelectedRecurrenceYearlyMonth = existingEvent.RecurrancePattern.MonthOfTheYear.Value;
+ }
+ if (existingEvent.RecurrancePattern.EndDate.HasValue)
+ {
+ RecurrenceEndDate = existingEvent.RecurrancePattern.EndDate.Value.DateTime;
+ SelectedRecurrenceEndType = RecurrenceEndType.UntilEndDate;
+ }
+ else if (existingEvent.RecurrancePattern.TotalOccurrences.HasValue)
+ {
+ RecurrenceEndInterval = existingEvent.RecurrancePattern.TotalOccurrences.Value;
+ SelectedRecurrenceEndType = RecurrenceEndType.AfterOccurences;
+ }
+ else
+ {
+ RecurrenceEndDate = null;
+ SelectedRecurrenceEndType = RecurrenceEndType.Indefinitely;
+ }
+ }
+ }
+ CreateOrUpdateEvent = new Command(CreateOrUpdateCalendarEvent);
+ }
+
+ public bool AllDay
+ {
+ get => allDay;
+ set
+ {
+ if (SetProperty(ref allDay, value))
+ {
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ OnPropertyChanged(nameof(DisplayTimeInformation));
+ }
+ }
+ }
+
+ public string CalendarId { get; set; }
+
+ public string CalendarName { get; set; }
+
+ public bool CanAlterRecurrence => SelectedRecurrenceType != null;
+
+ public bool CanCreateOrUpdateEvent => !string.IsNullOrWhiteSpace(EventTitle)
+ && ((EndDate.Date == StartDate.Date && (EndTime > StartTime || AllDay)) || EndDate.Date > StartDate.Date)
+ && (!CanAlterRecurrence || StartDate < RecurrenceEndDate || SelectedRecurrenceEndType == RecurrenceEndType.Indefinitely || (RecurrenceEndInterval.HasValue && RecurrenceEndInterval.Value > 0))
+ && IsValidUrl(Url);
+
+ public ICommand CreateOrUpdateEvent { get; }
+
+ public string Description
+ {
+ get => description;
+ set => SetProperty(ref description, value);
+ }
+
+ public bool DisplayTimeInformation => !AllDay && !CanAlterRecurrence;
+
+ public DateTime EndDate
+ {
+ get => endDate;
+ set
+ {
+ if (SetProperty(ref endDate, value))
+ {
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ }
+ }
+ }
+
+ public TimeSpan EndTime
+ {
+ get => endTime;
+ set
+ {
+ if (SetProperty(ref endTime, value))
+ {
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ }
+ }
+ }
+
+ public string EventActionText => string.IsNullOrEmpty(EventId) ? "Add Event" : "Update Event";
+
+ public string EventId { get; set; }
+
+ public string EventLocation
+ {
+ get => eventLocation;
+ set => SetProperty(ref eventLocation, value);
+ }
+
+ public string EventTitle
+ {
+ get => eventTitle;
+ set
+ {
+ if (SetProperty(ref eventTitle, value))
+ {
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ }
+ }
+ }
+
+ public bool IsMonthDaySpecific
+ {
+ get => isMonthDaySpecific;
+ set
+ {
+ if (SetProperty(ref isMonthDaySpecific, value))
+ {
+ if (value)
+ {
+ SelectedRecurrenceMonthDay = 1;
+ SelectedRecurrenceMonthWeek = null;
+ SelectedMonthWeekRecurrenceDay = null;
+ }
+ else
+ {
+ SelectedRecurrenceMonthDay = 0;
+ SelectedRecurrenceMonthWeek = IterationOffset.First;
+ SelectedMonthWeekRecurrenceDay = CalendarDayOfWeek.Monday;
+ }
+ }
+ }
+ }
+
+ public List MonthWeekRecurrenceDay { get; set; } = new List()
+ {
+ CalendarDayOfWeek.Monday,
+ CalendarDayOfWeek.Tuesday,
+ CalendarDayOfWeek.Wednesday,
+ CalendarDayOfWeek.Thursday,
+ CalendarDayOfWeek.Friday,
+ CalendarDayOfWeek.Saturday,
+ CalendarDayOfWeek.Sunday
+ };
+
+ public ObservableCollection RecurrenceDays { get; }
+
+ public DateTime? RecurrenceEndDate
+ {
+ get => recurrenceEndDate;
+ set
+ {
+ if (SetProperty(ref recurrenceEndDate, value))
+ {
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ }
+ }
+ }
+
+ public uint? RecurrenceEndInterval
+ {
+ get => recurrenceEndInterval;
+ set
+ {
+ if (SetProperty(ref recurrenceEndInterval, value))
+ {
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ }
+ }
+ }
+
+ public ObservableCollection RecurrenceEndTypes { get; } = new ObservableCollection()
+ {
+ RecurrenceEndType.Indefinitely,
+ RecurrenceEndType.AfterOccurences,
+ RecurrenceEndType.UntilEndDate
+ };
+
+ public uint RecurrenceInterval
+ {
+ get => recurrenceInterval;
+ set => SetProperty(ref recurrenceInterval, value);
+ }
+
+ public ObservableCollection RecurrenceMonthDay { get; set; } = new ObservableCollection()
+ {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31
+ };
+
+ public List RecurrenceMonthWeek { get; } = new List()
+ {
+ IterationOffset.First,
+ IterationOffset.Second,
+ IterationOffset.Third,
+ IterationOffset.Fourth,
+ IterationOffset.Last
+ };
+
+ public List RecurrenceTypes { get; } = new List()
+ {
+ RecurrenceFrequency.Daily,
+ RecurrenceFrequency.Weekly,
+ RecurrenceFrequency.Monthly,
+ RecurrenceFrequency.Yearly
+ };
+
+ public List RecurrenceYearlyMonth { get; } = new List()
+ {
+ MonthOfYear.January,
+ MonthOfYear.February,
+ MonthOfYear.March,
+ MonthOfYear.April,
+ MonthOfYear.May,
+ MonthOfYear.June,
+ MonthOfYear.July,
+ MonthOfYear.August,
+ MonthOfYear.September,
+ MonthOfYear.October,
+ MonthOfYear.November,
+ MonthOfYear.December
+ };
+
+ public CalendarDayOfWeek? SelectedMonthWeekRecurrenceDay
+ {
+ get => selectedMonthWeekRecurrenceDay;
+ set => SetProperty(ref selectedMonthWeekRecurrenceDay, value);
+ }
+
+ public CalendarDayOfWeek SelectedRecurrenceDay
+ {
+ get => selectedRecurrenceDay;
+ set
+ {
+ if (SetProperty(ref selectedRecurrenceDay, value))
+ {
+ SetCheckBoxes(value);
+ }
+ }
+ }
+
+ public string SelectedRecurrenceEndType
+ {
+ get => selectedRecurrenceEndType;
+
+ set
+ {
+ if (SetProperty(ref selectedRecurrenceEndType, value) && selectedRecurrenceEndType != null)
+ {
+ switch (value)
+ {
+ case RecurrenceEndType.Indefinitely:
+ RecurrenceEndDate = null;
+ RecurrenceEndInterval = null;
+ break;
+ case RecurrenceEndType.AfterOccurences:
+ RecurrenceEndDate = null;
+ RecurrenceEndInterval = !RecurrenceEndInterval.HasValue ? 1 : RecurrenceEndInterval;
+ break;
+ case RecurrenceEndType.UntilEndDate:
+ RecurrenceEndInterval = null;
+ RecurrenceEndDate = !RecurrenceEndDate.HasValue ? DateTime.Now.AddMonths(6) : RecurrenceEndDate;
+ break;
+ default:
+ RecurrenceEndDate = null;
+ RecurrenceEndInterval = null;
+ break;
+ }
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ }
+ }
+ }
+
+ public uint SelectedRecurrenceMonthDay
+ {
+ get => selectedRecurrenceMonthDay;
+ set
+ {
+ if (SetProperty(ref selectedRecurrenceMonthDay, value))
+ {
+ if (value != 0)
+ {
+ IsMonthDaySpecific = true;
+ }
+ }
+ }
+ }
+
+ public IterationOffset? SelectedRecurrenceMonthWeek
+ {
+ get => selectedRecurrenceMonthWeek;
+ set => SetProperty(ref selectedRecurrenceMonthWeek, value);
+ }
+
+ public RecurrenceFrequency? SelectedRecurrenceType
+ {
+ get => selectedRecurrenceType;
+
+ set
+ {
+ if (SetProperty(ref selectedRecurrenceType, value))
+ {
+ OnPropertyChanged(nameof(CanAlterRecurrence));
+ OnPropertyChanged(nameof(DisplayTimeInformation));
+ OnPropertyChanged(nameof(SelectedRecurrenceTypeDisplay));
+ }
+ }
+ }
+
+ public string SelectedRecurrenceTypeDisplay => SelectedRecurrenceType.ToString().Replace("ily", "yly").Replace("ly", "(s)");
+
+ public MonthOfYear SelectedRecurrenceYearlyMonth
+ {
+ get => selectedRecurrenceYearlyMonth;
+ set
+ {
+ if (SetProperty(ref selectedRecurrenceYearlyMonth, value))
+ {
+ var days = Enumerable.Range(1, DateTime.DaysInMonth(StartDate.Year, (int)value)).Select(x => (uint)x).ToList();
+ RecurrenceMonthDay.Clear();
+ days.ForEach(x => RecurrenceMonthDay.Add(x));
+ }
+ }
+ }
+
+ public DateTime StartDate
+ {
+ get => startDate;
+ set
+ {
+ if (SetProperty(ref startDate, value))
+ {
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ }
+ }
+ }
+
+ public TimeSpan StartTime
+ {
+ get => startTime;
+ set
+ {
+ if (SetProperty(ref startTime, value))
+ {
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ }
+ }
+ }
+
+ public string Url
+ {
+ get => url;
+ set
+ {
+ if (SetProperty(ref url, value))
+ {
+ OnPropertyChanged(nameof(CanCreateOrUpdateEvent));
+ }
+ }
+ }
+
+ bool IsUpdatingCheckBoxGroup { get; set; } = false;
+
+ public bool IsValidUrl(string url)
+ {
+ if (string.IsNullOrEmpty(url))
+ {
+ return true;
+ }
+ else if (!Regex.IsMatch(url, @"^https?:\/\/", RegexOptions.IgnoreCase))
+ {
+ url = "http://" + url;
+ Url = url;
+ }
+
+ if (Uri.TryCreate(url, UriKind.Absolute, out var uriResult))
+ {
+ return uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps;
+ }
+
+ return false;
+ }
+
+ async void CreateOrUpdateCalendarEvent()
+ {
+ var startDto = new DateTimeOffset(StartDate + StartTime);
+ var endDto = new DateTimeOffset(EndDate + EndTime);
+ var newEvent = new CalendarEvent()
+ {
+ Id = EventId,
+ CalendarId = CalendarId,
+ Title = EventTitle,
+ AllDay = AllDay,
+ Description = Description,
+ Location = EventLocation,
+ Url = url,
+ StartDate = startDto,
+ EndDate = !AllDay ? !CanAlterRecurrence ? (DateTimeOffset?)endDto : new DateTimeOffset(StartDate + EndTime) : null
+ };
+
+ if (CanAlterRecurrence)
+ {
+ List daysOfTheWeek = null;
+ var selectedFrequency = SelectedRecurrenceType;
+ IterationOffset? dayIterationOffset = null;
+ switch (selectedFrequency)
+ {
+ case RecurrenceFrequency.Daily:
+ case RecurrenceFrequency.Weekly:
+ daysOfTheWeek = RecurrenceDays.Where(x => x.IsChecked).ToList().ConvertAll(x => x.Day);
+ break;
+
+ default:
+ if (SelectedMonthWeekRecurrenceDay.HasValue)
+ {
+ daysOfTheWeek = new List()
+ {
+ SelectedMonthWeekRecurrenceDay.Value
+ };
+ }
+
+ if (SelectedRecurrenceMonthWeek != null)
+ {
+ dayIterationOffset = SelectedRecurrenceMonthWeek;
+ }
+ break;
+ }
+
+ newEvent.RecurrancePattern = new RecurrenceRule()
+ {
+ Frequency = selectedFrequency,
+ Interval = RecurrenceInterval,
+ DaysOfTheWeek = daysOfTheWeek,
+ DayOfTheMonth = SelectedRecurrenceMonthDay,
+ WeekOfMonth = dayIterationOffset,
+ MonthOfTheYear = SelectedRecurrenceYearlyMonth,
+ EndDate = RecurrenceEndDate,
+ TotalOccurrences = RecurrenceEndInterval
+ };
+ }
+
+ if (string.IsNullOrEmpty(EventId))
+ {
+ var eventId = await Calendars.CreateCalendarEvent(newEvent);
+ await DisplayAlertAsync("Created event id: " + eventId);
+ }
+ else
+ {
+ if (await Calendars.UpdateCalendarEvent(newEvent))
+ {
+ await DisplayAlertAsync("Updated event id: " + newEvent.Id);
+ }
+ }
+ }
+
+ void OnChildCheckBoxChangedEvent(object sender, PropertyChangedEventArgs e)
+ {
+ if (sender == null)
+ {
+ return;
+ }
+ if (!(sender is CalendarDayOfWeekSwitch calendarDayOfWeek))
+ {
+ return;
+ }
+ if (!IsUpdatingCheckBoxGroup)
+ {
+ SelectedRecurrenceDay = (CalendarDayOfWeek)RecurrenceDays.Where(x => x.IsChecked).Sum(x => (int)x.Day);
+ }
+ }
+
+ void SetCheckBoxes(CalendarDayOfWeek bitFlagValue)
+ {
+ try
+ {
+ IsUpdatingCheckBoxGroup = true;
+ foreach (var day in RecurrenceDays)
+ {
+ day.IsChecked = bitFlagValue.HasFlag(day.Day);
+ }
+ }
+ finally
+ {
+ IsUpdatingCheckBoxGroup = false;
+ }
+ }
+ }
+}
diff --git a/Samples/Samples/ViewModel/CalendarViewModel.cs b/Samples/Samples/ViewModel/CalendarViewModel.cs
index ee35dffbc..ea38c32e0 100644
--- a/Samples/Samples/ViewModel/CalendarViewModel.cs
+++ b/Samples/Samples/ViewModel/CalendarViewModel.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.ObjectModel;
+using System.Linq;
using System.Windows.Input;
using Xamarin.Essentials;
using Xamarin.Forms;
@@ -8,125 +9,186 @@ namespace Samples.ViewModel
{
public class CalendarViewModel : BaseViewModel
{
- bool hasAppeared;
+ const int endDateDaysToOffset = 14;
- Calendar selectedCalendar;
-
- bool useStartDateTime;
- DateTime startDate;
- TimeSpan startTime;
+ const string endOfDay = "23:59";
- bool useEndDateTime;
- DateTime endDate;
- TimeSpan endTime;
+ bool alreadyAppeared;
public CalendarViewModel()
{
- startDate = DateTime.Now.Date; // from today
- endDate = startDate + TimeSpan.FromDays(14); // to 14 days from now
+ GetCalendars = new Command(OnClickGetCalendars);
+ StartDateSelectedCommand = new Command(OnStartDateSelected);
+ StartTimeSelectedCommand = new Command(OnStartTimeSelected);
+ EndDateSelectedCommand = new Command(OnEndDateSelected);
+ EndTimeSelectedCommand = new Command(OnEndTimeSelected);
+ StartDateEnabledCheckBoxChanged = new Command(OnStartCheckboxChanged);
+ EndDateEnabledCheckBoxChanged = new Command(OnEndCheckboxChanged);
+ }
- startTime = DateTime.Now.TimeOfDay; // from now
- endTime = TimeSpan.FromDays(1) - TimeSpan.FromSeconds(1); // to the end of the day
+ public override void OnAppearing()
+ {
+ base.OnAppearing();
+
+ if (!alreadyAppeared)
+ {
+ alreadyAppeared = true;
- RefreshCalendarsCommand = new Command(OnRefreshCalendars);
+ RefreshCalendars();
+ }
}
- public ICommand RefreshCalendarsCommand { get; }
+ Calendar selectedCalendar;
- public ObservableCollection CalendarList { get; } = new ObservableCollection();
+ bool startdatePickersEnabled;
- public ObservableCollection EventList { get; } = new ObservableCollection();
+ bool enddatePickersEnabled;
- public Calendar SelectedCalendar
+ public bool StartDatePickersEnabled
{
- get => selectedCalendar;
- set => SetProperty(ref selectedCalendar, value, onChanged: OnRefreshEvents);
+ get => startdatePickersEnabled;
+ set => SetProperty(ref startdatePickersEnabled, value);
}
- public bool UseStartDateTime
+ public bool EndDatePickersEnabled
{
- get => useStartDateTime;
- set => SetProperty(ref useStartDateTime, value, onChanged: OnRefreshEvents);
+ get => enddatePickersEnabled;
+ set => SetProperty(ref enddatePickersEnabled, value);
}
- public bool UseEndDateTime
+ public bool CanCreateCalendarEvent => !string.IsNullOrWhiteSpace(SelectedCalendar?.Name) && SelectedCalendar?.Name != "All" && !SelectedCalendar.IsReadOnly;
+
+ public ICommand GetCalendars { get; }
+
+ public ICommand StartDateEnabledCheckBoxChanged { get; }
+
+ public ICommand EndDateEnabledCheckBoxChanged { get; }
+
+ public ICommand StartDateSelectedCommand { get; }
+
+ public ICommand StartTimeSelectedCommand { get; }
+
+ public ICommand EndDateSelectedCommand { get; }
+
+ public ICommand EndTimeSelectedCommand { get; }
+
+ public DateTime StartDate { get; set; } = DateTime.Now;
+
+ public TimeSpan StartTime { get; set; }
+
+ public DateTime EndDate { get; set; } = DateTime.Now.AddDays(endDateDaysToOffset);
+
+ public TimeSpan EndTime { get; set; } = TimeSpan.Parse(endOfDay);
+
+ public bool HasCalendarReadAccess { get; set; }
+
+ public ObservableCollection CalendarList { get; } = new ObservableCollection();
+
+ public ObservableCollection EventList { get; } = new ObservableCollection();
+
+ public Calendar SelectedCalendar
{
- get => useEndDateTime;
- set => SetProperty(ref useEndDateTime, value, onChanged: OnRefreshEvents);
+ get => selectedCalendar;
+
+ set
+ {
+ if (SetProperty(ref selectedCalendar, value) && selectedCalendar != null)
+ {
+ OnChangeRequestCalendarSpecificEvents(selectedCalendar.Id);
+ OnPropertyChanged(nameof(CanCreateCalendarEvent));
+ }
+ }
}
- public DateTime StartDate
+ void OnStartCheckboxChanged(object parameter)
{
- get => startDate;
- set => SetProperty(ref startDate, value, onChanged: OnRefreshEvents);
+ if (parameter is bool b)
+ {
+ StartDatePickersEnabled = b;
+
+ RefreshEventList(SelectedCalendar?.Id);
+ }
}
- public TimeSpan StartTime
+ void OnEndCheckboxChanged(object parameter)
{
- get => startTime;
- set => SetProperty(ref startTime, value, onChanged: OnRefreshEvents);
+ if (parameter is bool b)
+ {
+ EndDatePickersEnabled = b;
+
+ RefreshEventList(SelectedCalendar?.Id);
+ }
}
- public DateTime EndDate
+ public void RefreshCalendars() => OnClickGetCalendars();
+
+ async void OnClickGetCalendars()
{
- get => endDate;
- set => SetProperty(ref endDate, value, onChanged: OnRefreshEvents);
+ CalendarList.Clear();
+
+ CalendarList.Add(new Calendar() { Id = null, IsReadOnly = true, Name = "All" });
+ var calendars = await Calendars.GetCalendarsAsync();
+ foreach (var calendar in calendars)
+ {
+ CalendarList.Add(calendar);
+ }
+ SelectedCalendar = CalendarList[0];
}
- public TimeSpan EndTime
+ void OnStartDateSelected(object parameter)
{
- get => endTime;
- set => SetProperty(ref endTime, value, onChanged: OnRefreshEvents);
- }
+ var startDate = parameter as DateTime?;
- public DateTimeOffset? StartDateTime =>
- UseStartDateTime
- ? (StartDate + StartTime)
- : (DateTimeOffset?)null;
+ if (!startDate.HasValue)
+ return;
- public DateTimeOffset? EndDateTime =>
- UseEndDateTime
- ? (EndDate + EndTime)
- : (DateTimeOffset?)null;
+ startDate += StartTime;
- public override void OnAppearing()
+ RefreshEventList(SelectedCalendar?.Id, startDate);
+ }
+
+ void OnStartTimeSelected(object parameter)
{
- base.OnAppearing();
+ if (parameter == null)
+ return;
- if (!hasAppeared)
- {
- OnRefreshCalendars();
- hasAppeared = true;
- }
+ RefreshEventList(SelectedCalendar?.Id);
}
- async void OnRefreshCalendars()
+ void OnEndDateSelected(object parameter)
{
- var calendars = await Calendars.GetCalendarsAsync();
+ var endDate = parameter as DateTime?;
- CalendarList.Clear();
- CalendarList.Add(new Calendar { Id = null, Name = "All" });
+ if (!endDate.HasValue)
+ return;
- foreach (var calendar in calendars)
- {
- CalendarList.Add(calendar);
- }
+ endDate += EndTime;
+ RefreshEventList(SelectedCalendar?.Id, null, endDate);
+ }
- SelectedCalendar = CalendarList[0];
+ void OnEndTimeSelected(object parameter)
+ {
+ if (parameter == null)
+ return;
+
+ RefreshEventList();
}
- async void OnRefreshEvents()
+ public void OnChangeRequestCalendarSpecificEvents(string calendarId = null, DateTime? startDateTime = null, DateTime? endDateTime = null) => RefreshEventList(calendarId, startDateTime, endDateTime);
+
+ async void RefreshEventList(string calendarId = null, DateTime? startDate = null, DateTime? endDate = null)
{
- var events = await Calendars.GetEventsAsync(
- SelectedCalendar?.Id,
- StartDateTime,
- EndDateTime);
+ startDate = StartDatePickersEnabled && !startDate.HasValue ? (DateTime?)StartDate.Date + StartTime : startDate;
+ endDate = (EndDatePickersEnabled && !endDate.HasValue) ? (DateTime?)EndDate.Date + EndTime : endDate;
+ if (!CalendarList.Any())
+ return;
EventList.Clear();
- foreach (var e in events)
+ var events = await Calendars.GetEventsAsync(calendarId, startDate, endDate);
+ foreach (var calendarEvent in events)
{
- EventList.Add(e);
+ EventList.Add(calendarEvent);
}
}
}
diff --git a/Xamarin.Essentials/Calendars/CalendarRequest.uwp.cs b/Xamarin.Essentials/Calendars/CalendarRequest.uwp.cs
new file mode 100644
index 000000000..261e86b36
--- /dev/null
+++ b/Xamarin.Essentials/Calendars/CalendarRequest.uwp.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Windows.ApplicationModel.Appointments;
+
+namespace Xamarin.Essentials
+{
+ static class CalendarRequest
+ {
+ static AppointmentStore uwpAppointmentStore;
+
+ static AppointmentStoreAccessType lastRequestType;
+
+ public static async System.Threading.Tasks.Task GetInstanceAsync(AppointmentStoreAccessType type = AppointmentStoreAccessType.AppCalendarsReadWrite)
+ {
+ if (uwpAppointmentStore == null || lastRequestType != type)
+ {
+ uwpAppointmentStore = await AppointmentManager.RequestStoreAsync(type);
+ lastRequestType = type;
+ }
+
+ return uwpAppointmentStore;
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Calendars/Calendars.android.cs b/Xamarin.Essentials/Calendars/Calendars.android.cs
index 10bbf6e0c..d1de0344e 100644
--- a/Xamarin.Essentials/Calendars/Calendars.android.cs
+++ b/Xamarin.Essentials/Calendars/Calendars.android.cs
@@ -1,64 +1,182 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using Android.Content;
using Android.Database;
using Android.Provider;
+using Java.Security;
namespace Xamarin.Essentials
{
public static partial class Calendars
{
- static Task> PlatformGetCalendarsAsync()
+ const string andCondition = "AND";
+ const string dailyFrequency = "DAILY";
+ const string weeklyFrequency = "WEEKLY";
+ const string monthlyFrequency = "MONTHLY";
+ const string yearlyFrequency = "YEARLY";
+ const string byFrequencySearch = "FREQ=";
+ const string byDaySearch = "BYDAY=";
+ const string byMonthDaySearch = "BYMONTHDAY=";
+ const string byMonthSearch = "BYMONTH=";
+ const string bySetPosSearch = "BYSETPOS=";
+ const string byIntervalSearch = "INTERVAL=";
+ const string byCountSearch = "COUNT=";
+ const string byUntilSearch = "UNTIL=";
+
+ static async Task> PlatformGetCalendarsAsync()
{
+ await Permissions.RequestAsync();
+
var calendarsUri = CalendarContract.Calendars.ContentUri;
var calendarsProjection = new List
{
CalendarContract.Calendars.InterfaceConsts.Id,
+ CalendarContract.Calendars.InterfaceConsts.CalendarAccessLevel,
CalendarContract.Calendars.InterfaceConsts.CalendarDisplayName
};
- var queryConditions =
- $"{CalendarContract.Calendars.InterfaceConsts.Deleted} != 1";
+ var queryConditions = $"{CalendarContract.Calendars.InterfaceConsts.Deleted} != 1";
- using var cur = Platform.AppContext.ApplicationContext.ContentResolver.Query(calendarsUri, calendarsProjection.ToArray(), queryConditions, null, null);
+ using (var currentContextContentResolver = Platform.AppContext.ApplicationContext.ContentResolver.Query(calendarsUri, calendarsProjection.ToArray(), queryConditions, null, null))
+ {
+ var calendars = new List();
+ while (currentContextContentResolver.MoveToNext())
+ {
+ calendars.Add(new Calendar()
+ {
+ Id = currentContextContentResolver.GetString(calendarsProjection.IndexOf(CalendarContract.Calendars.InterfaceConsts.Id)),
+ IsReadOnly = IsCalendarReadOnly((CalendarAccess)currentContextContentResolver.GetInt(calendarsProjection.IndexOf(CalendarContract.Calendars.InterfaceConsts.CalendarAccessLevel))),
+ Name = currentContextContentResolver.GetString(calendarsProjection.IndexOf(CalendarContract.Calendars.InterfaceConsts.CalendarDisplayName)),
+ });
+ }
+ return calendars;
+ }
+ }
- return Task.FromResult>(ToCalendars(cur, calendarsProjection).ToList());
+ static bool IsCalendarReadOnly(CalendarAccess accessLevel)
+ {
+ switch (accessLevel)
+ {
+ case CalendarAccess.AccessContributor:
+ case CalendarAccess.AccessRoot:
+ case CalendarAccess.AccessOwner:
+ case CalendarAccess.AccessEditor:
+ return true;
+ default:
+ return false;
+ }
}
- static Task PlatformGetCalendarAsync(string calendarId)
+ static async Task> PlatformGetEventsAsync(string calendarId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null)
{
- // Android ids are always integers
- if (!int.TryParse(calendarId, out _))
- throw InvalidCalendar(calendarId);
+ await Permissions.RequestAsync();
+
+ var sDate = startDate ?? DateTimeOffset.Now.Add(defaultStartTimeFromNow);
+ var eDate = endDate ?? sDate.Add(defaultEndTimeFromStartTime);
+
+ var eventsProjection = new List
+ {
+ CalendarContract.Instances.EventId,
+ CalendarContract.Instances.Begin,
+ CalendarContract.Instances.End,
+ CalendarContract.Events.InterfaceConsts.EventTimezone,
+ CalendarContract.Events.InterfaceConsts.EventEndTimezone,
+ CalendarContract.Events.InterfaceConsts.CalendarId,
+ CalendarContract.Events.InterfaceConsts.Title
+ };
+ var instanceUriBuilder = CalendarContract.Instances.ContentUri.BuildUpon();
+ ContentUris.AppendId(instanceUriBuilder, sDate.AddMilliseconds(sDate.Offset.TotalMilliseconds).ToUnixTimeMilliseconds());
+ ContentUris.AppendId(instanceUriBuilder, eDate.AddMilliseconds(eDate.Offset.TotalMilliseconds).ToUnixTimeMilliseconds());
+
+ var instancesUri = instanceUriBuilder.Build();
+ var calendarSpecificEvent = string.Empty;
+
+ if (!string.IsNullOrEmpty(calendarId))
+ {
+ // Android event ids are always integers
+ if (!int.TryParse(calendarId, out var resultId))
+ {
+ throw new ArgumentException($"[Android]: No Event found for event Id {calendarId}");
+ }
+ calendarSpecificEvent = $"{CalendarContract.Events.InterfaceConsts.CalendarId} = {resultId} {andCondition} ";
+ }
+ calendarSpecificEvent += $"{CalendarContract.Events.InterfaceConsts.Deleted} != 1";
+
+ var instances = new List();
+ using (var currentContextContentResolver = Platform.AppContext.ApplicationContext.ContentResolver.Query(instancesUri, eventsProjection.ToArray(), calendarSpecificEvent, null, $"{CalendarContract.Instances.Begin} ASC"))
+ {
+ while (currentContextContentResolver.MoveToNext())
+ {
+ var instanceStartTZ = TimeZoneInfo.FindSystemTimeZoneById(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventTimezone)));
+ var eventStartDate = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(currentContextContentResolver.GetLong(eventsProjection.IndexOf(CalendarContract.Instances.Begin))), instanceStartTZ);
+ var instanceEndTZ = TimeZoneInfo.FindSystemTimeZoneById(!string.IsNullOrEmpty(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventEndTimezone))) ? currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventEndTimezone)) : currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventTimezone)));
+ var eventEndDate = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(currentContextContentResolver.GetLong(eventsProjection.IndexOf(CalendarContract.Instances.End))), instanceEndTZ);
+
+ instances.Add(new CalendarEvent()
+ {
+ Id = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Instances.EventId)),
+ CalendarId = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.CalendarId)),
+ Title = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Title)),
+ StartDate = eventStartDate,
+ EndDate = eventEndDate
+ });
+ }
+ }
+ if (!instances.Any() && !string.IsNullOrEmpty(calendarId))
+ {
+ // Make sure this calendar exists by testing retrieval
+ try
+ {
+ GetCalendarById(calendarId);
+ }
+ catch (Exception)
+ {
+ throw new ArgumentOutOfRangeException($"[Android]: No calendar exists with the Id {calendarId}");
+ }
+ }
+ return instances;
+ }
+ static Calendar GetCalendarById(string calendarId)
+ {
var calendarsUri = CalendarContract.Calendars.ContentUri;
var calendarsProjection = new List
{
CalendarContract.Calendars.InterfaceConsts.Id,
CalendarContract.Calendars.InterfaceConsts.CalendarDisplayName
};
- var queryConditions =
- $"{CalendarContract.Calendars.InterfaceConsts.Deleted} != 1 AND " +
- $"{CalendarContract.Calendars.InterfaceConsts.Id} = {calendarId}";
- using var cur = Platform.AppContext.ApplicationContext.ContentResolver.Query(calendarsUri, calendarsProjection.ToArray(), queryConditions, null, null);
-
- if (cur.Count <= 0)
- throw InvalidCalendar(calendarId);
+ // Android event ids are always integers
+ if (!int.TryParse(calendarId, out var resultId))
+ {
+ throw new ArgumentException($"[Android]: No Event found for event Id {calendarId}");
+ }
- cur.MoveToNext();
+ var queryConditions = $"{CalendarContract.Calendars.InterfaceConsts.Deleted} != 1 {andCondition} {CalendarContract.Calendars.InterfaceConsts.Id} = {resultId}";
- return Task.FromResult(ToCalendar(cur, calendarsProjection));
+ using (var currentContextContentResolver = Platform.AppContext.ApplicationContext.ContentResolver.Query(calendarsUri, calendarsProjection.ToArray(), queryConditions, null, null))
+ {
+ if (currentContextContentResolver.Count > 0)
+ {
+ currentContextContentResolver.MoveToNext();
+ return new Calendar()
+ {
+ Id = currentContextContentResolver.GetString(calendarsProjection.IndexOf(CalendarContract.Calendars.InterfaceConsts.Id)),
+ Name = currentContextContentResolver.GetString(calendarsProjection.IndexOf(CalendarContract.Calendars.InterfaceConsts.CalendarDisplayName)),
+ };
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException($"[Android]: No calendar exists with the Id {calendarId}");
+ }
+ }
}
- static async Task> PlatformGetEventsAsync(string calendarId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null)
+ static async Task PlatformGetEventByIdAsync(string eventId)
{
- // Android ids are always integers
- if (!string.IsNullOrEmpty(calendarId) && !int.TryParse(calendarId, out _))
- throw InvalidCalendar(calendarId);
-
- var sDate = startDate ?? DateTimeOffset.Now.Add(defaultStartTimeFromNow);
- var eDate = endDate ?? sDate.Add(defaultEndTimeFromStartTime);
+ await Permissions.RequestAsync();
var eventsUri = CalendarContract.Events.ContentUri;
var eventsProjection = new List
@@ -68,136 +186,607 @@ static async Task> PlatformGetEventsAsync(string cale
CalendarContract.Events.InterfaceConsts.Title,
CalendarContract.Events.InterfaceConsts.Description,
CalendarContract.Events.InterfaceConsts.EventLocation,
+ CalendarContract.Events.InterfaceConsts.CustomAppUri,
CalendarContract.Events.InterfaceConsts.AllDay,
CalendarContract.Events.InterfaceConsts.Dtstart,
CalendarContract.Events.InterfaceConsts.Dtend,
- CalendarContract.Events.InterfaceConsts.Deleted
+ CalendarContract.Events.InterfaceConsts.Rrule,
+ CalendarContract.Events.InterfaceConsts.Rdate,
+ CalendarContract.Events.InterfaceConsts.Organizer,
+ CalendarContract.Events.InterfaceConsts.EventTimezone,
+ CalendarContract.Events.InterfaceConsts.EventEndTimezone,
};
- var calendarSpecificEvent =
- $"{CalendarContract.Events.InterfaceConsts.Dtend} >= {sDate.AddMilliseconds(sDate.Offset.TotalMilliseconds).ToUnixTimeMilliseconds()} AND " +
- $"{CalendarContract.Events.InterfaceConsts.Dtstart} <= {eDate.AddMilliseconds(sDate.Offset.TotalMilliseconds).ToUnixTimeMilliseconds()} AND " +
- $"{CalendarContract.Events.InterfaceConsts.Deleted} != 1 ";
- if (!string.IsNullOrEmpty(calendarId))
- calendarSpecificEvent += $" AND {CalendarContract.Events.InterfaceConsts.CalendarId} = {calendarId}";
- var sortOrder = $"{CalendarContract.Events.InterfaceConsts.Dtstart} ASC";
-
- using var cur = Platform.AppContext.ApplicationContext.ContentResolver.Query(eventsUri, eventsProjection.ToArray(), calendarSpecificEvent, null, sortOrder);
- // confirm the calendar exists if no events were found
- // the PlatformGetCalendarAsync wll throw if not
- if (cur.Count == 0 && !string.IsNullOrEmpty(calendarId))
- await PlatformGetCalendarAsync(calendarId).ConfigureAwait(false);
+ // Android event ids are always integers
+ if (!int.TryParse(eventId, out var resultId))
+ {
+ throw new ArgumentException($"[Android]: No Event found for event Id {eventId}");
+ }
- return ToEvents(cur, eventsProjection).ToList();
+ var calendarSpecificEvent = $"{CalendarContract.Events.InterfaceConsts.Id}={resultId}";
+ using (var currentContextContentResolver = Platform.AppContext.ApplicationContext.ContentResolver.Query(eventsUri, eventsProjection.ToArray(), calendarSpecificEvent, null, null))
+ {
+ if (currentContextContentResolver.Count > 0)
+ {
+ currentContextContentResolver.MoveToNext();
+ var instanceStartTZ = TimeZoneInfo.FindSystemTimeZoneById(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventTimezone)));
+ var eventStartDate = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(currentContextContentResolver.GetLong(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Dtstart))), instanceStartTZ);
+ var instanceEndTZ = TimeZoneInfo.FindSystemTimeZoneById(!string.IsNullOrEmpty(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventEndTimezone))) ? currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventEndTimezone)) : currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventTimezone)));
+ var eventEndDate = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(currentContextContentResolver.GetLong(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Dtend))), instanceEndTZ);
+
+ var eventResult = new CalendarEvent
+ {
+ Id = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Id)),
+ CalendarId = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.CalendarId)),
+ Title = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Title)),
+ Description = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Description)),
+ Location = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventLocation)),
+ Url = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.CustomAppUri)),
+ StartDate = eventStartDate,
+ EndDate = eventEndDate,
+ Attendees = GetAttendeesForEvent(eventId, currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Organizer))),
+ RecurrancePattern = !string.IsNullOrEmpty(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Rrule))) ? GetRecurranceRuleForEvent(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Rrule))) : null,
+ Reminders = GetRemindersForEvent(eventId)
+ };
+ return eventResult;
+ }
+ throw new ArgumentOutOfRangeException($"[Android]: No Event found for event Id {eventId}");
+ }
}
- static Task PlatformGetEventAsync(string eventId)
+ static async Task PlatformGetEventInstanceByIdAsync(string eventId, DateTimeOffset instance)
{
- // Android ids are always integers
- if (!string.IsNullOrEmpty(eventId) && !int.TryParse(eventId, out _))
- throw InvalidCalendar(eventId);
+ await Permissions.RequestAsync();
- var eventsUri = CalendarContract.Events.ContentUri;
var eventsProjection = new List
{
- CalendarContract.Events.InterfaceConsts.Id,
+ CalendarContract.Instances.EventId,
+ CalendarContract.Instances.Begin,
+ CalendarContract.Instances.End,
CalendarContract.Events.InterfaceConsts.CalendarId,
CalendarContract.Events.InterfaceConsts.Title,
CalendarContract.Events.InterfaceConsts.Description,
CalendarContract.Events.InterfaceConsts.EventLocation,
+ CalendarContract.Events.InterfaceConsts.CustomAppUri,
CalendarContract.Events.InterfaceConsts.AllDay,
CalendarContract.Events.InterfaceConsts.Dtstart,
- CalendarContract.Events.InterfaceConsts.Dtend
+ CalendarContract.Events.InterfaceConsts.Dtend,
+ CalendarContract.Events.InterfaceConsts.Rrule,
+ CalendarContract.Events.InterfaceConsts.Rdate,
+ CalendarContract.Events.InterfaceConsts.Organizer,
+ CalendarContract.Events.InterfaceConsts.EventTimezone,
+ CalendarContract.Events.InterfaceConsts.EventEndTimezone,
};
- var calendarSpecificEvent = $"{CalendarContract.Events.InterfaceConsts.Id} = {eventId}";
-
- using var cur = Platform.AppContext.ApplicationContext.ContentResolver.Query(eventsUri, eventsProjection.ToArray(), calendarSpecificEvent, null, null);
+ var instanceUriBuilder = CalendarContract.Instances.ContentUri.BuildUpon();
+ ContentUris.AppendId(instanceUriBuilder, instance.AddDays(-1).AddMilliseconds(instance.Offset.TotalMilliseconds).ToUnixTimeMilliseconds());
+ ContentUris.AppendId(instanceUriBuilder, instance.AddMilliseconds(instance.Offset.TotalMilliseconds).ToUnixTimeMilliseconds());
- if (cur.Count <= 0)
- throw InvalidEvent(eventId);
+ var instancesUri = instanceUriBuilder.Build();
+ var calendarSpecificEvent = $"{CalendarContract.Instances.EventId} = {eventId}";
- cur.MoveToNext();
-
- return Task.FromResult(ToEvent(cur, eventsProjection));
+ using (var currentContextContentResolver = Platform.AppContext.ApplicationContext.ContentResolver.Query(instancesUri, eventsProjection.ToArray(), calendarSpecificEvent, null, $"{CalendarContract.Instances.Begin} ASC"))
+ {
+ if (currentContextContentResolver.MoveToFirst())
+ {
+ var instanceStartTZ = TimeZoneInfo.FindSystemTimeZoneById(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventTimezone)));
+ var eventStartDate = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(currentContextContentResolver.GetLong(eventsProjection.IndexOf(CalendarContract.Instances.Begin))), instanceStartTZ);
+ var instanceEndTZ = TimeZoneInfo.FindSystemTimeZoneById(!string.IsNullOrEmpty(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventEndTimezone))) ? currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventEndTimezone)) : currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventTimezone)));
+ var eventEndDate = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeMilliseconds(currentContextContentResolver.GetLong(eventsProjection.IndexOf(CalendarContract.Instances.End))), instanceEndTZ);
+ return new CalendarEvent
+ {
+ Id = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Instances.EventId)),
+ CalendarId = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.CalendarId)),
+ Title = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Title)),
+ Description = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Description)),
+ Location = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.EventLocation)),
+ Url = currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.CustomAppUri)),
+ StartDate = eventStartDate,
+ EndDate = eventEndDate,
+ Attendees = GetAttendeesForEvent(eventId, currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Organizer))),
+ RecurrancePattern = !string.IsNullOrEmpty(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Rrule))) ? GetRecurranceRuleForEvent(currentContextContentResolver.GetString(eventsProjection.IndexOf(CalendarContract.Events.InterfaceConsts.Rrule))) : null,
+ Reminders = GetRemindersForEvent(eventId)
+ };
+ }
+ throw new ArgumentOutOfRangeException($"[Android]: No Event found for event Id {eventId}");
+ }
}
- static IEnumerable PlatformGetAttendees(string eventId)
+ static IEnumerable GetAttendeesForEvent(string eventId, string organizer)
{
- // Android ids are always integers
- if (!string.IsNullOrEmpty(eventId) && !int.TryParse(eventId, out _))
- throw InvalidCalendar(eventId);
-
var attendeesUri = CalendarContract.Attendees.ContentUri;
var attendeesProjection = new List
{
CalendarContract.Attendees.InterfaceConsts.EventId,
CalendarContract.Attendees.InterfaceConsts.AttendeeEmail,
- CalendarContract.Attendees.InterfaceConsts.AttendeeName
+ CalendarContract.Attendees.InterfaceConsts.AttendeeName,
+ CalendarContract.Attendees.InterfaceConsts.AttendeeType
+ };
+ var attendeeSpecificAttendees = $"{CalendarContract.Attendees.InterfaceConsts.EventId}={eventId}";
+ var currentContextContentResolver = Platform.AppContext.ApplicationContext.ContentResolver.Query(attendeesUri, attendeesProjection.ToArray(), attendeeSpecificAttendees, null, null);
+ var attendees = new List();
+ while (currentContextContentResolver.MoveToNext())
+ {
+ attendees.Add(new CalendarEventAttendee()
+ {
+ Name = currentContextContentResolver.GetString(attendeesProjection.IndexOf(CalendarContract.Attendees.InterfaceConsts.AttendeeName)),
+ Email = currentContextContentResolver.GetString(attendeesProjection.IndexOf(CalendarContract.Attendees.InterfaceConsts.AttendeeEmail)),
+ Type = (AttendeeType)currentContextContentResolver.GetInt(attendeesProjection.IndexOf(CalendarContract.Attendees.InterfaceConsts.AttendeeType)),
+ IsOrganizer = currentContextContentResolver.GetString(attendeesProjection.IndexOf(CalendarContract.Attendees.InterfaceConsts.AttendeeEmail)) == organizer
+ });
+ }
+ currentContextContentResolver.Dispose();
+ return attendees.OrderByDescending(x => x.IsOrganizer);
+ }
+
+ static IEnumerable GetRemindersForEvent(string eventId)
+ {
+ var remindersUri = CalendarContract.Reminders.ContentUri;
+ var remindersProjection = new List
+ {
+ CalendarContract.Reminders.InterfaceConsts.EventId,
+ CalendarContract.Reminders.InterfaceConsts.Minutes
};
- var attendeeSpecificAttendees =
- $"{CalendarContract.Attendees.InterfaceConsts.EventId}={eventId}";
+ var remindersSpecificAttendees = $"{CalendarContract.Reminders.InterfaceConsts.EventId}={eventId}";
+ var reminders = new List();
+ using (var currentContextContentResolver = Platform.AppContext.ApplicationContext.ContentResolver.Query(remindersUri, remindersProjection.ToArray(), remindersSpecificAttendees, null, null))
+ {
+ while (currentContextContentResolver.MoveToNext())
+ {
+ reminders.Add(new CalendarEventReminder()
+ {
+ MinutesPriorToEventStart = currentContextContentResolver.GetInt(remindersProjection.IndexOf(CalendarContract.Reminders.InterfaceConsts.Minutes))
+ });
+ }
+ }
+ return reminders;
+ }
+
+ static async Task PlatformCreateCalendar(Calendar newCalendar)
+ {
+ await Permissions.RequestAsync();
+
+ var calendarUri = CalendarContract.Calendars.ContentUri;
+ var currentContextContentResolver = Platform.AppContext.ApplicationContext.ContentResolver;
+ var calendarValues = new ContentValues();
+ calendarValues.Put(CalendarContract.Calendars.InterfaceConsts.AccountName, "Xamarin.Essentials.Calendar");
+ calendarValues.Put(CalendarContract.Calendars.InterfaceConsts.AccountType, CalendarContract.AccountTypeLocal);
+ calendarValues.Put(CalendarContract.Calendars.Name, newCalendar.Name);
+ calendarValues.Put(CalendarContract.Calendars.InterfaceConsts.CalendarDisplayName, newCalendar.Name);
+ calendarValues.Put(CalendarContract.Calendars.InterfaceConsts.CalendarAccessLevel, CalendarAccess.AccessOwner.ToString());
+ calendarValues.Put(CalendarContract.Calendars.InterfaceConsts.Visible, true);
+ calendarValues.Put(CalendarContract.Calendars.InterfaceConsts.SyncEvents, true);
+ calendarUri = calendarUri.BuildUpon()
+ .AppendQueryParameter(CalendarContract.CallerIsSyncadapter, "true")
+ .AppendQueryParameter(CalendarContract.Calendars.InterfaceConsts.AccountName, "Xamarin.Essentials.Calendar")
+ .AppendQueryParameter(CalendarContract.Calendars.InterfaceConsts.AccountType, CalendarContract.AccountTypeLocal)
+ .Build();
+ var result = currentContextContentResolver.Insert(calendarUri, calendarValues);
+ return result.ToString();
+ }
+
+ static async Task PlatformCreateCalendarEvent(CalendarEvent newEvent)
+ {
+ await Permissions.RequestAsync();
- using var cur = Platform.AppContext.ApplicationContext.ContentResolver.Query(attendeesUri, attendeesProjection.ToArray(), attendeeSpecificAttendees, null, null);
+ var result = 0;
+ if (string.IsNullOrEmpty(newEvent.CalendarId))
+ {
+ return string.Empty;
+ }
+ var eventUri = CalendarContract.Events.ContentUri;
+ var eventValues = SetupContentValues(newEvent);
- return ToAttendees(cur, attendeesProjection).ToList();
+ var resultUri = Platform.AppContext.ApplicationContext.ContentResolver.Insert(eventUri, eventValues);
+ if (int.TryParse(resultUri?.LastPathSegment, out result))
+ {
+ return result.ToString();
+ }
+ throw new ArgumentException("[Android]: Could not create appointment with supplied parameters");
}
- static IEnumerable ToCalendars(ICursor cur, List projection)
+ static async Task PlatformUpdateCalendarEvent(CalendarEvent eventToUpdate)
{
- while (cur.MoveToNext())
+ await Permissions.RequestAsync();
+
+ var thisEvent = await GetEventByIdAsync(eventToUpdate.Id);
+
+ var eventUri = CalendarContract.Events.ContentUri;
+ var eventValues = SetupContentValues(eventToUpdate, true);
+
+ if (string.IsNullOrEmpty(eventToUpdate.CalendarId) || thisEvent == null)
+ {
+ return false;
+ }
+ else if (thisEvent.CalendarId != eventToUpdate.CalendarId)
+ {
+ await DeleteCalendarEventById(thisEvent.Id, thisEvent.CalendarId);
+ var resultUri = Platform.AppContext.ApplicationContext.ContentResolver.Insert(eventUri, eventValues);
+ if (int.TryParse(resultUri?.LastPathSegment, out var result))
+ {
+ return true;
+ }
+
+ return false;
+ }
+ else if (Platform.AppContext.ApplicationContext.ContentResolver.Update(eventUri, eventValues, $"{CalendarContract.Attendees.InterfaceConsts.Id}={eventToUpdate.Id}", null) > 0)
{
- yield return ToCalendar(cur, projection);
+ return true;
}
+ throw new ArgumentException("[Android]: Could not update appointment with supplied parameters");
}
- static Calendar ToCalendar(ICursor cur, List projection) =>
- new Calendar
+ static async Task PlatformSetEventRecurrenceEndDate(string eventId, DateTimeOffset recurrenceEndDate)
+ {
+ await Permissions.RequestAsync();
+
+ var existingEvent = await GetEventByIdAsync(eventId);
+ if (string.IsNullOrEmpty(existingEvent?.CalendarId))
{
- Id = cur.GetString(projection.IndexOf(CalendarContract.Calendars.InterfaceConsts.Id)),
- Name = cur.GetString(projection.IndexOf(CalendarContract.Calendars.InterfaceConsts.CalendarDisplayName)),
- };
+ return false;
+ }
+ var thisEvent = await GetEventByIdAsync(eventId);
+
+ thisEvent.RecurrancePattern.EndDate = recurrenceEndDate;
+ thisEvent.RecurrancePattern.TotalOccurrences = null;
+
+ var eventUri = CalendarContract.Events.ContentUri;
+ var eventValues = SetupContentValues(thisEvent);
+
+ if (!(Platform.AppContext.ApplicationContext.ContentResolver.Update(eventUri, eventValues, $"{CalendarContract.Attendees.InterfaceConsts.Id}={eventId}", null) > 0))
+ {
+ throw new ArgumentException("[Android]: Could not update appointment with supplied parameters");
+ }
+ return true;
+ }
+
+ static ContentValues SetupContentValues(CalendarEvent newEvent, bool existingEvent = false)
+ {
+ var eventValues = new ContentValues();
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.CalendarId, newEvent.CalendarId);
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.Title, newEvent.Title);
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.Description, newEvent.Description);
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.EventLocation, newEvent.Location);
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.CustomAppUri, newEvent.Url);
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.AllDay, newEvent.AllDay);
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.Dtstart, newEvent.StartDate.ToUnixTimeMilliseconds().ToString());
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.Dtend, newEvent.EndDate.HasValue ? newEvent.EndDate.Value.ToUnixTimeMilliseconds().ToString() : newEvent.StartDate.AddDays(1).ToUnixTimeMilliseconds().ToString());
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.EventTimezone, TimeZoneInfo.Local.Id);
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.EventEndTimezone, TimeZoneInfo.Local.Id);
+ if (newEvent.RecurrancePattern != null)
+ {
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.Rrule, newEvent.RecurrancePattern.ConvertRule());
+ }
+ else if (existingEvent)
+ {
+ eventValues.PutNull(CalendarContract.Events.InterfaceConsts.Rrule);
+ eventValues.PutNull(CalendarContract.Events.InterfaceConsts.Duration);
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.Deleted, 0);
+ }
- static IEnumerable ToEvents(ICursor cur, List projection)
+ return eventValues;
+ }
+
+ static async Task PlatformDeleteCalendarEventInstanceByDate(string eventId, string calendarId, DateTimeOffset dateOfInstanceUtc)
{
- while (cur.MoveToNext())
+ await Permissions.RequestAsync();
+
+ var thisEvent = await GetEventInstanceByIdAsync(eventId, dateOfInstanceUtc);
+
+ var eventUri = ContentUris.WithAppendedId(CalendarContract.Events.ContentExceptionUri, long.Parse(eventId));
+
+ var eventValues = new ContentValues();
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.OriginalInstanceTime, thisEvent.StartDate.ToUnixTimeMilliseconds());
+ eventValues.Put(CalendarContract.Events.InterfaceConsts.Status, (int)EventsStatus.Canceled);
+
+ var resultUri = Platform.AppContext.ApplicationContext.ContentResolver.Insert(eventUri, eventValues);
+ if (int.TryParse(resultUri?.LastPathSegment, out var result))
{
- yield return ToEvent(cur, projection);
+ return result > 0;
}
+ return false;
}
- static CalendarEvent ToEvent(ICursor cur, List projection)
+ static async Task PlatformDeleteCalendarEventById(string eventId, string calendarId)
{
- var allDay = cur.GetInt(projection.IndexOf(CalendarContract.Events.InterfaceConsts.AllDay)) != 0;
- var start = DateTimeOffset.FromUnixTimeMilliseconds(cur.GetLong(projection.IndexOf(CalendarContract.Events.InterfaceConsts.Dtstart)));
- var end = DateTimeOffset.FromUnixTimeMilliseconds(cur.GetLong(projection.IndexOf(CalendarContract.Events.InterfaceConsts.Dtend)));
+ await Permissions.RequestAsync();
- return new CalendarEvent
+ if (string.IsNullOrEmpty(eventId))
{
- Id = cur.GetString(projection.IndexOf(CalendarContract.Events.InterfaceConsts.Id)),
- CalendarId = cur.GetString(projection.IndexOf(CalendarContract.Events.InterfaceConsts.CalendarId)),
- Title = cur.GetString(projection.IndexOf(CalendarContract.Events.InterfaceConsts.Title)),
- Description = cur.GetString(projection.IndexOf(CalendarContract.Events.InterfaceConsts.Description)),
- Location = cur.GetString(projection.IndexOf(CalendarContract.Events.InterfaceConsts.EventLocation)),
- AllDay = allDay,
- StartDate = start,
- EndDate = end,
- Attendees = PlatformGetAttendees(cur.GetString(projection.IndexOf(CalendarContract.Events.InterfaceConsts.Id))).ToList()
- };
+ throw new ArgumentException("[Android]: You must supply an event id to delete an event.");
+ }
+
+ var calendarEvent = await GetEventByIdAsync(eventId);
+
+ if (calendarEvent.CalendarId != calendarId)
+ {
+ throw new ArgumentOutOfRangeException("[Android]: Supplied event does not belong to supplied calendar");
+ }
+
+ var eventUri = ContentUris.WithAppendedId(CalendarContract.Events.ContentUri, long.Parse(eventId));
+ var result = Platform.AppContext.ApplicationContext.ContentResolver.Delete(eventUri, null, null);
+
+ return result > 0;
+ }
+
+ static async Task PlatformAddAttendeeToEvent(CalendarEventAttendee newAttendee, string eventId)
+ {
+ await Permissions.RequestAsync();
+
+ var calendarEvent = await GetEventByIdAsync(eventId);
+
+ if (calendarEvent == null)
+ {
+ throw new ArgumentException("[Android]: You must supply a valid event id to add an attendee to an event.");
+ }
+
+ var attendeeUri = CalendarContract.Attendees.ContentUri;
+ var attendeeValues = new ContentValues();
+
+ attendeeValues.Put(CalendarContract.Attendees.InterfaceConsts.EventId, eventId);
+ attendeeValues.Put(CalendarContract.Attendees.InterfaceConsts.AttendeeEmail, newAttendee.Email);
+ attendeeValues.Put(CalendarContract.Attendees.InterfaceConsts.AttendeeName, newAttendee.Name);
+ attendeeValues.Put(CalendarContract.Attendees.InterfaceConsts.AttendeeType, (int)newAttendee.Type);
+
+ var resultUri = Platform.AppContext.ApplicationContext.ContentResolver.Insert(attendeeUri, attendeeValues);
+
+ if (int.TryParse(resultUri?.LastPathSegment, out var result))
+ {
+ return result > 0;
+ }
+
+ return false;
+ }
+
+ static async Task PlatformRemoveAttendeeFromEvent(CalendarEventAttendee newAttendee, string eventId)
+ {
+ await Permissions.RequestAsync();
+
+ var calendarEvent = await GetEventByIdAsync(eventId);
+
+ if (calendarEvent == null)
+ {
+ throw new ArgumentException("[Android]: You must supply a valid event id to remove an attendee from an event.");
+ }
+
+ var attendeesUri = CalendarContract.Attendees.ContentUri;
+ var attendeeSpecificAttendees = $"{CalendarContract.Attendees.InterfaceConsts.AttendeeName}='{newAttendee.Name}' {andCondition} ";
+ attendeeSpecificAttendees += $"{CalendarContract.Attendees.InterfaceConsts.AttendeeEmail}='{newAttendee.Email}'";
+
+ var result = Platform.AppContext.ApplicationContext.ContentResolver.Delete(attendeesUri, attendeeSpecificAttendees, null);
+
+ return result > 0;
}
- static IEnumerable ToAttendees(ICursor cur, List projection)
+ // https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html
+ static RecurrenceRule GetRecurranceRuleForEvent(string rule)
{
- while (cur.MoveToNext())
+ var recurranceRule = new RecurrenceRule();
+ if (rule.Contains(byFrequencySearch, StringComparison.Ordinal))
{
- yield return ToAttendee(cur, projection);
+ var ruleFrequency = rule.Substring(rule.IndexOf(byFrequencySearch, StringComparison.Ordinal) + byFrequencySearch.Length);
+ ruleFrequency = ruleFrequency.Contains(";") ? ruleFrequency.Substring(0, ruleFrequency.IndexOf(";")) : ruleFrequency;
+ switch (ruleFrequency)
+ {
+ case dailyFrequency:
+ recurranceRule.Frequency = RecurrenceFrequency.Daily;
+ break;
+ case weeklyFrequency:
+ recurranceRule.Frequency = RecurrenceFrequency.Weekly;
+ break;
+ case monthlyFrequency:
+ recurranceRule.Frequency = RecurrenceFrequency.Monthly;
+ break;
+ case yearlyFrequency:
+ recurranceRule.Frequency = RecurrenceFrequency.Yearly;
+ break;
+ }
}
+
+ if (rule.Contains(byIntervalSearch, StringComparison.Ordinal))
+ {
+ var ruleInterval = rule.Substring(rule.IndexOf(byIntervalSearch, StringComparison.Ordinal) + byIntervalSearch.Length);
+ ruleInterval = ruleInterval.Contains(";") ? ruleInterval.Substring(0, ruleInterval.IndexOf(";", StringComparison.Ordinal)) : ruleInterval;
+ recurranceRule.Interval = uint.Parse(ruleInterval);
+ }
+ else
+ {
+ recurranceRule.Interval = 1;
+ }
+
+ if (rule.Contains(byCountSearch, StringComparison.Ordinal))
+ {
+ var ruleOccurences = rule.Substring(rule.IndexOf(byCountSearch, StringComparison.Ordinal) + byCountSearch.Length);
+ ruleOccurences = ruleOccurences.Contains(";") ? ruleOccurences.Substring(0, ruleOccurences.IndexOf(";", StringComparison.Ordinal)) : ruleOccurences;
+ recurranceRule.TotalOccurrences = uint.Parse(ruleOccurences);
+ }
+
+ if (rule.Contains(byUntilSearch, StringComparison.Ordinal))
+ {
+ var ruleEndDate = rule.Substring(rule.IndexOf(byUntilSearch, StringComparison.Ordinal) + byUntilSearch.Length);
+ ruleEndDate = ruleEndDate.Contains(";") ? ruleEndDate.Substring(0, ruleEndDate.IndexOf(";", StringComparison.Ordinal)) : ruleEndDate;
+ recurranceRule.EndDate = DateTimeOffset.ParseExact(ruleEndDate.Replace("T", string.Empty).Replace("Z", string.Empty), "yyyyMMddHHmmss", null);
+ }
+
+ if (rule.Contains(byDaySearch, StringComparison.Ordinal))
+ {
+ var ruleOccurenceDays = rule.Substring(rule.IndexOf(byDaySearch, StringComparison.Ordinal) + byDaySearch.Length);
+ ruleOccurenceDays = ruleOccurenceDays.Contains(";") ? ruleOccurenceDays.Substring(0, ruleOccurenceDays.IndexOf(";", StringComparison.Ordinal)) : ruleOccurenceDays;
+ recurranceRule.DaysOfTheWeek = new List();
+ foreach (var ruleOccurenceDay in ruleOccurenceDays.Split(','))
+ {
+ var day = ruleOccurenceDay;
+ var regex = new Regex(@"[-]?\d+");
+ var iterationOffset = regex.Match(ruleOccurenceDay);
+ if (iterationOffset.Success)
+ {
+ day = ruleOccurenceDay.Substring(iterationOffset.Index + iterationOffset.Length);
+
+ if (recurranceRule.Frequency == RecurrenceFrequency.Monthly)
+ {
+ recurranceRule.Frequency = RecurrenceFrequency.MonthlyOnDay;
+ }
+ else
+ {
+ recurranceRule.Frequency = RecurrenceFrequency.YearlyOnDay;
+ }
+ int.TryParse(iterationOffset.Value.Split(',').FirstOrDefault(), out var result);
+ recurranceRule.WeekOfMonth = (IterationOffset)result;
+ }
+ switch (day)
+ {
+ case "MO":
+ recurranceRule.DaysOfTheWeek.Add(CalendarDayOfWeek.Monday);
+ break;
+ case "TU":
+ recurranceRule.DaysOfTheWeek.Add(CalendarDayOfWeek.Tuesday);
+ break;
+ case "WE":
+ recurranceRule.DaysOfTheWeek.Add(CalendarDayOfWeek.Wednesday);
+ break;
+ case "TH":
+ recurranceRule.DaysOfTheWeek.Add(CalendarDayOfWeek.Thursday);
+ break;
+ case "FR":
+ recurranceRule.DaysOfTheWeek.Add(CalendarDayOfWeek.Friday);
+ break;
+ case "SA":
+ recurranceRule.DaysOfTheWeek.Add(CalendarDayOfWeek.Saturday);
+ break;
+ case "SU":
+ recurranceRule.DaysOfTheWeek.Add(CalendarDayOfWeek.Sunday);
+ break;
+ }
+ }
+ }
+
+ if (rule.Contains(byMonthDaySearch, StringComparison.Ordinal))
+ {
+ var ruleOccurenceMonthDays = rule?.Substring(rule.IndexOf(byMonthDaySearch, StringComparison.Ordinal) + byMonthDaySearch.Length);
+ ruleOccurenceMonthDays = ruleOccurenceMonthDays.Contains(";") ? ruleOccurenceMonthDays.Substring(0, ruleOccurenceMonthDays.IndexOf(";", StringComparison.Ordinal)) : ruleOccurenceMonthDays;
+ uint.TryParse(ruleOccurenceMonthDays.Split(',').FirstOrDefault(), out var result);
+ recurranceRule.DayOfTheMonth = result;
+ }
+
+ if (rule.Contains(byMonthSearch, StringComparison.Ordinal))
+ {
+ var ruleOccurenceMonths = rule.Substring(rule.IndexOf(byMonthSearch, StringComparison.Ordinal) + byMonthSearch.Length);
+ ruleOccurenceMonths = ruleOccurenceMonths.Contains(";") ? ruleOccurenceMonths.Substring(0, ruleOccurenceMonths.IndexOf(";", StringComparison.Ordinal)) : ruleOccurenceMonths;
+ recurranceRule.MonthOfTheYear = (MonthOfYear)Convert.ToUInt32(ruleOccurenceMonths.Split(',').FirstOrDefault());
+ }
+
+ if (rule.Contains(bySetPosSearch, StringComparison.Ordinal))
+ {
+ var ruleDayIterationOffset = rule.Substring(rule.IndexOf(bySetPosSearch, StringComparison.Ordinal) + bySetPosSearch.Length);
+ ruleDayIterationOffset = ruleDayIterationOffset.Contains(";") ? ruleDayIterationOffset.Substring(0, ruleDayIterationOffset.IndexOf(";", StringComparison.Ordinal)) : ruleDayIterationOffset;
+ Enum.TryParse(ruleDayIterationOffset.Split(',').FirstOrDefault(), out var result);
+ recurranceRule.WeekOfMonth = result;
+ if (recurranceRule.Frequency == RecurrenceFrequency.Monthly)
+ {
+ recurranceRule.Frequency = RecurrenceFrequency.MonthlyOnDay;
+ }
+ else
+ {
+ recurranceRule.Frequency = RecurrenceFrequency.YearlyOnDay;
+ }
+ }
+ return recurranceRule;
+ }
+
+ static string ConvertRule(this RecurrenceRule recurrenceRule)
+ {
+ var eventRecurrence = string.Empty;
+
+ switch (recurrenceRule.Frequency)
+ {
+ case RecurrenceFrequency.Daily:
+ case RecurrenceFrequency.Weekly:
+ if (recurrenceRule.DaysOfTheWeek != null && recurrenceRule.DaysOfTheWeek.Count > 0)
+ {
+ eventRecurrence += $"{byFrequencySearch}{weeklyFrequency};";
+ eventRecurrence += $"{byDaySearch}{recurrenceRule.DaysOfTheWeek.ToDayString()};";
+ }
+ else
+ {
+ eventRecurrence += $"{byFrequencySearch}{dailyFrequency};";
+ }
+ eventRecurrence += $"{byIntervalSearch}{recurrenceRule.Interval};";
+ break;
+ case RecurrenceFrequency.Monthly:
+ eventRecurrence += $"{byFrequencySearch}{monthlyFrequency};";
+ if (recurrenceRule.DaysOfTheWeek != null && recurrenceRule.DaysOfTheWeek.Count > 0)
+ {
+ eventRecurrence += $"{byDaySearch}{recurrenceRule.WeekOfMonth}{recurrenceRule.DaysOfTheWeek.ToDayString()};";
+ }
+ else if (recurrenceRule.DayOfTheMonth != 0)
+ {
+ eventRecurrence += $"{byMonthDaySearch}{recurrenceRule.DayOfTheMonth};";
+ }
+ else
+ {
+ eventRecurrence += $"{byIntervalSearch}{recurrenceRule.Interval};";
+ }
+ break;
+ case RecurrenceFrequency.Yearly:
+ eventRecurrence += $"{byFrequencySearch}{yearlyFrequency};";
+ if (recurrenceRule.DaysOfTheWeek != null && recurrenceRule.DaysOfTheWeek.Count > 0)
+ {
+ eventRecurrence += $"{byMonthSearch}{(int)recurrenceRule.MonthOfTheYear};";
+ eventRecurrence += $"{byDaySearch}{recurrenceRule.WeekOfMonth}{recurrenceRule.DaysOfTheWeek.ToDayString()};";
+ }
+ else if (recurrenceRule.DayOfTheMonth != 0)
+ {
+ eventRecurrence += $"{byMonthSearch}{(int)recurrenceRule.MonthOfTheYear};";
+ eventRecurrence += $"{byMonthDaySearch}{recurrenceRule.DayOfTheMonth};";
+ }
+ else
+ {
+ eventRecurrence += $"{byIntervalSearch}{recurrenceRule.Interval};";
+ }
+ break;
+ }
+
+ if (recurrenceRule.EndDate.HasValue)
+ {
+ eventRecurrence += $"UNTIL={recurrenceRule.EndDate.Value.ToUniversalTime():yyyyMMddTHHmmssZ};";
+ }
+ else if (recurrenceRule.TotalOccurrences.HasValue)
+ {
+ eventRecurrence += $"COUNT={recurrenceRule.TotalOccurrences.Value};";
+ }
+
+ return eventRecurrence.Substring(0, eventRecurrence.Length - 1);
}
- static CalendarEventAttendee ToAttendee(ICursor cur, List attendeesProjection) =>
- new CalendarEventAttendee
- {
- Name = cur.GetString(attendeesProjection.IndexOf(CalendarContract.Attendees.InterfaceConsts.AttendeeName)),
- Email = cur.GetString(attendeesProjection.IndexOf(CalendarContract.Attendees.InterfaceConsts.AttendeeEmail)),
- };
+ static string ToShortString(this CalendarDayOfWeek day)
+ {
+ switch (day)
+ {
+ case CalendarDayOfWeek.Monday:
+ return "MO";
+ case CalendarDayOfWeek.Tuesday:
+ return "TU";
+ case CalendarDayOfWeek.Wednesday:
+ return "WE";
+ case CalendarDayOfWeek.Thursday:
+ return "TH";
+ case CalendarDayOfWeek.Friday:
+ return "FR";
+ case CalendarDayOfWeek.Saturday:
+ return "SA";
+ case CalendarDayOfWeek.Sunday:
+ return "SU";
+ }
+ return "INVALID";
+ }
+
+ static string ToDayString(this List dayList)
+ {
+ var toReturn = string.Empty;
+ foreach (var day in dayList)
+ {
+ toReturn += day.ToShortString() + ",";
+ }
+ return toReturn.Substring(0, toReturn.Length - 1);
+ }
}
}
diff --git a/Xamarin.Essentials/Calendars/Calendars.ios.cs b/Xamarin.Essentials/Calendars/Calendars.ios.cs
new file mode 100644
index 000000000..b3adb2360
--- /dev/null
+++ b/Xamarin.Essentials/Calendars/Calendars.ios.cs
@@ -0,0 +1,536 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using EventKit;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class Calendars
+ {
+ static async Task> PlatformGetCalendarsAsync()
+ {
+ await Permissions.RequestAsync();
+
+ EKCalendar[] calendars;
+ try
+ {
+ calendars = CalendarRequest.Instance.Calendars;
+ }
+ catch (NullReferenceException ex)
+ {
+ throw new Exception($"iOS: Unexpected null reference exception {ex.Message}");
+ }
+ var calendarList = (from calendar in calendars
+ select new Calendar
+ {
+ Id = calendar.CalendarIdentifier,
+ Name = calendar.Title,
+ IsReadOnly = !calendar.AllowsContentModifications
+ }).ToList();
+
+ return calendarList;
+ }
+
+ static async Task> PlatformGetEventsAsync(string calendarId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null)
+ {
+ await Permissions.RequestAsync();
+
+ var startDateToConvert = startDate ?? DateTimeOffset.Now.Add(defaultStartTimeFromNow);
+ var endDateToConvert = endDate ?? startDateToConvert.Add(defaultEndTimeFromStartTime); // NOTE: 4 years is the maximum period that a iOS calendar events can search
+ var sDate = startDateToConvert.ToNSDate();
+ var eDate = endDateToConvert.ToNSDate();
+ EKCalendar[] calendars = null;
+ if (!string.IsNullOrWhiteSpace(calendarId))
+ {
+ calendars = CalendarRequest.Instance.Calendars.Where(x => x.CalendarIdentifier == calendarId).ToArray();
+
+ if (calendars.Length == 0 && !string.IsNullOrWhiteSpace(calendarId))
+ throw new ArgumentOutOfRangeException($"[iOS]: No calendar exists with the Id {calendarId}");
+ }
+
+ var query = CalendarRequest.Instance.PredicateForEvents(sDate, eDate, calendars);
+ var events = CalendarRequest.Instance.EventsMatching(query);
+
+ var eventList = (from calendarEvent in events
+ select new CalendarEvent
+ {
+ Id = calendarEvent.CalendarItemIdentifier,
+ CalendarId = calendarEvent.Calendar.CalendarIdentifier,
+ Title = calendarEvent.Title,
+ StartDate = calendarEvent.StartDate.ToDateTimeOffsetWithTimeZone(calendarEvent.TimeZone),
+ EndDate = !calendarEvent.AllDay ? (DateTimeOffset?)calendarEvent.EndDate.ToDateTimeOffsetWithTimeZone(calendarEvent.TimeZone) : null
+ })
+ .OrderBy(calendarEvent => calendarEvent.StartDate)
+ .ToList();
+
+ return eventList;
+ }
+
+ static DateTimeOffset ToDateTimeOffsetWithTimeZone(this NSDate originalDate, NSTimeZone timeZone)
+ {
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone != null ? timeZone.Name : NSTimeZone.LocalTimeZone.Name);
+ return TimeZoneInfo.ConvertTime(originalDate.ToDateTime(), timeZoneInfo);
+ }
+
+ static async Task PlatformGetEventByIdAsync(string eventId)
+ {
+ await Permissions.RequestAsync();
+
+ if (string.IsNullOrWhiteSpace(eventId))
+ {
+ throw new ArgumentException($"[iOS]: No Event found for event Id {eventId}");
+ }
+
+ var calendarEvent = CalendarRequest.Instance.GetCalendarItem(eventId) as EKEvent;
+ if (calendarEvent == null)
+ {
+ throw new ArgumentOutOfRangeException($"[iOS]: No Event found for event Id {eventId}");
+ }
+ RecurrenceRule recurrenceRule = null;
+ if (calendarEvent.HasRecurrenceRules)
+ {
+ recurrenceRule = GetRecurrenceRule(calendarEvent.RecurrenceRules[0], calendarEvent.TimeZone);
+ }
+ List alarms = null;
+ if (calendarEvent.HasAlarms)
+ {
+ alarms = new List();
+ foreach (var a in calendarEvent.Alarms)
+ {
+ alarms.Add(new CalendarEventReminder() { MinutesPriorToEventStart = (calendarEvent.StartDate.ToDateTime() - a.AbsoluteDate.ToDateTime()).Minutes });
+ }
+ }
+ var attendees = calendarEvent.Attendees != null ? GetAttendeesForEvent(calendarEvent.Attendees) : new List();
+ if (calendarEvent.Organizer != null)
+ {
+ attendees.ToList().Insert(0, new CalendarEventAttendee
+ {
+ Name = calendarEvent.Organizer.Name,
+ Email = calendarEvent.Organizer.Name,
+ Type = calendarEvent.Organizer.ParticipantRole.ToAttendeeType(),
+ IsOrganizer = true
+ });
+ }
+
+ return new CalendarEvent
+ {
+ Id = calendarEvent.CalendarItemIdentifier,
+ CalendarId = calendarEvent.Calendar.CalendarIdentifier,
+ Title = calendarEvent.Title,
+ Description = calendarEvent.Notes,
+ Location = calendarEvent.Location,
+ Url = calendarEvent.Url != null ? calendarEvent.Url.ToString() : string.Empty,
+ StartDate = calendarEvent.StartDate.ToDateTimeOffsetWithTimeZone(calendarEvent.TimeZone),
+ EndDate = !calendarEvent.AllDay ? (DateTimeOffset?)calendarEvent.EndDate.ToDateTimeOffsetWithTimeZone(calendarEvent.TimeZone) : null,
+ Attendees = attendees,
+ RecurrancePattern = recurrenceRule,
+ Reminders = alarms
+ };
+ }
+
+ static async Task PlatformGetEventInstanceByIdAsync(string eventId, DateTimeOffset instanceDate)
+ {
+ await Permissions.RequestAsync();
+
+ if (string.IsNullOrWhiteSpace(eventId))
+ {
+ throw new ArgumentException($"[iOS]: No Event found for event Id {eventId}");
+ }
+
+ var calendarEvent = CalendarRequest.Instance.GetCalendarItem(eventId) as EKEvent;
+ var instanceOfEvent = (await GetEventsAsync(calendarEvent.Calendar.CalendarIdentifier, instanceDate, instanceDate.AddDays(1))).FirstOrDefault(x => x.Id == eventId);
+
+ calendarEvent.StartDate = instanceOfEvent.StartDate.ToNSDate();
+ calendarEvent.EndDate = instanceOfEvent.AllDay ? null : instanceOfEvent.EndDate.Value.ToNSDate();
+ if (calendarEvent == null)
+ {
+ throw new ArgumentOutOfRangeException($"[iOS]: No Event found for event Id {eventId}");
+ }
+
+ RecurrenceRule recurrenceRule = null;
+ if (calendarEvent.HasRecurrenceRules)
+ {
+ recurrenceRule = GetRecurrenceRule(calendarEvent.RecurrenceRules[0], calendarEvent.TimeZone);
+ }
+ List alarms = null;
+ if (calendarEvent.HasAlarms)
+ {
+ alarms = new List();
+ foreach (var a in calendarEvent.Alarms)
+ {
+ alarms.Add(new CalendarEventReminder() { MinutesPriorToEventStart = (calendarEvent.StartDate.ToDateTimeOffsetWithTimeZone(calendarEvent.TimeZone) - a.AbsoluteDate.ToDateTimeOffsetWithTimeZone(calendarEvent.TimeZone)).Minutes });
+ }
+ }
+ var attendees = calendarEvent.Attendees != null ? GetAttendeesForEvent(calendarEvent.Attendees) : new List();
+ if (calendarEvent.Organizer != null)
+ {
+ attendees.ToList().Insert(0, new CalendarEventAttendee
+ {
+ Name = calendarEvent.Organizer.Name,
+ Email = calendarEvent.Organizer.Name,
+ Type = calendarEvent.Organizer.ParticipantRole.ToAttendeeType(),
+ IsOrganizer = true
+ });
+ }
+ return new CalendarEvent
+ {
+ Id = calendarEvent.CalendarItemIdentifier,
+ CalendarId = calendarEvent.Calendar.CalendarIdentifier,
+ Title = calendarEvent.Title,
+ Description = calendarEvent.Notes,
+ Location = calendarEvent.Location,
+ Url = calendarEvent.Url != null ? calendarEvent.Url.ToString() : string.Empty,
+ StartDate = calendarEvent.StartDate.ToDateTimeOffsetWithTimeZone(calendarEvent.TimeZone),
+ EndDate = !calendarEvent.AllDay ? (DateTimeOffset?)calendarEvent.EndDate.ToDateTimeOffsetWithTimeZone(calendarEvent.TimeZone) : null,
+ Attendees = attendees,
+ RecurrancePattern = recurrenceRule,
+ Reminders = alarms
+ };
+ }
+
+ static RecurrenceRule GetRecurrenceRule(this EKRecurrenceRule iOSRule, NSTimeZone timeZone)
+ {
+ var recurrenceRule = new RecurrenceRule();
+ recurrenceRule.Frequency = (RecurrenceFrequency)iOSRule.Frequency;
+ if (iOSRule.DaysOfTheWeek != null)
+ {
+ recurrenceRule = iOSRule.DaysOfTheWeek.ConvertToCalendarDayOfWeekList(recurrenceRule);
+ }
+ recurrenceRule.Interval = (uint)iOSRule.Interval;
+
+ if (iOSRule.SetPositions != null)
+ {
+ if (iOSRule.SetPositions.Length > 0)
+ {
+ var day = iOSRule.SetPositions[0] as NSNumber;
+ recurrenceRule.WeekOfMonth = (IterationOffset)((int)day);
+ if (recurrenceRule.Frequency == RecurrenceFrequency.Monthly)
+ {
+ recurrenceRule.Frequency = RecurrenceFrequency.MonthlyOnDay;
+ }
+ else
+ {
+ recurrenceRule.Frequency = RecurrenceFrequency.YearlyOnDay;
+ }
+ }
+ }
+
+ if (iOSRule.DaysOfTheMonth != null)
+ {
+ if (iOSRule.DaysOfTheMonth.Count() > 0)
+ {
+ recurrenceRule.DayOfTheMonth = (uint)iOSRule.DaysOfTheMonth?.FirstOrDefault();
+ }
+ }
+
+ if (iOSRule.MonthsOfTheYear != null)
+ {
+ if (iOSRule.MonthsOfTheYear.Count() > 0)
+ {
+ recurrenceRule.MonthOfTheYear = (MonthOfYear)(uint)iOSRule.MonthsOfTheYear?.FirstOrDefault();
+ }
+ }
+
+ recurrenceRule.EndDate = iOSRule.RecurrenceEnd?.EndDate?.ToDateTimeOffsetWithTimeZone(timeZone);
+
+ recurrenceRule.TotalOccurrences = (uint?)iOSRule.RecurrenceEnd?.OccurrenceCount;
+
+ return recurrenceRule;
+ }
+
+ static RecurrenceRule ConvertToCalendarDayOfWeekList(this EKRecurrenceDayOfWeek[] recurrenceDays, RecurrenceRule rule)
+ {
+ var enumValues = Enum.GetValues(typeof(CalendarDayOfWeek));
+
+ rule.DaysOfTheWeek = recurrenceDays.ToList().Select(x => (CalendarDayOfWeek)enumValues.GetValue(Convert.ToInt32(x.DayOfTheWeek))).ToList();
+
+ foreach (var day in recurrenceDays)
+ {
+ if (day.WeekNumber != 0)
+ {
+ if (rule.Frequency == RecurrenceFrequency.Monthly)
+ {
+ rule.Frequency = RecurrenceFrequency.MonthlyOnDay;
+ }
+ else
+ {
+ rule.Frequency = RecurrenceFrequency.YearlyOnDay;
+ }
+ rule.WeekOfMonth = (IterationOffset)(int)(day.WeekNumber - 1);
+ }
+ }
+ return rule;
+ }
+
+ static IEnumerable GetAttendeesForEvent(IEnumerable inviteList)
+ {
+ var attendees = (from attendee in inviteList
+ select new CalendarEventAttendee
+ {
+ Name = attendee.Name,
+ Email = attendee.Name,
+ Type = attendee.ParticipantRole.ToAttendeeType()
+ })
+ .OrderBy(attendee => attendee.Name)
+ .ToList();
+
+ return attendees;
+ }
+
+ static AttendeeType ToAttendeeType(this EKParticipantRole role)
+ {
+ switch (role)
+ {
+ case EKParticipantRole.Required:
+ return AttendeeType.Required;
+ case EKParticipantRole.Optional:
+ return AttendeeType.Optional;
+ case EKParticipantRole.NonParticipant:
+ case EKParticipantRole.Chair:
+ return AttendeeType.Resource;
+ case EKParticipantRole.Unknown:
+ default:
+ return AttendeeType.None;
+ }
+ }
+
+ static async Task PlatformCreateCalendarEvent(CalendarEvent newEvent)
+ {
+ await Permissions.RequestAsync();
+
+ if (string.IsNullOrEmpty(newEvent.CalendarId))
+ {
+ return string.Empty;
+ }
+
+ var calendarEvent = EKEvent.FromStore(CalendarRequest.Instance);
+ calendarEvent = SetUpEvent(calendarEvent, newEvent);
+ var error = new NSError();
+
+ if (CalendarRequest.Instance.SaveEvent(calendarEvent, EKSpan.FutureEvents, true, out error))
+ {
+ return calendarEvent.EventIdentifier;
+ }
+ throw new ArgumentException("[iOS]: Could not create appointment with supplied parameters");
+ }
+
+ static async Task PlatformUpdateCalendarEvent(CalendarEvent eventToUpdate)
+ {
+ await Permissions.RequestAsync();
+
+ var existingEvent = await GetEventByIdAsync(eventToUpdate.Id);
+ EKEvent thisEvent;
+ if (string.IsNullOrEmpty(eventToUpdate.CalendarId) || existingEvent == null)
+ {
+ return false;
+ }
+ else if (existingEvent.CalendarId != eventToUpdate.CalendarId)
+ {
+ await DeleteCalendarEventById(existingEvent.Id, existingEvent.CalendarId);
+ thisEvent = EKEvent.FromStore(CalendarRequest.Instance);
+ }
+ else
+ {
+ thisEvent = CalendarRequest.Instance.GetCalendarItem(eventToUpdate.Id) as EKEvent;
+ }
+
+ thisEvent = SetUpEvent(thisEvent, eventToUpdate);
+
+ if (CalendarRequest.Instance.SaveEvent(thisEvent, EKSpan.FutureEvents, true, out var error))
+ {
+ return true;
+ }
+ throw new ArgumentException("[iOS]: Could not update appointment with supplied parameters");
+ }
+
+ static async Task PlatformSetEventRecurrenceEndDate(string eventId, DateTimeOffset recurrenceEndDate)
+ {
+ await Permissions.RequestAsync();
+
+ var existingEvent = await GetEventByIdAsync(eventId);
+ if (string.IsNullOrEmpty(existingEvent?.CalendarId))
+ {
+ return false;
+ }
+ var thisEvent = CalendarRequest.Instance.GetCalendarItem(eventId) as EKEvent;
+
+ existingEvent.RecurrancePattern.EndDate = recurrenceEndDate;
+ existingEvent.RecurrancePattern.TotalOccurrences = null;
+ thisEvent = SetUpEvent(thisEvent, existingEvent);
+
+ if (!CalendarRequest.Instance.SaveEvent(thisEvent, EKSpan.FutureEvents, true, out var error))
+ {
+ throw new ArgumentException("[iOS]: Could not update appointments recurrence dates with supplied parameters");
+ }
+ return true;
+ }
+
+ static EKEvent SetUpEvent(EKEvent eventToUpdate, CalendarEvent eventToUpdateFrom)
+ {
+ var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(NSTimeZone.LocalTimeZone.Name);
+ eventToUpdate.Title = eventToUpdateFrom.Title;
+ eventToUpdate.Calendar = CalendarRequest.Instance.GetCalendar(eventToUpdateFrom.CalendarId);
+ eventToUpdate.Notes = eventToUpdateFrom.Description;
+ eventToUpdate.Location = eventToUpdateFrom.Location;
+ eventToUpdate.AllDay = eventToUpdateFrom.AllDay;
+ eventToUpdate.StartDate = TimeZoneInfo.ConvertTime(eventToUpdateFrom.StartDate, timeZoneInfo).ToNSDate();
+ eventToUpdate.TimeZone = NSTimeZone.LocalTimeZone;
+ eventToUpdate.Url = !string.IsNullOrWhiteSpace(eventToUpdateFrom.Url) ? new NSUrl(eventToUpdateFrom.Url) : null;
+ eventToUpdate.EndDate = eventToUpdateFrom.EndDate.HasValue ? TimeZoneInfo.ConvertTime(eventToUpdateFrom.EndDate.Value, timeZoneInfo).ToNSDate() : TimeZoneInfo.ConvertTime(eventToUpdateFrom.StartDate, timeZoneInfo).AddDays(1).ToNSDate();
+ if (eventToUpdateFrom.RecurrancePattern != null && eventToUpdateFrom.RecurrancePattern.Frequency != null)
+ {
+ eventToUpdate.RecurrenceRules = new EKRecurrenceRule[1] { eventToUpdateFrom.RecurrancePattern.ConvertRule() };
+ }
+ return eventToUpdate;
+ }
+
+ static EKRecurrenceFrequency ConvertToiOS(this RecurrenceFrequency? recurrenceFrequency)
+ {
+ switch (recurrenceFrequency)
+ {
+ case RecurrenceFrequency.Daily:
+ return EKRecurrenceFrequency.Daily;
+ case RecurrenceFrequency.Weekly:
+ return EKRecurrenceFrequency.Weekly;
+ case RecurrenceFrequency.Monthly:
+ case RecurrenceFrequency.MonthlyOnDay:
+ return EKRecurrenceFrequency.Monthly;
+ case RecurrenceFrequency.Yearly:
+ case RecurrenceFrequency.YearlyOnDay:
+ return EKRecurrenceFrequency.Yearly;
+ default:
+ return EKRecurrenceFrequency.Daily;
+ }
+ }
+
+ static EKRecurrenceDayOfWeek[] ConvertToiOS(this List daysOfTheWeek)
+ {
+ if (daysOfTheWeek == null || !daysOfTheWeek.Any())
+ return null;
+
+ var toReturn = new List();
+ foreach (var day in daysOfTheWeek)
+ {
+ toReturn.Add(EKRecurrenceDayOfWeek.FromDay(day.ConvertToiOS()));
+ }
+ return toReturn.ToArray();
+ }
+
+ static NSNumber[] ConvertToiOS(this int dayOfTheMonth) => new NSNumber[1] { dayOfTheMonth };
+
+ static EKDay ConvertToiOS(this CalendarDayOfWeek day) => (EKDay)Math.Log((int)day, 2);
+
+ static EKRecurrenceRule ConvertRule(this RecurrenceRule recurrenceRule) => new EKRecurrenceRule(
+ type: recurrenceRule.Frequency.ConvertToiOS(),
+ interval: (nint)recurrenceRule.Interval,
+ days: recurrenceRule.Frequency != RecurrenceFrequency.Daily ? recurrenceRule.DaysOfTheWeek.ConvertToiOS() : null,
+ monthDays: (recurrenceRule.DaysOfTheWeek != null && recurrenceRule.DaysOfTheWeek.Count > 0) ? null : ((int)recurrenceRule.DayOfTheMonth).ConvertToiOS(),
+ months: recurrenceRule.Frequency == RecurrenceFrequency.Yearly ? ((int)recurrenceRule.MonthOfTheYear).ConvertToiOS() : null,
+ weeksOfTheYear: null,
+ daysOfTheYear: null,
+ setPositions: recurrenceRule.Frequency == RecurrenceFrequency.Yearly || recurrenceRule.Frequency == RecurrenceFrequency.Monthly ? ((int)recurrenceRule.WeekOfMonth).ConvertToiOS() : null,
+ end: recurrenceRule.EndDate.HasValue ? EKRecurrenceEnd.FromEndDate(TimeZoneInfo.ConvertTime(recurrenceRule.EndDate.Value, TimeZoneInfo.Local).ToNSDate()) : recurrenceRule.TotalOccurrences.HasValue ? EKRecurrenceEnd.FromOccurrenceCount((nint)recurrenceRule.TotalOccurrences.Value) : null);
+
+ static async Task PlatformDeleteCalendarEventInstanceByDate(string eventId, string calendarId, DateTimeOffset dateOfInstanceUtc)
+ {
+ await Permissions.RequestAsync();
+
+ if (string.IsNullOrEmpty(eventId))
+ {
+ throw new ArgumentException("[iOS]: You must supply an event id to delete an event.");
+ }
+ var calendars = CalendarRequest.Instance.Calendars.Where(x => x.CalendarIdentifier == calendarId).ToArray();
+ var query = CalendarRequest.Instance.PredicateForEvents(dateOfInstanceUtc.ToNSDate(), dateOfInstanceUtc.AddDays(1).ToNSDate(), calendars);
+ var events = CalendarRequest.Instance.EventsMatching(query);
+ var thisEvent = events.FirstOrDefault(x => x.CalendarItemIdentifier == eventId);
+
+ if ((thisEvent?.Calendar.CalendarIdentifier ?? string.Empty) != calendarId)
+ {
+ throw new ArgumentOutOfRangeException("[iOS]: Supplied event does not belong to supplied calendar.");
+ }
+
+ if (CalendarRequest.Instance.RemoveEvent(thisEvent, EKSpan.ThisEvent, true, out var error))
+ {
+ return true;
+ }
+ throw new Exception(error.DebugDescription);
+ }
+
+ static async Task PlatformDeleteCalendarEventById(string eventId, string calendarId)
+ {
+ await Permissions.RequestAsync();
+
+ if (string.IsNullOrEmpty(eventId))
+ {
+ throw new ArgumentException("[iOS]: You must supply an event id to delete an event.");
+ }
+
+ var calendarEvent = CalendarRequest.Instance.GetCalendarItem(eventId) as EKEvent;
+
+ if ((calendarEvent?.Calendar.CalendarIdentifier ?? string.Empty) != calendarId)
+ {
+ throw new ArgumentOutOfRangeException("[iOS]: Supplied event does not belong to supplied calendar.");
+ }
+
+ if (CalendarRequest.Instance.RemoveEvent(calendarEvent, EKSpan.FutureEvents, true, out var error))
+ {
+ return true;
+ }
+ throw new Exception(error.DebugDescription);
+ }
+
+ static async Task PlatformCreateCalendar(Calendar newCalendar)
+ {
+ await Permissions.RequestAsync();
+
+ var calendar = EKCalendar.Create(EKEntityType.Event, CalendarRequest.Instance);
+ calendar.Title = newCalendar.Name;
+ var source = CalendarRequest.Instance.Sources.FirstOrDefault(x => x.SourceType == EKSourceType.Local);
+ calendar.Source = source;
+
+ if (CalendarRequest.Instance.SaveCalendar(calendar, true, out var error))
+ {
+ return calendar.CalendarIdentifier;
+ }
+ throw new Exception(error.DebugDescription);
+ }
+
+ // Not possible at this point in time from what I've found - https://stackoverflow.com/questions/28826222/add-invitees-to-calendar-event-programmatically-ios
+ static async Task PlatformAddAttendeeToEvent(CalendarEventAttendee newAttendee, string eventId)
+ {
+ await Permissions.RequestAsync();
+
+ var attendee = ObjCRuntime.Class.GetHandle("EKAttendee");
+
+ var attendeeObject = ObjCRuntime.Runtime.GetNSObject(attendee);
+ var email = new NSString("emailAddress");
+
+ // tst.Init();
+ attendeeObject.SetValueForKey(new NSString(newAttendee.Email), email);
+
+ var result = attendeeObject as EKParticipant;
+ return true;
+ }
+
+ static async Task PlatformRemoveAttendeeFromEvent(CalendarEventAttendee newAttendee, string eventId)
+ {
+ await Permissions.RequestAsync();
+
+ var calendarEvent = CalendarRequest.Instance.GetCalendarItem(eventId) as EKEvent;
+
+ var calendarEventAttendees = calendarEvent.Attendees.ToList();
+ calendarEventAttendees.RemoveAll(x => x.Name == newAttendee.Name);
+
+ // calendarEvent.Attendees = calendarEventAttendees; - readonly cannot be done at this stage.
+
+ if (CalendarRequest.Instance.SaveEvent(calendarEvent, EKSpan.FutureEvents, true, out var error))
+ {
+ return true;
+ }
+ throw new Exception(error.DebugDescription);
+ }
+ }
+}
diff --git a/Xamarin.Essentials/Calendars/Calendars.netstandard.tvos.watchos.tizen.cs b/Xamarin.Essentials/Calendars/Calendars.netstandard.tvos.watchos.tizen.cs
index 4423f7e5f..9a1a33eb7 100644
--- a/Xamarin.Essentials/Calendars/Calendars.netstandard.tvos.watchos.tizen.cs
+++ b/Xamarin.Essentials/Calendars/Calendars.netstandard.tvos.watchos.tizen.cs
@@ -6,16 +6,28 @@ namespace Xamarin.Essentials
{
public static partial class Calendars
{
- static Task> PlatformGetCalendarsAsync() =>
- throw ExceptionUtils.NotSupportedOrImplementedException;
+ static Task> PlatformGetCalendarsAsync() => throw ExceptionUtils.NotSupportedOrImplementedException;
- static Task PlatformGetCalendarAsync(string calendarId) =>
- throw ExceptionUtils.NotSupportedOrImplementedException;
+ static Task> PlatformGetEventsAsync(string calendarId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null) => throw ExceptionUtils.NotSupportedOrImplementedException;
- static Task> PlatformGetEventsAsync(string calendarId, DateTimeOffset? startDate, DateTimeOffset? endDate) =>
- throw ExceptionUtils.NotSupportedOrImplementedException;
+ static Task PlatformGetEventByIdAsync(string eventId) => throw ExceptionUtils.NotSupportedOrImplementedException;
- static Task PlatformGetEventAsync(string eventId) =>
- throw ExceptionUtils.NotSupportedOrImplementedException;
+ static Task PlatformGetEventInstanceByIdAsync(string eventId, DateTimeOffset instanceDate) => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformCreateCalendarEvent(CalendarEvent newEvent) => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformUpdateCalendarEvent(CalendarEvent eventToUpdate) => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformSetEventRecurrenceEndDate(string eventId, DateTimeOffset recurrenceEndDate) => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformDeleteCalendarEventInstanceByDate(string eventId, string calendarId, DateTimeOffset dateOfInstanceUtc) => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformDeleteCalendarEventById(string eventId, string calendarId) => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformCreateCalendar(Calendar newCalendar) => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformAddAttendeeToEvent(CalendarEventAttendee newAttendee, string eventId) => throw ExceptionUtils.NotSupportedOrImplementedException;
+
+ static Task PlatformRemoveAttendeeFromEvent(CalendarEventAttendee newAttendee, string eventId) => throw ExceptionUtils.NotSupportedOrImplementedException;
}
}
diff --git a/Xamarin.Essentials/Calendars/Calendars.shared.cs b/Xamarin.Essentials/Calendars/Calendars.shared.cs
index f4cf654d7..6b83033f4 100644
--- a/Xamarin.Essentials/Calendars/Calendars.shared.cs
+++ b/Xamarin.Essentials/Calendars/Calendars.shared.cs
@@ -6,51 +6,32 @@ namespace Xamarin.Essentials
{
public static partial class Calendars
{
- static readonly TimeSpan defaultStartTimeFromNow = TimeSpan.Zero;
- static readonly TimeSpan defaultEndTimeFromStartTime = TimeSpan.FromDays(14);
+ static TimeSpan defaultStartTimeFromNow = TimeSpan.Zero;
- public static async Task> GetCalendarsAsync()
- {
- await Permissions.RequestAsync();
+ static TimeSpan defaultEndTimeFromStartTime = TimeSpan.FromDays(14);
- return await PlatformGetCalendarsAsync();
- }
+ public static Task> GetCalendarsAsync() => PlatformGetCalendarsAsync();
- public static async Task GetCalendarAsync(string calendarId)
- {
- if (calendarId == null)
- throw new ArgumentNullException(nameof(calendarId));
- if (string.IsNullOrWhiteSpace(calendarId))
- throw InvalidCalendar(calendarId);
+ public static Task> GetEventsAsync(string calendarId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null) => PlatformGetEventsAsync(calendarId, startDate, endDate);
- await Permissions.RequestAsync();
+ public static Task GetEventByIdAsync(string eventId) => PlatformGetEventByIdAsync(eventId);
- return await PlatformGetCalendarAsync(calendarId);
- }
+ public static Task GetEventInstanceByIdAsync(string eventId, DateTimeOffset instanceDate) => PlatformGetEventInstanceByIdAsync(eventId, instanceDate);
- public static async Task> GetEventsAsync(string calendarId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null)
- {
- await Permissions.RequestAsync();
+ public static Task CreateCalendarEvent(CalendarEvent newEvent) => PlatformCreateCalendarEvent(newEvent);
- return await PlatformGetEventsAsync(calendarId, startDate, endDate);
- }
+ public static Task UpdateCalendarEvent(CalendarEvent eventToUpdate) => PlatformUpdateCalendarEvent(eventToUpdate);
- public static async Task GetEventAsync(string eventId)
- {
- if (eventId == null)
- throw new ArgumentNullException(nameof(eventId));
- if (string.IsNullOrWhiteSpace(eventId))
- throw InvalidEvent(eventId);
+ public static Task SetEventRecurrenceEndDate(string eventId, DateTimeOffset recurrenceEndDate) => PlatformSetEventRecurrenceEndDate(eventId, recurrenceEndDate);
- await Permissions.RequestAsync();
+ public static Task DeleteCalendarEventInstanceByDate(string eventId, string calendarId, DateTimeOffset dateOfInstanceUtc) => PlatformDeleteCalendarEventInstanceByDate(eventId, calendarId, dateOfInstanceUtc);
- return await PlatformGetEventAsync(eventId);
- }
+ public static Task DeleteCalendarEventById(string eventId, string calendarId) => PlatformDeleteCalendarEventById(eventId, calendarId);
- static ArgumentException InvalidCalendar(string calendarId) =>
- new ArgumentOutOfRangeException($"No calendar exists with the ID '{calendarId}'.", nameof(calendarId));
+ public static Task CreateCalendar(Calendar newCalendar) => PlatformCreateCalendar(newCalendar);
- static ArgumentException InvalidEvent(string eventId) =>
- new ArgumentOutOfRangeException($"No event exists with the ID '{eventId}'.", nameof(eventId));
+ public static Task AddAttendeeToEvent(CalendarEventAttendee newAttendee, string eventId) => PlatformAddAttendeeToEvent(newAttendee, eventId);
+
+ public static Task RemoveAttendeeFromEvent(CalendarEventAttendee newAttendee, string eventId) => PlatformRemoveAttendeeFromEvent(newAttendee, eventId);
}
}
diff --git a/Xamarin.Essentials/Calendars/Calendars.uwp.cs b/Xamarin.Essentials/Calendars/Calendars.uwp.cs
index b1f300ac6..7296f6072 100644
--- a/Xamarin.Essentials/Calendars/Calendars.uwp.cs
+++ b/Xamarin.Essentials/Calendars/Calendars.uwp.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Windows.ApplicationModel.Appointments;
@@ -8,126 +9,547 @@ namespace Xamarin.Essentials
{
public static partial class Calendars
{
- static Task uwpAppointmentStore;
-
- static Task GetInstanceAsync() =>
- uwpAppointmentStore ??= AppointmentManager.RequestStoreAsync(AppointmentStoreAccessType.AllCalendarsReadOnly).AsTask();
-
static async Task> PlatformGetCalendarsAsync()
{
- var instance = await GetInstanceAsync().ConfigureAwait(false);
+ await Permissions.RequestAsync();
+
+ var instance = await CalendarRequest.GetInstanceAsync(AppointmentStoreAccessType.AllCalendarsReadWrite);
+ var uwpCalendarList = await instance.FindAppointmentCalendarsAsync(FindAppointmentCalendarsOptions.IncludeHidden);
- var calendars = await instance.FindAppointmentCalendarsAsync(FindAppointmentCalendarsOptions.IncludeHidden).AsTask().ConfigureAwait(false);
+ var calendars = (from calendar in uwpCalendarList
+ select new Calendar
+ {
+ Id = calendar.LocalId,
+ Name = calendar.DisplayName,
- return ToCalendars(calendars).ToList();
+ // This logic seems reversed but I'm unsure why, this actually works as expected.
+ IsReadOnly = calendar.CanCreateOrUpdateAppointments
+ }).ToList();
+
+ return calendars;
}
static async Task> PlatformGetEventsAsync(string calendarId = null, DateTimeOffset? startDate = null, DateTimeOffset? endDate = null)
{
- var options = new FindAppointmentsOptions();
+ await Permissions.RequestAsync();
- // properties
+ var options = new FindAppointmentsOptions();
options.FetchProperties.Add(AppointmentProperties.Subject);
- options.FetchProperties.Add(AppointmentProperties.Details);
- options.FetchProperties.Add(AppointmentProperties.Location);
options.FetchProperties.Add(AppointmentProperties.StartTime);
options.FetchProperties.Add(AppointmentProperties.Duration);
options.FetchProperties.Add(AppointmentProperties.AllDay);
- options.FetchProperties.Add(AppointmentProperties.Invitees);
-
- // calendar
- if (!string.IsNullOrEmpty(calendarId))
- options.CalendarIds.Add(calendarId);
-
- // dates
var sDate = startDate ?? DateTimeOffset.Now.Add(defaultStartTimeFromNow);
var eDate = endDate ?? sDate.Add(defaultEndTimeFromStartTime);
+
if (eDate < sDate)
eDate = sDate;
- var instance = await GetInstanceAsync().ConfigureAwait(false);
+ var instance = await CalendarRequest.GetInstanceAsync(AppointmentStoreAccessType.AllCalendarsReadOnly);
+ var events = await instance.FindAppointmentsAsync(sDate, eDate.Subtract(sDate), options);
- var events = await instance.FindAppointmentsAsync(sDate, eDate.Subtract(sDate), options).AsTask().ConfigureAwait(false);
+ var eventList = (from e in events
+ select new CalendarEvent
+ {
+ Id = e.LocalId,
+ CalendarId = e.CalendarId,
+ Title = e.Subject,
+ StartDate = e.StartTime,
+ EndDate = !e.AllDay ? (DateTimeOffset?)e.StartTime.Add(e.Duration) : null
+ })
+ .Where(e => e.CalendarId == calendarId || calendarId == null)
+ .OrderBy(e => e.StartDate)
+ .ToList();
- // confirm the calendar exists if no events were found
- // the PlatformGetCalendarAsync wll throw if not
- if ((events == null || events.Count == 0) && !string.IsNullOrEmpty(calendarId))
- await PlatformGetCalendarAsync(calendarId).ConfigureAwait(false);
+ if (!eventList.Any() && !string.IsNullOrWhiteSpace(calendarId))
+ {
+ await GetCalendarById(calendarId);
+ }
- return ToEvents(events.OrderBy(e => e.StartTime)).ToList();
+ return eventList;
}
- static async Task PlatformGetCalendarAsync(string calendarId)
+ static async Task GetCalendarById(string calendarId)
{
- var instance = await GetInstanceAsync().ConfigureAwait(false);
+ var instance = await CalendarRequest.GetInstanceAsync(AppointmentStoreAccessType.AllCalendarsReadOnly);
+ var uwpCalendarList = await instance.FindAppointmentCalendarsAsync(FindAppointmentCalendarsOptions.IncludeHidden);
- var calendar = await instance.GetAppointmentCalendarAsync(calendarId).AsTask().ConfigureAwait(false);
- if (calendar == null)
- throw InvalidCalendar(calendarId);
+ var result = (from calendar in uwpCalendarList
+ select new Calendar
+ {
+ Id = calendar.LocalId,
+ Name = calendar.DisplayName
+ })
+ .Where(c => c.Id == calendarId).FirstOrDefault();
+ if (result == null)
+ {
+ throw new ArgumentOutOfRangeException($"[UWP]: No calendar exists with the Id {calendarId}");
+ }
- return ToCalendar(calendar);
+ return result;
}
- static async Task PlatformGetEventAsync(string eventId)
+ static async Task PlatformGetEventByIdAsync(string eventId)
{
- var instance = await GetInstanceAsync().ConfigureAwait(false);
+ await Permissions.RequestAsync();
- var e = await instance.GetAppointmentAsync(eventId).AsTask().ConfigureAwait(false);
- if (e == null)
- throw InvalidEvent(eventId);
+ var instance = await CalendarRequest.GetInstanceAsync(AppointmentStoreAccessType.AllCalendarsReadOnly);
+
+ Appointment uwpAppointment;
+ try
+ {
+ uwpAppointment = await instance.GetAppointmentAsync(eventId);
+ uwpAppointment.DetailsKind = AppointmentDetailsKind.PlainText;
+ }
+ catch (ArgumentException)
+ {
+ if (string.IsNullOrWhiteSpace(eventId))
+ {
+ throw new ArgumentException($"[UWP]: No Event found for event Id {eventId}");
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException($"[UWP]: No Event found for event Id {eventId}");
+ }
+ }
+ RecurrenceRule rules = null;
+ if (uwpAppointment.Recurrence != null)
+ {
+ rules = new RecurrenceRule();
+ rules.Frequency = (RecurrenceFrequency)uwpAppointment.Recurrence.Unit;
+ rules.Interval = uwpAppointment.Recurrence.Interval;
+ rules.EndDate = uwpAppointment.Recurrence.Until;
+ rules.TotalOccurrences = uwpAppointment.Recurrence.Occurrences;
+ switch (rules.Frequency)
+ {
+ case RecurrenceFrequency.Daily:
+ case RecurrenceFrequency.Weekly:
+ rules.DaysOfTheWeek = ConvertBitFlagToIntList((int)uwpAppointment.Recurrence.DaysOfWeek, (int)AppointmentDaysOfWeek.Saturday).ToList();
+ break;
+ case RecurrenceFrequency.MonthlyOnDay:
+ rules.WeekOfMonth = (IterationOffset)uwpAppointment.Recurrence.WeekOfMonth;
+ rules.DaysOfTheWeek = ConvertBitFlagToIntList((int)uwpAppointment.Recurrence.DaysOfWeek, (int)AppointmentDaysOfWeek.Saturday).ToList();
+ break;
+ case RecurrenceFrequency.Monthly:
+ rules.DayOfTheMonth = uwpAppointment.Recurrence.Day;
+ break;
+ case RecurrenceFrequency.YearlyOnDay:
+ rules.WeekOfMonth = (IterationOffset)uwpAppointment.Recurrence.WeekOfMonth;
+ rules.DaysOfTheWeek = ConvertBitFlagToIntList((int)uwpAppointment.Recurrence.DaysOfWeek, (int)AppointmentDaysOfWeek.Saturday).ToList();
+ break;
+ case RecurrenceFrequency.Yearly:
+ rules.DayOfTheMonth = uwpAppointment.Recurrence.Day;
+ rules.MonthOfTheYear = (MonthOfYear)uwpAppointment.Recurrence.Month;
+ break;
+ }
+ }
- return ToEvent(e);
+ return new CalendarEvent()
+ {
+ Id = uwpAppointment.LocalId,
+ CalendarId = uwpAppointment.CalendarId,
+ Title = uwpAppointment.Subject,
+ Description = uwpAppointment.Details,
+ Location = uwpAppointment.Location,
+ Url = uwpAppointment.Uri != null ? uwpAppointment.Uri.ToString() : string.Empty,
+ StartDate = uwpAppointment.StartTime,
+ EndDate = !uwpAppointment.AllDay ? (DateTimeOffset?)uwpAppointment.StartTime.Add(uwpAppointment.Duration) : null,
+ Attendees = GetAttendeesForEvent(uwpAppointment.Invitees, uwpAppointment.Organizer),
+ RecurrancePattern = rules,
+ Reminders = uwpAppointment.Reminder.HasValue ? new List() { new CalendarEventReminder() { MinutesPriorToEventStart = uwpAppointment.Reminder.Value.Minutes } } : null
+ };
}
- static IEnumerable ToCalendars(IEnumerable native)
+ static async Task PlatformGetEventInstanceByIdAsync(string eventId, DateTimeOffset instanceDate)
{
- foreach (var calendar in native)
+ await Permissions.RequestAsync();
+
+ var instance = await CalendarRequest.GetInstanceAsync(AppointmentStoreAccessType.AllCalendarsReadOnly);
+
+ Appointment uwpAppointment;
+ try
{
- yield return ToCalendar(calendar);
+ uwpAppointment = await instance.GetAppointmentInstanceAsync(eventId, instanceDate);
+ uwpAppointment.DetailsKind = AppointmentDetailsKind.PlainText;
+ }
+ catch (ArgumentException)
+ {
+ if (string.IsNullOrWhiteSpace(eventId))
+ {
+ throw new ArgumentException($"[UWP]: No Event found for event Id {eventId}");
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException($"[UWP]: No Event found for event Id {eventId}");
+ }
+ }
+ RecurrenceRule rules = null;
+ if (uwpAppointment.Recurrence != null)
+ {
+ rules = new RecurrenceRule();
+ rules.Frequency = (RecurrenceFrequency)uwpAppointment.Recurrence.Unit;
+ rules.Interval = uwpAppointment.Recurrence.Interval;
+ rules.EndDate = uwpAppointment.Recurrence.Until;
+ rules.TotalOccurrences = uwpAppointment.Recurrence.Occurrences;
+ switch (rules.Frequency)
+ {
+ case RecurrenceFrequency.Daily:
+ case RecurrenceFrequency.Weekly:
+ rules.DaysOfTheWeek = ConvertBitFlagToIntList((int)uwpAppointment.Recurrence.DaysOfWeek, (int)AppointmentDaysOfWeek.Saturday).ToList();
+ break;
+ case RecurrenceFrequency.MonthlyOnDay:
+ rules.WeekOfMonth = (IterationOffset)uwpAppointment.Recurrence.WeekOfMonth;
+ rules.DaysOfTheWeek = ConvertBitFlagToIntList((int)uwpAppointment.Recurrence.DaysOfWeek, (int)AppointmentDaysOfWeek.Saturday).ToList();
+ break;
+ case RecurrenceFrequency.Monthly:
+ rules.DayOfTheMonth = uwpAppointment.Recurrence.Day;
+ break;
+ case RecurrenceFrequency.YearlyOnDay:
+ rules.WeekOfMonth = (IterationOffset)uwpAppointment.Recurrence.WeekOfMonth;
+ rules.DaysOfTheWeek = ConvertBitFlagToIntList((int)uwpAppointment.Recurrence.DaysOfWeek, (int)AppointmentDaysOfWeek.Saturday).ToList();
+ rules.MonthOfTheYear = (MonthOfYear)uwpAppointment.Recurrence.Month;
+ break;
+ case RecurrenceFrequency.Yearly:
+ rules.DayOfTheMonth = uwpAppointment.Recurrence.Day;
+ rules.MonthOfTheYear = (MonthOfYear)uwpAppointment.Recurrence.Month;
+ break;
+ }
}
- }
- static Calendar ToCalendar(AppointmentCalendar calendar) =>
- new Calendar
+ return new CalendarEvent()
{
- Id = calendar.LocalId,
- Name = calendar.DisplayName
+ Id = uwpAppointment.LocalId,
+ CalendarId = uwpAppointment.CalendarId,
+ Title = uwpAppointment.Subject,
+ Description = uwpAppointment.Details,
+ Location = uwpAppointment.Location,
+ Url = uwpAppointment.Uri != null ? uwpAppointment.Uri.ToString() : string.Empty,
+ StartDate = uwpAppointment.StartTime,
+ EndDate = !uwpAppointment.AllDay ? (DateTimeOffset?)uwpAppointment.StartTime.Add(uwpAppointment.Duration) : null,
+ Attendees = GetAttendeesForEvent(uwpAppointment.Invitees, uwpAppointment.Organizer),
+ RecurrancePattern = rules,
+ Reminders = uwpAppointment.Reminder.HasValue ? new List() { new CalendarEventReminder() { MinutesPriorToEventStart = uwpAppointment.Reminder.Value.Minutes } } : null
};
+ }
+
+ static List ConvertBitFlagToIntList(int wholeNumber, int maxValue)
+ {
+ var currentVal = wholeNumber;
+ var toReturn = new List();
+ for (var i = maxValue; i > 0; i /= 2)
+ {
+ if (currentVal >= i)
+ {
+ toReturn.Add((CalendarDayOfWeek)i);
+ currentVal -= i;
+ }
+ }
+ return toReturn;
+ }
- static IEnumerable ToEvents(IEnumerable native)
+ static int ConvertIntListToBitFlag(List listOfNumbers)
{
- foreach (var e in native)
+ var toReturn = 0;
+ foreach (var i in listOfNumbers)
{
- yield return ToEvent(e);
+ toReturn += i;
}
+ return toReturn;
+ }
+
+ public static T GetEnumByIndex(int index)
+ {
+ var enumValues = Enum.GetValues(typeof(T));
+ return (T)enumValues.GetValue(index);
}
- static CalendarEvent ToEvent(Appointment e) =>
- new CalendarEvent
+ static IEnumerable GetAttendeesForEvent(IEnumerable inviteList, AppointmentOrganizer organizer)
+ {
+ var attendees = (from attendee in inviteList
+ select new CalendarEventAttendee
+ {
+ Name = attendee.DisplayName,
+ Email = attendee.Address,
+ Type = (AttendeeType)attendee.Role + 1
+ })
+ .OrderBy(e => e.Name)
+ .ToList();
+ if (organizer != null)
{
- Id = e.LocalId,
- CalendarId = e.CalendarId,
- Title = e.Subject,
- Description = e.Details,
- Location = e.Location,
- StartDate = e.StartTime,
- AllDay = e.AllDay,
- EndDate = e.StartTime.Add(e.Duration),
- Attendees = e.Invitees != null
- ? ToAttendees(e.Invitees).ToList()
- : new List()
- };
+ attendees.Insert(0, new CalendarEventAttendee() { Name = organizer.DisplayName, Email = organizer.Address, IsOrganizer = true });
+ }
+
+ return attendees;
+ }
+
+ static async Task PlatformCreateCalendarEvent(CalendarEvent newEvent)
+ {
+ await Permissions.RequestAsync();
+
+ if (string.IsNullOrEmpty(newEvent.CalendarId))
+ {
+ return string.Empty;
+ }
+
+ var instance = await CalendarRequest.GetInstanceAsync();
+
+ var appointment = new Appointment();
+ appointment.Subject = newEvent.Title;
+ appointment.Details = newEvent.Description ?? string.Empty;
+ appointment.Location = newEvent.Location ?? string.Empty;
+ appointment.StartTime = newEvent.StartDate;
+ appointment.Duration = newEvent.EndDate.HasValue ? newEvent.EndDate.Value - newEvent.StartDate : TimeSpan.FromDays(1);
+ appointment.AllDay = newEvent.AllDay;
+ appointment.Uri = !string.IsNullOrEmpty(newEvent.Url) ? new Uri(newEvent.Url) : null;
- static IEnumerable ToAttendees(IEnumerable native)
+ if (newEvent.RecurrancePattern != null)
+ {
+ appointment.Recurrence = newEvent.RecurrancePattern.ConvertRule();
+ }
+
+ var calendar = await instance.GetAppointmentCalendarAsync(newEvent.CalendarId);
+ await calendar.SaveAppointmentAsync(appointment);
+
+ if (!string.IsNullOrEmpty(appointment.LocalId))
+ return appointment.LocalId;
+
+ throw new ArgumentException("[UWP]: Could not create appointment with supplied parameters");
+ }
+
+ static async Task PlatformUpdateCalendarEvent(CalendarEvent eventToUpdate)
{
- foreach (var attendee in native)
+ await Permissions.RequestAsync();
+
+ var existingEvent = await GetEventByIdAsync(eventToUpdate.Id);
+
+ Appointment thisEvent = null;
+ var instance = await CalendarRequest.GetInstanceAsync();
+ if (string.IsNullOrEmpty(eventToUpdate.CalendarId) || existingEvent == null)
+ {
+ return false;
+ }
+ else if (existingEvent.CalendarId != eventToUpdate.CalendarId)
+ {
+ await DeleteCalendarEventById(existingEvent.Id, existingEvent.CalendarId);
+ thisEvent = new Appointment();
+ }
+ else
+ {
+ thisEvent = await instance.GetAppointmentAsync(eventToUpdate.Id);
+ }
+
+ if (eventToUpdate.RecurrancePattern != null)
+ {
+ thisEvent.Recurrence = eventToUpdate.RecurrancePattern.ConvertRule();
+ }
+
+ var url = eventToUpdate.Url;
+ if (!string.IsNullOrWhiteSpace(url))
{
- yield return new CalendarEventAttendee
+ url = eventToUpdate.Url;
+ if (!Regex.IsMatch(url, @"^https?:\/\/", RegexOptions.IgnoreCase))
{
- Name = attendee.DisplayName,
- Email = attendee.Address
- };
+ url = "http://" + url;
+ }
}
+
+ thisEvent.Subject = eventToUpdate.Title;
+ thisEvent.Details = eventToUpdate.Description;
+ thisEvent.Location = eventToUpdate.Location;
+ thisEvent.StartTime = eventToUpdate.StartDate;
+ thisEvent.Duration = eventToUpdate.EndDate.HasValue ? eventToUpdate.EndDate.Value - eventToUpdate.StartDate : TimeSpan.FromDays(1);
+ thisEvent.AllDay = eventToUpdate.AllDay;
+ thisEvent.Uri = !string.IsNullOrEmpty(url) ? new Uri(url) : null;
+
+ var calendar = await instance.GetAppointmentCalendarAsync(eventToUpdate.CalendarId);
+ await calendar.SaveAppointmentAsync(thisEvent);
+
+ if (!string.IsNullOrEmpty(thisEvent.LocalId))
+ {
+ return true;
+ }
+ throw new ArgumentException("[UWP]: Could not update appointment with supplied parameters");
+ }
+
+ static async Task PlatformSetEventRecurrenceEndDate(string eventId, DateTimeOffset recurrenceEndDate)
+ {
+ await Permissions.RequestAsync();
+
+ var existingEvent = await GetEventByIdAsync(eventId);
+ var instance = await CalendarRequest.GetInstanceAsync();
+
+ if (string.IsNullOrEmpty(existingEvent?.CalendarId))
+ {
+ return false;
+ }
+ var thisEvent = await instance.GetAppointmentAsync(existingEvent.Id);
+
+ if (existingEvent.RecurrancePattern != null)
+ {
+ existingEvent.RecurrancePattern.EndDate = recurrenceEndDate;
+ existingEvent.RecurrancePattern.TotalOccurrences = null;
+ thisEvent.Recurrence = existingEvent.RecurrancePattern.ConvertRule();
+ }
+
+ var calendar = await instance.GetAppointmentCalendarAsync(existingEvent.CalendarId);
+ await calendar.SaveAppointmentAsync(thisEvent);
+ if (string.IsNullOrEmpty(thisEvent.LocalId))
+ {
+ throw new ArgumentException("[UWP]: Could not update appointments Recurrence End Date with supplied parameters");
+ }
+ return true;
+ }
+
+ static AppointmentRecurrence ConvertRule(this RecurrenceRule recurrenceRule)
+ {
+ var eventRecurrence = new AppointmentRecurrence();
+ eventRecurrence.Unit = (AppointmentRecurrenceUnit)recurrenceRule.Frequency;
+ eventRecurrence.Interval = recurrenceRule.Interval;
+ eventRecurrence.Until = recurrenceRule.EndDate;
+ eventRecurrence.Occurrences = recurrenceRule.TotalOccurrences;
+
+ switch (recurrenceRule.Frequency)
+ {
+ case RecurrenceFrequency.Daily:
+ case RecurrenceFrequency.Weekly:
+ if (recurrenceRule.DaysOfTheWeek != null && recurrenceRule.DaysOfTheWeek.Count > 0)
+ {
+ eventRecurrence.DaysOfWeek = recurrenceRule.DaysOfTheWeek != null && recurrenceRule.DaysOfTheWeek.Count > 0 ? (AppointmentDaysOfWeek)recurrenceRule.DaysOfTheWeek.Sum(x => (int)x) : 0;
+ eventRecurrence.Unit = AppointmentRecurrenceUnit.Weekly;
+ }
+ break;
+ case RecurrenceFrequency.Monthly:
+ case RecurrenceFrequency.MonthlyOnDay:
+ if (recurrenceRule.DaysOfTheWeek != null && recurrenceRule.DaysOfTheWeek.Count > 0)
+ {
+ eventRecurrence.DaysOfWeek = (AppointmentDaysOfWeek)recurrenceRule.DaysOfTheWeek.Sum(x => (int)x);
+ eventRecurrence.WeekOfMonth = (AppointmentWeekOfMonth)recurrenceRule.WeekOfMonth;
+ eventRecurrence.Unit = AppointmentRecurrenceUnit.MonthlyOnDay;
+ }
+ else
+ {
+ eventRecurrence.Day = (uint)recurrenceRule.DayOfTheMonth;
+ }
+ break;
+ case RecurrenceFrequency.Yearly:
+ case RecurrenceFrequency.YearlyOnDay:
+ if (recurrenceRule.DaysOfTheWeek != null && recurrenceRule.DaysOfTheWeek.Count > 0)
+ {
+ eventRecurrence.WeekOfMonth = (AppointmentWeekOfMonth)recurrenceRule.WeekOfMonth;
+ eventRecurrence.DaysOfWeek = (AppointmentDaysOfWeek)recurrenceRule.DaysOfTheWeek.Sum(x => (int)x);
+ eventRecurrence.Unit = AppointmentRecurrenceUnit.YearlyOnDay;
+ }
+ else
+ {
+ eventRecurrence.Day = (uint)recurrenceRule.DayOfTheMonth;
+ }
+ eventRecurrence.Month = (uint)recurrenceRule.MonthOfTheYear;
+ break;
+ }
+ return eventRecurrence;
+ }
+
+ static async Task PlatformCreateCalendar(Calendar newCalendar)
+ {
+ await Permissions.RequestAsync();
+
+ var instance = await CalendarRequest.GetInstanceAsync();
+
+ var calendar = await instance.CreateAppointmentCalendarAsync(newCalendar.Name);
+
+ if (calendar != null)
+ return calendar.LocalId;
+
+ throw new ArgumentException("[UWP]: Could not create appointment with supplied parameters");
+ }
+
+ static async Task PlatformDeleteCalendarEventInstanceByDate(string eventId, string calendarId, DateTimeOffset dateOfInstanceUtc)
+ {
+ await Permissions.RequestAsync();
+
+ if (string.IsNullOrEmpty(eventId))
+ {
+ throw new ArgumentException("[UWP]: You must supply an event id to delete an event.");
+ }
+ var calendarEvent = await GetEventInstanceByIdAsync(eventId, dateOfInstanceUtc);
+
+ if (calendarEvent.CalendarId != calendarId)
+ {
+ throw new ArgumentOutOfRangeException("[UWP]: Supplied event does not belong to supplied calendar");
+ }
+
+ var mainDisplayInfo = DeviceDisplay.MainDisplayInfo;
+ var rect = new Windows.Foundation.Rect(0, 0, mainDisplayInfo.Width, mainDisplayInfo.Height);
+
+ if (await AppointmentManager.ShowRemoveAppointmentAsync(calendarEvent.Id, rect, Windows.UI.Popups.Placement.Default, calendarEvent.StartDate))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ static async Task PlatformDeleteCalendarEventById(string eventId, string calendarId)
+ {
+ await Permissions.RequestAsync();
+
+ if (string.IsNullOrEmpty(eventId))
+ {
+ throw new ArgumentException("[UWP]: You must supply an event id to delete an event.");
+ }
+ var calendarEvent = await GetEventByIdAsync(eventId);
+
+ if (calendarEvent.CalendarId != calendarId)
+ {
+ throw new ArgumentOutOfRangeException("[UWP]: Supplied event does not belong to supplied calendar");
+ }
+
+ var mainDisplayInfo = DeviceDisplay.MainDisplayInfo;
+ var rect = new Windows.Foundation.Rect(0, 0, mainDisplayInfo.Width, mainDisplayInfo.Height);
+
+ if (await AppointmentManager.ShowRemoveAppointmentAsync(eventId, rect, Windows.UI.Popups.Placement.Default))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ static async Task PlatformAddAttendeeToEvent(CalendarEventAttendee newAttendee, string eventId)
+ {
+ await Permissions.RequestAsync();
+
+ var instance = await CalendarRequest.GetInstanceAsync();
+
+ var calendarEvent = await instance.GetAppointmentAsync(eventId);
+ var calendar = await instance.GetAppointmentCalendarAsync(calendarEvent.CalendarId);
+ var cntInvitiees = calendarEvent.Invitees.Count;
+
+ if (calendarEvent == null)
+ throw new ArgumentException("[UWP]: You must supply a valid event id to add an attendee to.");
+
+ calendarEvent.Invitees.Add(new AppointmentInvitee() { DisplayName = newAttendee.Name, Address = newAttendee.Email, Role = (AppointmentParticipantRole)(newAttendee.Type - 1) });
+ await calendar.SaveAppointmentAsync(calendarEvent);
+
+ return calendarEvent.Invitees.Count == cntInvitiees + 1;
+ }
+
+ static async Task PlatformRemoveAttendeeFromEvent(CalendarEventAttendee newAttendee, string eventId)
+ {
+ await Permissions.RequestAsync();
+
+ var instance = await CalendarRequest.GetInstanceAsync();
+
+ var calendarEvent = await instance.GetAppointmentAsync(eventId);
+ var calendar = await instance.GetAppointmentCalendarAsync(calendarEvent.CalendarId);
+
+ if (calendarEvent == null)
+ throw new ArgumentException("[UWP]: You must supply a valid event id to remove an attendee from.");
+
+ var attendeeToRemove = calendarEvent.Invitees.Where(x => x.DisplayName == newAttendee.Name && x.Address == newAttendee.Email).FirstOrDefault();
+
+ calendarEvent.Invitees.Remove(attendeeToRemove);
+
+ await calendar.SaveAppointmentAsync(calendarEvent);
+
+ return attendeeToRemove != null;
}
}
}
diff --git a/Xamarin.Essentials/Permissions/Permissions.ios.cs b/Xamarin.Essentials/Permissions/Permissions.ios.cs
index 232ba3325..65c0636a7 100644
--- a/Xamarin.Essentials/Permissions/Permissions.ios.cs
+++ b/Xamarin.Essentials/Permissions/Permissions.ios.cs
@@ -4,6 +4,8 @@
using System.Threading.Tasks;
using AddressBook;
using AVFoundation;
+using EventKit;
+using Foundation;
using MediaPlayer;
using Speech;
@@ -313,5 +315,17 @@ internal static Task RequestSpeechPermission()
return tcs.Task;
}
}
+
+ static Task RequestRemindersAsync()
+ {
+ var tcs = new TaskCompletionSource(CalendarRequest.Instance);
+ CalendarRequest.Instance.RequestAccess(
+ EKEntityType.Reminder,
+ (bool granted, NSError e) =>
+ {
+ tcs.SetResult(granted ? PermissionStatus.Granted : PermissionStatus.Denied);
+ });
+ return tcs.Task;
+ }
}
}
diff --git a/Xamarin.Essentials/Types/Calendar.shared.cs b/Xamarin.Essentials/Types/Calendar.shared.cs
index 1ba0e17a7..0ac9bdd10 100644
--- a/Xamarin.Essentials/Types/Calendar.shared.cs
+++ b/Xamarin.Essentials/Types/Calendar.shared.cs
@@ -3,36 +3,19 @@
namespace Xamarin.Essentials
{
+ [Preserve(AllMembers = true)]
public class Calendar
{
- public Calendar()
- {
- }
-
- public Calendar(string id, string name)
- {
- Id = id;
- Name = name;
- }
-
public string Id { get; set; }
public string Name { get; set; }
+
+ public bool IsReadOnly { get; set; }
}
+ [Preserve(AllMembers = true)]
public class CalendarEvent
{
- public CalendarEvent()
- {
- }
-
- public CalendarEvent(string id, string calendarId, string title)
- {
- Id = id;
- CalendarId = calendarId;
- Title = title;
- }
-
public string Id { get; set; }
public string CalendarId { get; set; }
@@ -43,32 +26,138 @@ public CalendarEvent(string id, string calendarId, string title)
public string Location { get; set; }
- public bool AllDay { get; set; }
+ public bool AllDay
+ {
+ get => !EndDate.HasValue;
+ set => EndDate = value ? (DateTimeOffset?)null : StartDate;
+ }
public DateTimeOffset StartDate { get; set; }
- public DateTimeOffset EndDate { get; set; }
+ public TimeSpan? Duration
+ {
+ get => EndDate.HasValue ? EndDate - StartDate : null;
+ set => EndDate = value.HasValue ? StartDate.Add(value.Value) : (DateTimeOffset?)null;
+ }
- public TimeSpan Duration =>
- AllDay ? TimeSpan.FromDays(1) : EndDate - StartDate;
+ public string Url { get; set; }
+
+ public DateTimeOffset? EndDate { get; set; }
public IEnumerable Attendees { get; set; }
+
+ public IEnumerable Reminders { get; set; }
+
+ public RecurrenceRule RecurrancePattern { get; set; }
}
+ [Preserve(AllMembers = true)]
public class CalendarEventAttendee
{
- public CalendarEventAttendee()
- {
- }
-
- public CalendarEventAttendee(string name, string email)
- {
- Name = name;
- Email = email;
- }
-
public string Name { get; set; }
public string Email { get; set; }
+
+ public AttendeeType Type { get; set; }
+
+ public bool IsOrganizer { get; set; }
+ }
+
+ [Preserve(AllMembers = true)]
+ public class CalendarEventReminder
+ {
+ public int MinutesPriorToEventStart { get; set; }
+ }
+
+ [Preserve(AllMembers = true)]
+ public class RecurrenceRule
+ {
+ public uint? TotalOccurrences { get; set; }
+
+ public uint Interval { get; set; }
+
+ public DateTimeOffset? EndDate { get; set; }
+
+ public RecurrenceFrequency? Frequency { get; set; }
+
+ // Only allow event to occur on these days [not available for daily]
+ public List DaysOfTheWeek { get; set; }
+
+ public uint DayOfTheMonth { get; set; }
+
+ public MonthOfYear? MonthOfTheYear { get; set; }
+
+ public IterationOffset? WeekOfMonth { get; set; }
+ }
+
+ public enum RecurrenceFrequency
+ {
+ None = -1,
+ Daily = 0,
+ Weekly = 1,
+ Monthly = 2,
+ MonthlyOnDay = 3,
+ Yearly = 4,
+ YearlyOnDay = 5
+ }
+
+ public enum CalendarDayOfWeek
+ {
+ None = 0,
+ Sunday = 1,
+ Monday = 2,
+ Tuesday = 4,
+ Wednesday = 8,
+ Thursday = 16,
+ Friday = 32,
+ Saturday = 64,
+ Weekday = Monday | Tuesday | Wednesday | Thursday | Friday,
+ Weekend = Saturday | Sunday,
+ AllDays = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday,
+ }
+
+ public enum MonthOfYear
+ {
+ January = 1,
+ February = 2,
+ March = 3,
+ April = 4,
+ May = 5,
+ June = 6,
+ July = 7,
+ August = 8,
+ September = 9,
+ October = 10,
+ November = 11,
+ December = 12
+ }
+
+#if __ANDROID__ || __IOS__
+ public enum IterationOffset
+ {
+ Last = -1,
+ First = 1,
+ Second = 2,
+ Third = 3,
+ Fourth = 4
+ }
+
+#else
+ public enum IterationOffset
+ {
+ First = 0,
+ Second = 1,
+ Third = 2,
+ Fourth = 3,
+ Last = 4
+ }
+
+#endif
+ public enum AttendeeType
+ {
+ None = 0,
+ Required = 1,
+ Optional = 2,
+ Resource = 3
}
}
diff --git a/Xamarin.Essentials/Types/PlatformExtensions/CalendarExtensions.ios.cs b/Xamarin.Essentials/Types/PlatformExtensions/CalendarExtensions.ios.cs
new file mode 100644
index 000000000..a20e99b98
--- /dev/null
+++ b/Xamarin.Essentials/Types/PlatformExtensions/CalendarExtensions.ios.cs
@@ -0,0 +1,14 @@
+using System;
+using Foundation;
+
+namespace Xamarin.Essentials
+{
+ public static partial class CalendarExtensions
+ {
+ // https://developer.apple.com/documentation/foundation/nsdate
+ // NSDate minimum date is 2001/01/01
+ static DateTime iosNSDateTimeSystemZeroPoint = new DateTime(2001, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
+
+ public static NSDate ToNSDate(this DateTimeOffset date) => NSDate.FromTimeIntervalSinceReferenceDate((date.UtcDateTime - iosNSDateTimeSystemZeroPoint).TotalSeconds);
+ }
+}
diff --git a/Xamarin.Essentials/Types/PlatformExtensions/NumberExtensions.shared.cs b/Xamarin.Essentials/Types/PlatformExtensions/NumberExtensions.shared.cs
new file mode 100644
index 000000000..00dea9d63
--- /dev/null
+++ b/Xamarin.Essentials/Types/PlatformExtensions/NumberExtensions.shared.cs
@@ -0,0 +1,21 @@
+namespace Xamarin.Essentials
+{
+ public static partial class NumberExtensions
+ {
+ public static string ToOrdinal(this int num)
+ {
+ var modedNum = num % 10;
+ if (((num / 10) % 10) == 1)
+ {
+ return $"{num}th";
+ }
+ switch (modedNum)
+ {
+ case 1: return $"{num}st";
+ case 2: return $"{num}nd";
+ case 3: return $"{num}rd";
+ default: return $"{num}th";
+ }
+ }
+ }
+}