diff --git a/device_calendar/CHANGELOG.md b/device_calendar/CHANGELOG.md index b7a2d22a..e94e8a12 100644 --- a/device_calendar/CHANGELOG.md +++ b/device_calendar/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.2.0 + +* Added time zone support + ## 3.1.0 6th March 2020 - Bug fixes and new features * Boolean variable `isDefault` added for issue [145](https://github.com/builttoroam/flutter_plugins/issues/145) (**NOTE**: This is not supported Android API 16 or lower, `isDefault` will always be false) diff --git a/device_calendar/README.md b/device_calendar/README.md index 6c50ab06..12541f3f 100644 --- a/device_calendar/README.md +++ b/device_calendar/README.md @@ -16,6 +16,10 @@ A cross platform plugin for modifying calendars on the user's device. * **NOTE**: Deleting multiple instances in **Android** takes time to update, you'll see the changes after a few seconds * Ability to add, modify or remove attendees and receive if an attendee is an organiser for an event * Ability to setup reminders for an event +* Ability to specify a time zone for event start and end date + * **NOTE**: Due to a limitation of iOS API, single time zone property is used for iOS (`event.startTimeZone`) + * **NOTE**: For the time zone list, please refer to the `TZ database name` column on [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) + * **NOTE**: If the time zone values are null or invalid, it will be defaulted to the device's current time zone. ## Android Integration diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 59de2f69..f1afa9c0 100644 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -2,7 +2,6 @@ package com.builttoroam.devicecalendar import android.Manifest import android.annotation.SuppressLint -import android.app.Activity import android.content.ContentResolver import android.content.ContentUris import android.content.ContentValues @@ -14,7 +13,6 @@ import android.provider.CalendarContract import android.provider.CalendarContract.CALLER_IS_SYNCADAPTER import android.provider.CalendarContract.Events import android.text.format.DateUtils -import io.flutter.plugin.common.PluginRegistry.Registrar import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_EMAIL_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_NAME_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_PROJECTION @@ -23,29 +21,31 @@ import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_STATUS import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_TYPE_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_COLOR_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX +import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_COLOR_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_DISPLAY_NAME_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ID_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_IS_PRIMARY_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_OLDER_API +import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION +import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_BEGIN_INDEX +import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_END_INDEX +import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_ID_INDEX +import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_LAST_DATE_INDEX +import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_RRULE_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_ALL_DAY_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_BEGIN_INDEX +import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_CUSTOM_APP_URI_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_DESCRIPTION_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_END_INDEX +import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_END_TIMEZONE_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_EVENT_LOCATION_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_CUSTOM_APP_URI_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_ID_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_RECURRING_RULE_INDEX +import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_START_TIMEZONE_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_TITLE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_ID_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_RRULE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_LAST_DATE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_BEGIN_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_END_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.REMINDER_MINUTES_INDEX import com.builttoroam.devicecalendar.common.Constants.Companion.REMINDER_PROJECTION import com.builttoroam.devicecalendar.common.DayOfWeek @@ -65,12 +65,12 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry +import io.flutter.plugin.common.PluginRegistry.Registrar import org.dmfs.rfc5545.DateTime import org.dmfs.rfc5545.Weekday import org.dmfs.rfc5545.recur.Freq import java.text.SimpleDateFormat import java.util.* -import com.builttoroam.devicecalendar.models.CalendarMethodsParametersCacheModel class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { private val RETRIEVE_CALENDARS_REQUEST_CODE = 0 @@ -426,11 +426,10 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } else { values.put(Events.DTSTART, event.start!!) - values.put(Events.DTEND, event.end!!) + values.put(Events.EVENT_TIMEZONE, getTimeZone(event.startTimeZone).id) - // MK using current device time zone - val currentTimeZone: TimeZone = java.util.Calendar.getInstance().timeZone - values.put(Events.EVENT_TIMEZONE, currentTimeZone.id) + values.put(Events.DTEND, event.end!!) + values.put(Events.EVENT_END_TIMEZONE, getTimeZone(event.endTimeZone).id) } values.put(Events.TITLE, event.title) values.put(Events.DESCRIPTION, event.description) @@ -446,6 +445,18 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return values } + private fun getTimeZone(timeZoneString: String?): TimeZone { + val deviceTimeZone: TimeZone = java.util.Calendar.getInstance().timeZone + var timeZone = TimeZone.getTimeZone(timeZoneString ?: deviceTimeZone.id) + + // Invalid time zone names defaults to GMT so update that to be device's time zone + if (timeZone.id == "GMT" && timeZoneString != "GMT") { + timeZone = TimeZone.getTimeZone(deviceTimeZone.id) + } + + return timeZone + } + @SuppressLint("MissingPermission") private fun insertAttendees(attendees: List, eventId: Long?, contentResolver: ContentResolver?) { if (attendees.isEmpty()) { @@ -637,7 +648,9 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val recurringRule = cursor.getString(EVENT_PROJECTION_RECURRING_RULE_INDEX) val allDay = cursor.getInt(EVENT_PROJECTION_ALL_DAY_INDEX) > 0 val location = cursor.getString(EVENT_PROJECTION_EVENT_LOCATION_INDEX) - var url = cursor.getString(EVENT_PROJECTION_CUSTOM_APP_URI_INDEX) + val url = cursor.getString(EVENT_PROJECTION_CUSTOM_APP_URI_INDEX) + val startTimeZone = cursor.getString(EVENT_PROJECTION_START_TIMEZONE_INDEX) + val endTimeZone = cursor.getString(EVENT_PROJECTION_END_TIMEZONE_INDEX) val event = Event() event.title = title ?: "New Event" @@ -650,6 +663,8 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { event.location = location event.url = url event.recurrenceRule = parseRecurrenceRuleString(recurringRule) + event.startTimeZone = startTimeZone + event.endTimeZone = endTimeZone return event } diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index ef63e228..649faf37 100644 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -42,6 +42,8 @@ class DeviceCalendarPlugin() : MethodCallHandler { private val EVENT_ALL_DAY_ARGUMENT = "eventAllDay" private val EVENT_START_DATE_ARGUMENT = "eventStartDate" private val EVENT_END_DATE_ARGUMENT = "eventEndDate" + private val EVENT_START_TIMEZONE_ARGUMENT = "eventStartTimeZone" + private val EVENT_END_TIMEZONE_ARGUMENT = "eventEndTimeZone" private val RECURRENCE_RULE_ARGUMENT = "recurrenceRule" private val RECURRENCE_FREQUENCY_ARGUMENT = "recurrenceFrequency" private val TOTAL_OCCURRENCES_ARGUMENT = "totalOccurrences" @@ -145,6 +147,8 @@ class DeviceCalendarPlugin() : MethodCallHandler { event.allDay = call.argument(EVENT_ALL_DAY_ARGUMENT) ?: false event.start = call.argument(EVENT_START_DATE_ARGUMENT)!! event.end = call.argument(EVENT_END_DATE_ARGUMENT)!! + event.startTimeZone = call.argument(EVENT_START_TIMEZONE_ARGUMENT) + event.endTimeZone = call.argument(EVENT_END_TIMEZONE_ARGUMENT) event.location = call.argument(EVENT_LOCATION_ARGUMENT) event.url = call.argument(EVENT_URL_ARGUMENT) diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt index 4a09d862..40d0b367 100644 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -1,6 +1,7 @@ package com.builttoroam.devicecalendar.common import android.provider.CalendarContract +import java.util.* class Constants { companion object { @@ -46,6 +47,8 @@ class Constants { const val EVENT_PROJECTION_ALL_DAY_INDEX: Int = 8 const val EVENT_PROJECTION_EVENT_LOCATION_INDEX: Int = 9 const val EVENT_PROJECTION_CUSTOM_APP_URI_INDEX: Int = 10 + const val EVENT_PROJECTION_START_TIMEZONE_INDEX: Int = 11 + const val EVENT_PROJECTION_END_TIMEZONE_INDEX: Int = 12 val EVENT_PROJECTION: Array = arrayOf( CalendarContract.Instances.EVENT_ID, @@ -58,7 +61,9 @@ class Constants { CalendarContract.Events.RRULE, CalendarContract.Events.ALL_DAY, CalendarContract.Events.EVENT_LOCATION, - CalendarContract.Events.CUSTOM_APP_URI + CalendarContract.Events.CUSTOM_APP_URI, + CalendarContract.Events.EVENT_TIMEZONE, + CalendarContract.Events.EVENT_END_TIMEZONE ) const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt index f85a5809..de9538d4 100644 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt @@ -7,6 +7,8 @@ class Event { var description: String? = null var start: Long? = null var end: Long? = null + var startTimeZone: String? = null + var endTimeZone: String? = null var allDay: Boolean = false var location: String? = null var url: String? = null diff --git a/device_calendar/example/lib/presentation/pages/calendar_event.dart b/device_calendar/example/lib/presentation/pages/calendar_event.dart index 73c5caff..52adeb8a 100644 --- a/device_calendar/example/lib/presentation/pages/calendar_event.dart +++ b/device_calendar/example/lib/presentation/pages/calendar_event.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/services.dart'; import 'event_attendee.dart'; @@ -230,6 +232,19 @@ class _CalendarEventPageState extends State { ), ), if (!_event.allDay) ... [ + if (Platform.isAndroid) + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event.startTimeZone, + decoration: const InputDecoration( + labelText: 'Start date time zone', + hintText: 'Australia/Sydney'), + onSaved: (String value) { + _event.startTimeZone = value; + }, + ), + ), Padding( padding: const EdgeInsets.all(10.0), child: DateTimePicker( @@ -256,6 +271,18 @@ class _CalendarEventPageState extends State { }, ), ), + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: Platform.isAndroid ? _event.endTimeZone : _event.startTimeZone, + decoration: InputDecoration( + labelText: Platform.isAndroid ? 'End date time zone' : 'Start and end time zone', + hintText: 'Australia/Sydney'), + onSaved: (String value) => Platform.isAndroid + ? _event.endTimeZone = value + : _event.startTimeZone = value, + ), + ), ], GestureDetector( onTap: () async { diff --git a/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift b/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift index d802ea4f..ceedaf6a 100644 --- a/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -30,6 +30,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin { let description: String? let start: Int64 let end: Int64 + let startTimeZone: String? let allDay: Bool let attendees: [Attendee] let location: String? @@ -90,6 +91,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin { let eventAllDayArgument = "eventAllDay" let eventStartDateArgument = "eventStartDate" let eventEndDateArgument = "eventEndDate" + let eventStartTimeZoneArgument = "eventStartTimeZone" let eventLocationArgument = "eventLocation" let eventURLArgument = "eventURL" let attendeesArgument = "attendees" @@ -294,6 +296,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin { description: ekEvent.notes, start: Int64(ekEvent.startDate.millisecondsSinceEpoch), end: Int64(ekEvent.endDate.millisecondsSinceEpoch), + startTimeZone: ekEvent.timeZone?.identifier, allDay: ekEvent.isAllDay, attendees: attendees, location: ekEvent.location, @@ -531,6 +534,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin { let endDateDateMillisecondsSinceEpoch = arguments[eventEndDateArgument] as! NSNumber let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch.doubleValue / 1000.0) let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch.doubleValue / 1000.0) + let startTimeZoneString = arguments[eventStartTimeZoneArgument] as? String let title = arguments[self.eventTitleArgument] as! String let description = arguments[self.eventDescriptionArgument] as? String let location = arguments[self.eventLocationArgument] as? String @@ -562,7 +566,12 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin { ekEvent!.isAllDay = isAllDay ekEvent!.startDate = startDate if (isAllDay) { ekEvent!.endDate = startDate } - else { ekEvent!.endDate = endDate } + else { + ekEvent!.endDate = endDate + + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current + ekEvent!.timeZone = timeZone + } ekEvent!.calendar = ekCalendar! ekEvent!.location = location @@ -732,8 +741,14 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin { } } -extension UIColor { +extension Date { + func convert(from initTimeZone: TimeZone, to targetTimeZone: TimeZone) -> Date { + let delta = TimeInterval(initTimeZone.secondsFromGMT() - targetTimeZone.secondsFromGMT()) + return addingTimeInterval(delta) + } +} +extension UIColor { func rgb() -> Int? { var fRed : CGFloat = 0 var fGreen : CGFloat = 0 diff --git a/device_calendar/lib/src/device_calendar.dart b/device_calendar/lib/src/device_calendar.dart index 497b8dd7..5acdd272 100644 --- a/device_calendar/lib/src/device_calendar.dart +++ b/device_calendar/lib/src/device_calendar.dart @@ -218,22 +218,7 @@ class DeviceCalendarPlugin { } try { - result.data = - await channel.invokeMethod('createOrUpdateEvent', { - 'calendarId': event.calendarId, - 'eventId': event.eventId, - 'eventTitle': event.title, - 'eventDescription': event.description, - 'eventLocation': event.location, - 'eventAllDay': event.allDay, - 'eventStartDate': event.start.millisecondsSinceEpoch, - 'eventEndDate': event.end.millisecondsSinceEpoch, - 'eventLocation': event.location, - 'eventURL': event.url?.data?.contentText, - 'recurrenceRule': event.recurrenceRule?.toJson(), - 'attendees': event.attendees?.map((a) => a.toJson())?.toList(), - 'reminders': event.reminders?.map((r) => r.toJson())?.toList() - }); + result.data = await channel.invokeMethod('createOrUpdateEvent', event.toJson()); } catch (e) { _parsePlatformExceptionAndUpdateResult(e, result); } diff --git a/device_calendar/lib/src/models/event.dart b/device_calendar/lib/src/models/event.dart index 0338a66b..d085d65e 100644 --- a/device_calendar/lib/src/models/event.dart +++ b/device_calendar/lib/src/models/event.dart @@ -8,7 +8,7 @@ class Event { /// Read-only. The unique identifier for this event. This is auto-generated when a new event is created String eventId; - /// The identifier of the calendar that this event is associated with + /// Read-only. The identifier of the calendar that this event is associated with String calendarId; /// The title of this event @@ -23,6 +23,14 @@ class Event { /// Indicates when the event ends DateTime end; + /// Time zone of the event start date\ + /// **Note**: In iOS this will set time zones for both start and end date + String startTimeZone; + + /// Time zone of the event end date\ + /// **Note**: Not used in iOS, only single time zone is used. Please use `startTimeZone` + String endTimeZone; + /// Indicates if this is an all-day event bool allDay; @@ -38,6 +46,7 @@ class Event { /// The recurrence rule for this event RecurrenceRule recurrenceRule; + /// A list of reminders (by minutes) for this event List reminders; Event(this.calendarId, @@ -45,9 +54,12 @@ class Event { this.title, this.start, this.end, + this.startTimeZone, + this.endTimeZone, this.description, this.attendees, this.recurrenceRule, + this.reminders, this.allDay = false}); Event.fromJson(Map json) { @@ -67,6 +79,8 @@ class Event { if (endMillisecondsSinceEpoch != null) { end = DateTime.fromMillisecondsSinceEpoch(endMillisecondsSinceEpoch); } + startTimeZone = json['startTimeZone']; + endTimeZone = json['endTimeZone']; allDay = json['allDay']; location = json['location']; @@ -101,17 +115,20 @@ class Event { } } - // TODO: look at using this method - /* Map toJson() { - final Map data = Map(); - data['eventId'] = eventId; + Map toJson() { + final data = {}; + data['calendarId'] = calendarId; - data['title'] = title; - data['description'] = description; - data['start'] = start.millisecondsSinceEpoch; - data['end'] = end.millisecondsSinceEpoch; - data['allDay'] = allDay; - data['location'] = location; + data['eventId'] = eventId; + data['eventTitle'] = title; + data['eventDescription'] = description; + data['eventStartDate'] = start.millisecondsSinceEpoch; + data['eventEndDate'] = end.millisecondsSinceEpoch; + data['eventStartTimeZone'] = startTimeZone; + data['eventEndTimeZone'] = endTimeZone; + data['eventAllDay'] = allDay; + data['eventLocation'] = location; + data['eventURL'] = url?.data?.contentText; if (attendees != null) { data['attendees'] = attendees.map((a) => a.toJson()).toList(); } @@ -121,6 +138,7 @@ class Event { if (reminders != null) { data['reminders'] = reminders.map((r) => r.toJson()).toList(); } + return data; - }*/ + } }