From 40b113534ca14c8a5f54d288b5f5197b16ee3fe0 Mon Sep 17 00:00:00 2001 From: GoldenSoju <42365471+GoldenSoju@users.noreply.github.com> Date: Thu, 6 Oct 2022 13:47:20 +0900 Subject: [PATCH 01/46] Implementing Rrule package (#403) Co-authored-by: GoldenSoju Co-authored-by: thomassth --- android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 3 +- .../devicecalendar/AvailabilitySerializer.kt | 10 +- .../devicecalendar/CalendarDelegate.kt | 851 +++++--- .../devicecalendar/DayOfWeekSerializer.kt | 14 - .../devicecalendar/DeviceCalendarPlugin.kt | 196 +- .../RecurrenceFrequencySerializer.kt | 15 - .../devicecalendar/common/Constants.kt | 58 +- .../devicecalendar/common/DayOfWeek.kt | 11 - .../devicecalendar/common/ErrorMessages.kt | 15 +- .../common/RecurrenceFrequency.kt | 5 - .../devicecalendar/models/Attendee.kt | 9 +- .../devicecalendar/models/Calendar.kt | 9 +- .../CalendarMethodsParametersCacheModel.kt | 18 +- .../devicecalendar/models/Event.kt | 1 - .../devicecalendar/models/RecurrenceRule.kt | 22 +- example/analysis_options.yaml | 4 +- example/ios/Podfile.lock | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 6 +- example/lib/presentation/event_item.dart | 12 +- .../lib/presentation/pages/calendar_add.dart | 1 - .../presentation/pages/calendar_event.dart | 1703 +++++++++-------- .../presentation/pages/calendar_events.dart | 7 +- example/lib/presentation/pages/calendars.dart | 6 +- .../presentation/pages/event_attendee.dart | 4 +- example/pubspec.yaml | 61 +- ios/Classes/SwiftDeviceCalendarPlugin.swift | 1005 +++++----- lib/device_calendar.dart | 4 +- lib/src/common/calendar_enums.dart | 8 +- lib/src/common/recurrence_frequency.dart | 6 - lib/src/device_calendar.dart | 110 +- lib/src/models/event.dart | 47 +- lib/src/models/recurrence_rule.dart | 132 -- pubspec.yaml | 5 +- test/device_calendar_test.dart | 6 +- 35 files changed, 2396 insertions(+), 1972 deletions(-) delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt delete mode 100644 lib/src/common/recurrence_frequency.dart delete mode 100644 lib/src/models/recurrence_rule.dart diff --git a/android/build.gradle b/android/build.gradle index eb0783b7..66d86743 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -51,6 +51,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.google.code.gson:gson:2.8.8' api 'androidx.appcompat:appcompat:1.3.1' - implementation 'org.dmfs:lib-recur:0.11.2' + implementation 'org.dmfs:lib-recur:0.12.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 2212bc37..3c9d0852 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu May 17 10:56:13 AEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt index 707b9286..5a803a6b 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt @@ -4,9 +4,13 @@ import com.builttoroam.devicecalendar.models.Availability import com.google.gson.* import java.lang.reflect.Type -class AvailabilitySerializer: JsonSerializer { - override fun serialize(src: Availability?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { +class AvailabilitySerializer : JsonSerializer { + override fun serialize( + src: Availability?, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + if (src != null) { return JsonPrimitive(src.name) } return JsonObject() diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 744f636e..1cd3f98a 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -10,62 +10,14 @@ import android.content.pm.PackageManager import android.database.Cursor import android.graphics.Color import android.net.Uri +import android.os.Build import android.os.Handler import android.os.Looper import android.provider.CalendarContract import android.provider.CalendarContract.CALLER_IS_SYNCADAPTER import android.provider.CalendarContract.Events import android.text.format.DateUtils -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 -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_RELATIONSHIP_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_STATUS_INDEX -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_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.CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX -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_AVAILABILITY_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_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_STATUS_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_TITLE_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 -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.GENERIC_ERROR -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.INVALID_ARGUMENT -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.NOT_ALLOWED -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.NOT_AUTHORIZED -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.NOT_FOUND import com.builttoroam.devicecalendar.common.ErrorMessages -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.NOT_AUTHORIZED_MESSAGE -import com.builttoroam.devicecalendar.common.RecurrenceFrequency import com.builttoroam.devicecalendar.models.* import com.builttoroam.devicecalendar.models.Calendar import com.google.gson.Gson @@ -75,44 +27,52 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry import kotlinx.coroutines.* import org.dmfs.rfc5545.DateTime +import org.dmfs.rfc5545.DateTime.UTC import org.dmfs.rfc5545.Weekday -import org.dmfs.rfc5545.recur.Freq -import java.text.SimpleDateFormat +import org.dmfs.rfc5545.recur.RecurrenceRule.WeekdayNum import java.util.* - -class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { - private val RETRIEVE_CALENDARS_REQUEST_CODE = 0 - private val RETRIEVE_EVENTS_REQUEST_CODE = RETRIEVE_CALENDARS_REQUEST_CODE + 1 - private val RETRIEVE_CALENDAR_REQUEST_CODE = RETRIEVE_EVENTS_REQUEST_CODE + 1 - private val CREATE_OR_UPDATE_EVENT_REQUEST_CODE = RETRIEVE_CALENDAR_REQUEST_CODE + 1 - private val DELETE_EVENT_REQUEST_CODE = CREATE_OR_UPDATE_EVENT_REQUEST_CODE + 1 - private val REQUEST_PERMISSIONS_REQUEST_CODE = DELETE_EVENT_REQUEST_CODE + 1 - private val DELETE_CALENDAR_REQUEST_CODE = REQUEST_PERMISSIONS_REQUEST_CODE + 1 - private val PART_TEMPLATE = ";%s=" - private val BYMONTHDAY_PART = "BYMONTHDAY" - private val BYMONTH_PART = "BYMONTH" - private val BYSETPOS_PART = "BYSETPOS" - - private val _cachedParametersMap: MutableMap = mutableMapOf() - private var _binding: ActivityPluginBinding? = null - private var _context: Context? = null +import kotlin.math.absoluteValue +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import com.builttoroam.devicecalendar.common.Constants.Companion as Cst +import com.builttoroam.devicecalendar.common.ErrorCodes.Companion as EC +import com.builttoroam.devicecalendar.common.ErrorMessages.Companion as EM +import org.dmfs.rfc5545.recur.Freq as RruleFreq +import org.dmfs.rfc5545.recur.RecurrenceRule as Rrule + +private const val RETRIEVE_CALENDARS_REQUEST_CODE = 0 +private const val RETRIEVE_EVENTS_REQUEST_CODE = RETRIEVE_CALENDARS_REQUEST_CODE + 1 +private const val RETRIEVE_CALENDAR_REQUEST_CODE = RETRIEVE_EVENTS_REQUEST_CODE + 1 +private const val CREATE_OR_UPDATE_EVENT_REQUEST_CODE = RETRIEVE_CALENDAR_REQUEST_CODE + 1 +private const val DELETE_EVENT_REQUEST_CODE = CREATE_OR_UPDATE_EVENT_REQUEST_CODE + 1 +private const val REQUEST_PERMISSIONS_REQUEST_CODE = DELETE_EVENT_REQUEST_CODE + 1 +private const val DELETE_CALENDAR_REQUEST_CODE = REQUEST_PERMISSIONS_REQUEST_CODE + 1 + +class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : + PluginRegistry.RequestPermissionsResultListener { + + private val _cachedParametersMap: MutableMap = + mutableMapOf() + private var _binding: ActivityPluginBinding? = binding + private var _context: Context? = context private var _gson: Gson? = null private val uiThreadHandler = Handler(Looper.getMainLooper()) - constructor(binding: ActivityPluginBinding?, context: Context) { - _binding = binding - _context = context + init { val gsonBuilder = GsonBuilder() - gsonBuilder.registerTypeAdapter(RecurrenceFrequency::class.java, RecurrenceFrequencySerializer()) - gsonBuilder.registerTypeAdapter(DayOfWeek::class.java, DayOfWeekSerializer()) gsonBuilder.registerTypeAdapter(Availability::class.java, AvailabilitySerializer()) gsonBuilder.registerTypeAdapter(EventStatus::class.java, EventStatusSerializer()) _gson = gsonBuilder.create() } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray): Boolean { - val permissionGranted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + val permissionGranted = + grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED if (!_cachedParametersMap.containsKey(requestCode)) { // this plugin doesn't handle this request code @@ -120,13 +80,17 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } val cachedValues: CalendarMethodsParametersCacheModel = _cachedParametersMap[requestCode] - ?: // unlikely scenario where another plugin is potentially using the same request code but it's not one we are tracking so return to - // indicate we're not handling the request - return false + ?: // unlikely scenario where another plugin is potentially using the same request code but it's not one we are tracking so return to + // indicate we're not handling the request + return false try { if (!permissionGranted) { - finishWithError(NOT_AUTHORIZED, NOT_AUTHORIZED_MESSAGE, cachedValues.pendingChannelResult) + finishWithError( + EC.NOT_AUTHORIZED, + EM.NOT_AUTHORIZED_MESSAGE, + cachedValues.pendingChannelResult + ) return false } @@ -135,22 +99,36 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { retrieveCalendars(cachedValues.pendingChannelResult) } RETRIEVE_EVENTS_REQUEST_CODE -> { - retrieveEvents(cachedValues.calendarId, cachedValues.calendarEventsStartDate, cachedValues.calendarEventsEndDate, cachedValues.calendarEventsIds, cachedValues.pendingChannelResult) + retrieveEvents( + cachedValues.calendarId, + cachedValues.calendarEventsStartDate, + cachedValues.calendarEventsEndDate, + cachedValues.calendarEventsIds, + cachedValues.pendingChannelResult + ) } RETRIEVE_CALENDAR_REQUEST_CODE -> { retrieveCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) } CREATE_OR_UPDATE_EVENT_REQUEST_CODE -> { - createOrUpdateEvent(cachedValues.calendarId, cachedValues.event, cachedValues.pendingChannelResult) + createOrUpdateEvent( + cachedValues.calendarId, + cachedValues.event, + cachedValues.pendingChannelResult + ) } DELETE_EVENT_REQUEST_CODE -> { - deleteEvent(cachedValues.calendarId, cachedValues.eventId, cachedValues.pendingChannelResult) + deleteEvent( + cachedValues.calendarId, + cachedValues.eventId, + cachedValues.pendingChannelResult + ) } REQUEST_PERMISSIONS_REQUEST_CODE -> { finishWithSuccess(permissionGranted, cachedValues.pendingChannelResult) } DELETE_CALENDAR_REQUEST_CODE -> { - deleteCalendar(cachedValues.calendarId,cachedValues.pendingChannelResult) + deleteCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) } } @@ -164,7 +142,10 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { if (arePermissionsGranted()) { finishWithSuccess(true, pendingChannelResult) } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, REQUEST_PERMISSIONS_REQUEST_CODE) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + REQUEST_PERMISSIONS_REQUEST_CODE + ) requestPermissions(parameters) } } @@ -179,9 +160,9 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val contentResolver: ContentResolver? = _context?.contentResolver val uri: Uri = CalendarContract.Calendars.CONTENT_URI val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query(uri, CALENDAR_PROJECTION, null, null, null) + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION, null, null, null) } else { - contentResolver?.query(uri, CALENDAR_PROJECTION_OLDER_API, null, null, null) + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION_OLDER_API, null, null, null) } val calendars: MutableList = mutableListOf() try { @@ -192,22 +173,33 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { finishWithSuccess(_gson?.toJson(calendars), pendingChannelResult) } catch (e: Exception) { - finishWithError(GENERIC_ERROR, e.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) } finally { cursor?.close() } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, RETRIEVE_CALENDARS_REQUEST_CODE) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDARS_REQUEST_CODE + ) requestPermissions(parameters) } } - private fun retrieveCalendar(calendarId: String, pendingChannelResult: MethodChannel.Result, isInternalCall: Boolean = false): Calendar? { + private fun retrieveCalendar( + calendarId: String, + pendingChannelResult: MethodChannel.Result, + isInternalCall: Boolean = false + ): Calendar? { if (isInternalCall || arePermissionsGranted()) { val calendarIdNumber = calendarId.toLongOrNull() if (calendarIdNumber == null) { if (!isInternalCall) { - finishWithError(INVALID_ARGUMENT, CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, + pendingChannelResult + ) } return null } @@ -216,9 +208,21 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val uri: Uri = CalendarContract.Calendars.CONTENT_URI val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query(ContentUris.withAppendedId(uri, calendarIdNumber), CALENDAR_PROJECTION, null, null, null) + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION, + null, + null, + null + ) } else { - contentResolver?.query(ContentUris.withAppendedId(uri, calendarIdNumber), CALENDAR_PROJECTION_OLDER_API, null, null, null) + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION_OLDER_API, + null, + null, + null + ) } try { @@ -231,74 +235,116 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } else { if (!isInternalCall) { - finishWithError(NOT_FOUND, "The calendar with the ID $calendarId could not be found", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) } } } catch (e: Exception) { - finishWithError(GENERIC_ERROR, e.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) } finally { cursor?.close() } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, RETRIEVE_CALENDAR_REQUEST_CODE, calendarId) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDAR_REQUEST_CODE, + calendarId + ) requestPermissions(parameters) } return null } - fun deleteCalendar(calendarId: String, pendingChannelResult: MethodChannel.Result, isInternalCall: Boolean = false): Calendar? { + fun deleteCalendar( + calendarId: String, + pendingChannelResult: MethodChannel.Result, + isInternalCall: Boolean = false + ): Calendar? { if (isInternalCall || arePermissionsGranted()) { val calendarIdNumber = calendarId.toLongOrNull() if (calendarIdNumber == null) { if (!isInternalCall) { - finishWithError(INVALID_ARGUMENT, CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, + pendingChannelResult + ) } return null } val contentResolver: ContentResolver? = _context?.contentResolver - val calendar = retrieveCalendar(calendarId,pendingChannelResult,true); - if(calendar != null) { - val calenderUriWithId = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarIdNumber) + val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) + if (calendar != null) { + val calenderUriWithId = ContentUris.withAppendedId( + CalendarContract.Calendars.CONTENT_URI, + calendarIdNumber + ) val deleteSucceeded = contentResolver?.delete(calenderUriWithId, null, null) ?: 0 finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) - }else { + } else { if (!isInternalCall) { - finishWithError(NOT_FOUND, "The calendar with the ID $calendarId could not be found", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) } } } else { val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult = pendingChannelResult, - calendarDelegateMethodCode = DELETE_CALENDAR_REQUEST_CODE, - calendarId = calendarId) + pendingChannelResult = pendingChannelResult, + calendarDelegateMethodCode = DELETE_CALENDAR_REQUEST_CODE, + calendarId = calendarId + ) requestPermissions(parameters) } return null } - fun createCalendar(calendarName: String, calendarColor: String?, localAccountName: String, pendingChannelResult: MethodChannel.Result) { + fun createCalendar( + calendarName: String, + calendarColor: String?, + localAccountName: String, + pendingChannelResult: MethodChannel.Result + ) { val contentResolver: ContentResolver? = _context?.contentResolver var uri = CalendarContract.Calendars.CONTENT_URI uri = uri.buildUpon() - .appendQueryParameter(CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, localAccountName) - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - .build() + .appendQueryParameter(CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, localAccountName) + .appendQueryParameter( + CalendarContract.Calendars.ACCOUNT_TYPE, + CalendarContract.ACCOUNT_TYPE_LOCAL + ) + .build() val values = ContentValues() values.put(CalendarContract.Calendars.NAME, calendarName) values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, calendarName) values.put(CalendarContract.Calendars.ACCOUNT_NAME, localAccountName) values.put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - values.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER) - values.put(CalendarContract.Calendars.CALENDAR_COLOR, Color.parseColor((calendarColor - ?: "0xFFFF0000").replace("0x", "#"))) // Red colour as a default + values.put( + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, + CalendarContract.Calendars.CAL_ACCESS_OWNER + ) + values.put( + CalendarContract.Calendars.CALENDAR_COLOR, Color.parseColor( + (calendarColor + ?: "0xFFFF0000").replace("0x", "#") + ) + ) // Red colour as a default values.put(CalendarContract.Calendars.OWNER_ACCOUNT, localAccountName) - values.put(CalendarContract.Calendars.CALENDAR_TIME_ZONE, java.util.Calendar.getInstance().timeZone.id) + values.put( + CalendarContract.Calendars.CALENDAR_TIME_ZONE, + java.util.Calendar.getInstance().timeZone.id + ) val result = contentResolver?.insert(uri, values) // Get the calendar ID that is the last element in the Uri @@ -307,16 +353,30 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { finishWithSuccess(calendarId.toString(), pendingChannelResult) } - fun retrieveEvents(calendarId: String, startDate: Long?, endDate: Long?, eventIds: List, pendingChannelResult: MethodChannel.Result) { + fun retrieveEvents( + calendarId: String, + startDate: Long?, + endDate: Long?, + eventIds: List, + pendingChannelResult: MethodChannel.Result + ) { if (startDate == null && endDate == null && eventIds.isEmpty()) { - finishWithError(INVALID_ARGUMENT, ErrorMessages.RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + ErrorMessages.RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE, + pendingChannelResult + ) return } if (arePermissionsGranted()) { val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) if (calendar == null) { - finishWithError(NOT_FOUND, "Couldn't retrieve the Calendar with ID $calendarId", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) return } @@ -328,7 +388,8 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val eventsUri = eventsUriBuilder.build() val eventsCalendarQuery = "(${Events.CALENDAR_ID} = $calendarId)" val eventsNotDeletedQuery = "(${Events.DELETED} != 1)" - val eventsIdsQuery = "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" + val eventsIdsQuery = + "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" var eventsSelectionQuery = "$eventsCalendarQuery AND $eventsNotDeletedQuery" if (eventIds.isNotEmpty()) { @@ -336,13 +397,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } val eventsSortOrder = Events.DTSTART + " DESC" - val eventsCursor = contentResolver?.query(eventsUri, EVENT_PROJECTION, eventsSelectionQuery, null, eventsSortOrder) + val eventsCursor = contentResolver?.query( + eventsUri, + Cst.EVENT_PROJECTION, + eventsSelectionQuery, + null, + eventsSortOrder + ) val events: MutableList = mutableListOf() val exceptionHandler = CoroutineExceptionHandler { _, exception -> uiThreadHandler.post { - finishWithError(GENERIC_ERROR, exception.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) } } @@ -353,7 +420,8 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } for (event in events) { val attendees = retrieveAttendees(calendar, event.eventId!!, contentResolver) - event.organizer = attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } + event.organizer = + attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } event.attendees = attendees event.reminders = retrieveReminders(event.eventId!!, contentResolver) } @@ -366,23 +434,41 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, RETRIEVE_EVENTS_REQUEST_CODE, calendarId, startDate, endDate) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_EVENTS_REQUEST_CODE, + calendarId, + startDate, + endDate + ) requestPermissions(parameters) } return } - fun createOrUpdateEvent(calendarId: String, event: Event?, pendingChannelResult: MethodChannel.Result) { + fun createOrUpdateEvent( + calendarId: String, + event: Event?, + pendingChannelResult: MethodChannel.Result + ) { if (arePermissionsGranted()) { if (event == null) { - finishWithError(GENERIC_ERROR, CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, pendingChannelResult) + finishWithError( + EC.GENERIC_ERROR, + EM.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, + pendingChannelResult + ) return } val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) if (calendar == null) { - finishWithError(NOT_FOUND, "Couldn't retrieve the Calendar with ID $calendarId", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) return } @@ -391,7 +477,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val exceptionHandler = CoroutineExceptionHandler { _, exception -> uiThreadHandler.post { - finishWithError(GENERIC_ERROR, exception.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) } } @@ -407,14 +493,22 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } else { job = GlobalScope.launch(Dispatchers.IO + exceptionHandler) { - contentResolver?.update(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), values, null, null) - val existingAttendees = retrieveAttendees(calendar, eventId.toString(), contentResolver) - val attendeesToDelete = if (event.attendees.isNotEmpty()) existingAttendees.filter { existingAttendee -> event.attendees.all { it.emailAddress != existingAttendee.emailAddress } } else existingAttendees + contentResolver?.update( + ContentUris.withAppendedId(Events.CONTENT_URI, eventId), + values, + null, + null + ) + val existingAttendees = + retrieveAttendees(calendar, eventId.toString(), contentResolver) + val attendeesToDelete = + if (event.attendees.isNotEmpty()) existingAttendees.filter { existingAttendee -> event.attendees.all { it.emailAddress != existingAttendee.emailAddress } } else existingAttendees for (attendeeToDelete in attendeesToDelete) { deleteAttendee(eventId, attendeeToDelete, contentResolver) } - val attendeesToInsert = event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } + val attendeesToInsert = + event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } insertAttendees(attendeesToInsert, eventId, contentResolver) deleteExistingReminders(contentResolver, eventId) insertReminders(event.reminders, eventId, contentResolver!!) @@ -427,13 +521,13 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } if (existingSelfAttendee != null && newSelfAttendee != null && newSelfAttendee.attendanceStatus != null && - existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus) { + existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus + ) { updateAttendeeStatus(eventId, newSelfAttendee, contentResolver) } } } - job.invokeOnCompletion { - cause -> + job.invokeOnCompletion { cause -> if (cause == null) { uiThreadHandler.post { finishWithSuccess(eventId.toString(), pendingChannelResult) @@ -441,21 +535,28 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, CREATE_OR_UPDATE_EVENT_REQUEST_CODE, calendarId) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + CREATE_OR_UPDATE_EVENT_REQUEST_CODE, + calendarId + ) parameters.event = event requestPermissions(parameters) } } private fun deleteExistingReminders(contentResolver: ContentResolver?, eventId: Long) { - val cursor = CalendarContract.Reminders.query(contentResolver, eventId, arrayOf( - CalendarContract.Reminders._ID - )) + val cursor = CalendarContract.Reminders.query( + contentResolver, eventId, arrayOf( + CalendarContract.Reminders._ID + ) + ) while (cursor != null && cursor.moveToNext()) { var reminderUri: Uri? = null val reminderId = cursor.getLong(0) if (reminderId > 0) { - reminderUri = ContentUris.withAppendedId(CalendarContract.Reminders.CONTENT_URI, reminderId) + reminderUri = + ContentUris.withAppendedId(CalendarContract.Reminders.CONTENT_URI, reminderId) } if (reminderUri != null) { contentResolver?.delete(reminderUri, null, null) @@ -465,7 +566,11 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun insertReminders(reminders: List, eventId: Long?, contentResolver: ContentResolver) { + private fun insertReminders( + reminders: List, + eventId: Long?, + contentResolver: ContentResolver + ) { if (reminders.isEmpty()) { return } @@ -481,25 +586,45 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { private fun buildEventContentValues(event: Event, calendarId: String): ContentValues { val values = ContentValues() - val duration: String? = null - values.put(Events.ALL_DAY, event.eventAllDay) + + values.put(Events.ALL_DAY, if (event.eventAllDay) 1 else 0) values.put(Events.DTSTART, event.eventStartDate!!) values.put(Events.EVENT_TIMEZONE, getTimeZone(event.eventStartTimeZone).id) - values.put(Events.DTEND, event.eventEndDate!!) - values.put(Events.EVENT_END_TIMEZONE, getTimeZone(event.eventEndTimeZone).id) values.put(Events.TITLE, event.eventTitle) values.put(Events.DESCRIPTION, event.eventDescription) values.put(Events.EVENT_LOCATION, event.eventLocation) values.put(Events.CUSTOM_APP_URI, event.eventURL) values.put(Events.CALENDAR_ID, calendarId) - values.put(Events.DURATION, duration) values.put(Events.AVAILABILITY, getAvailability(event.availability)) - values.put(Events.STATUS, getEventStatus(event.eventStatus)) + var status: Int? = getEventStatus(event.eventStatus) + if (status != null) { + values.put(Events.STATUS, status) + } + + var duration: String? = null + var end: Long? = null + var endTimeZone: String? = null if (event.recurrenceRule != null) { val recurrenceRuleParams = buildRecurrenceRuleParams(event.recurrenceRule!!) values.put(Events.RRULE, recurrenceRuleParams) + val difference = event.eventEndDate!!.minus(event.eventStartDate!!) + val rawDuration = difference.toDuration(DurationUnit.MILLISECONDS) + rawDuration.toComponents { days, hours, minutes, seconds, _ -> + if (days > 0 || hours > 0 || minutes > 0 || seconds > 0) duration = "P" + if (days > 0) duration = duration.plus("${days}D") + if (hours > 0 || minutes > 0 || seconds > 0) duration = duration.plus("T") + if (hours > 0) duration = duration.plus("${hours}H") + if (minutes > 0) duration = duration.plus("${minutes}M") + if (seconds > 0) duration = duration.plus("${seconds}S") + } + } else { + end = event.eventEndDate!! + endTimeZone = getTimeZone(event.eventEndTimeZone).id } + values.put(Events.DTEND, end) + values.put(Events.EVENT_END_TIMEZONE, endTimeZone) + values.put(Events.DURATION, duration) return values } @@ -530,7 +655,11 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun insertAttendees(attendees: List, eventId: Long?, contentResolver: ContentResolver?) { + private fun insertAttendees( + attendees: List, + eventId: Long?, + contentResolver: ContentResolver? + ) { if (attendees.isEmpty()) { return } @@ -540,13 +669,13 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { put(CalendarContract.Attendees.ATTENDEE_NAME, it.name) put(CalendarContract.Attendees.ATTENDEE_EMAIL, it.emailAddress) put( - CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, - CalendarContract.Attendees.RELATIONSHIP_ATTENDEE + CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.RELATIONSHIP_ATTENDEE ) put(CalendarContract.Attendees.ATTENDEE_TYPE, it.role) put( - CalendarContract.Attendees.ATTENDEE_STATUS, - it.attendanceStatus + CalendarContract.Attendees.ATTENDEE_STATUS, + it.attendanceStatus ) put(CalendarContract.Attendees.EVENT_ID, eventId) } @@ -556,37 +685,71 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun deleteAttendee(eventId: Long, attendee: Attendee, contentResolver: ContentResolver?) { - val selection = "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" + private fun deleteAttendee( + eventId: Long, + attendee: Attendee, + contentResolver: ContentResolver? + ) { + val selection = + "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" val selectionArgs = arrayOf(eventId.toString() + "", attendee.emailAddress) contentResolver?.delete(CalendarContract.Attendees.CONTENT_URI, selection, selectionArgs) } - private fun updateAttendeeStatus(eventId: Long, attendee: Attendee, contentResolver: ContentResolver?) { - val selection = "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" + private fun updateAttendeeStatus( + eventId: Long, + attendee: Attendee, + contentResolver: ContentResolver? + ) { + val selection = + "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" val selectionArgs = arrayOf(eventId.toString() + "", attendee.emailAddress) val values = ContentValues() values.put(CalendarContract.Attendees.ATTENDEE_STATUS, attendee.attendanceStatus) - contentResolver?.update(CalendarContract.Attendees.CONTENT_URI, values, selection, selectionArgs) + contentResolver?.update( + CalendarContract.Attendees.CONTENT_URI, + values, + selection, + selectionArgs + ) } - fun deleteEvent(calendarId: String, eventId: String, pendingChannelResult: MethodChannel.Result, startDate: Long? = null, endDate: Long? = null, followingInstances: Boolean? = null) { + fun deleteEvent( + calendarId: String, + eventId: String, + pendingChannelResult: MethodChannel.Result, + startDate: Long? = null, + endDate: Long? = null, + followingInstances: Boolean? = null + ) { if (arePermissionsGranted()) { val existingCal = retrieveCalendar(calendarId, pendingChannelResult, true) if (existingCal == null) { - finishWithError(NOT_FOUND, "The calendar with the ID $calendarId could not be found", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) return } if (existingCal.isReadOnly) { - finishWithError(NOT_ALLOWED, "Calendar with ID $calendarId is read-only", pendingChannelResult) + finishWithError( + EC.NOT_ALLOWED, + "Calendar with ID $calendarId is read-only", + pendingChannelResult + ) return } val eventIdNumber = eventId.toLongOrNull() if (eventIdNumber == null) { - finishWithError(INVALID_ARGUMENT, EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + EM.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, + pendingChannelResult + ) return } @@ -597,15 +760,25 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) } else { if (!followingInstances!!) { // Only this instance - val exceptionUriWithId = ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) + val exceptionUriWithId = + ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate!!, endDate!!) + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) while (instanceCursor.moveToNext()) { - val foundEventID = instanceCursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX) + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) if (eventIdNumber == foundEventID) { - values.put(Events.ORIGINAL_INSTANCE_TIME, instanceCursor.getLong(EVENT_INSTANCE_DELETION_BEGIN_INDEX)) + values.put( + Events.ORIGINAL_INSTANCE_TIME, + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_BEGIN_INDEX) + ) values.put(Events.STATUS, Events.STATUS_CANCELED) } } @@ -614,32 +787,52 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { instanceCursor.close() finishWithSuccess(deleteSucceeded != null, pendingChannelResult) } else { // This and following instances - val eventsUriWithId = ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) + val eventsUriWithId = + ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate!!, endDate!!) + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) while (instanceCursor.moveToNext()) { - val foundEventID = instanceCursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX) + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) if (eventIdNumber == foundEventID) { - val newRule = org.dmfs.rfc5545.recur.RecurrenceRule(instanceCursor.getString(EVENT_INSTANCE_DELETION_RRULE_INDEX)) - val lastDate = instanceCursor.getLong(EVENT_INSTANCE_DELETION_LAST_DATE_INDEX) + val newRule = + Rrule(instanceCursor.getString(Cst.EVENT_INSTANCE_DELETION_RRULE_INDEX)) + val lastDate = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_LAST_DATE_INDEX) if (lastDate > 0 && newRule.count != null && newRule.count > 0) { // Update occurrence rule - val cursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate, lastDate) + val cursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate, + lastDate + ) while (cursor.moveToNext()) { - if (eventIdNumber == cursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX)) { + if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { newRule.count-- } } cursor.close() } else { // Indefinite and specified date rule - val cursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate - DateUtils.YEAR_IN_MILLIS, startDate - 1) + val cursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate - DateUtils.YEAR_IN_MILLIS, + startDate - 1 + ) var lastRecurrenceDate: Long? = null while (cursor.moveToNext()) { - if (eventIdNumber == cursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX)) { - lastRecurrenceDate = cursor.getLong(EVENT_INSTANCE_DELETION_END_INDEX) + if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { + lastRecurrenceDate = + cursor.getLong(Cst.EVENT_INSTANCE_DELETION_END_INDEX) } } @@ -660,7 +853,11 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, DELETE_EVENT_REQUEST_CODE, calendarId) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + DELETE_EVENT_REQUEST_CODE, + calendarId + ) parameters.eventId = eventId requestPermissions(parameters) } @@ -683,7 +880,12 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { private fun requestPermissions(requestCode: Int) { if (atLeastAPI(23)) { - _binding!!.activity.requestPermissions(arrayOf(Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR), requestCode) + _binding!!.activity.requestPermissions( + arrayOf( + Manifest.permission.WRITE_CALENDAR, + Manifest.permission.READ_CALENDAR + ), requestCode + ) } } @@ -692,13 +894,13 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return null } - val calId = cursor.getLong(CALENDAR_PROJECTION_ID_INDEX) - val displayName = cursor.getString(CALENDAR_PROJECTION_DISPLAY_NAME_INDEX) - val accessLevel = cursor.getInt(CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX) - val calendarColor = cursor.getInt(CALENDAR_PROJECTION_COLOR_INDEX) - val accountName = cursor.getString(CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX) - val accountType = cursor.getString(CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX) - val ownerAccount = cursor.getString(CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX) + val calId = cursor.getLong(Cst.CALENDAR_PROJECTION_ID_INDEX) + val displayName = cursor.getString(Cst.CALENDAR_PROJECTION_DISPLAY_NAME_INDEX) + val accessLevel = cursor.getInt(Cst.CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX) + val calendarColor = cursor.getInt(Cst.CALENDAR_PROJECTION_COLOR_INDEX) + val accountName = cursor.getString(Cst.CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX) + val accountType = cursor.getString(Cst.CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX) + val ownerAccount = cursor.getString(Cst.CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX) val calendar = Calendar( calId.toString(), @@ -711,7 +913,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { calendar.isReadOnly = isCalendarReadOnly(accessLevel) if (atLeastAPI(17)) { - val isPrimary = cursor.getString(CALENDAR_PROJECTION_IS_PRIMARY_INDEX) + val isPrimary = cursor.getString(Cst.CALENDAR_PROJECTION_IS_PRIMARY_INDEX) calendar.isDefault = isPrimary == "1" } else { calendar.isDefault = false @@ -723,21 +925,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { if (cursor == null) { return null } - - val eventId = cursor.getLong(EVENT_PROJECTION_ID_INDEX) - val title = cursor.getString(EVENT_PROJECTION_TITLE_INDEX) - val description = cursor.getString(EVENT_PROJECTION_DESCRIPTION_INDEX) - val begin = cursor.getLong(EVENT_PROJECTION_BEGIN_INDEX) - val end = cursor.getLong(EVENT_PROJECTION_END_INDEX) - 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) - 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 availability = parseAvailability(cursor.getInt(EVENT_PROJECTION_AVAILABILITY_INDEX)) - val eventStatus = parseEventStatus(cursor.getInt(EVENT_PROJECTION_STATUS_INDEX)) - + val eventId = cursor.getLong(Cst.EVENT_PROJECTION_ID_INDEX) + val title = cursor.getString(Cst.EVENT_PROJECTION_TITLE_INDEX) + val description = cursor.getString(Cst.EVENT_PROJECTION_DESCRIPTION_INDEX) + val begin = cursor.getLong(Cst.EVENT_PROJECTION_BEGIN_INDEX) + val end = cursor.getLong(Cst.EVENT_PROJECTION_END_INDEX) + val recurringRule = cursor.getString(Cst.EVENT_PROJECTION_RECURRING_RULE_INDEX) + val allDay = cursor.getInt(Cst.EVENT_PROJECTION_ALL_DAY_INDEX) > 0 + val location = cursor.getString(Cst.EVENT_PROJECTION_EVENT_LOCATION_INDEX) + val url = cursor.getString(Cst.EVENT_PROJECTION_CUSTOM_APP_URI_INDEX) + val startTimeZone = cursor.getString(Cst.EVENT_PROJECTION_START_TIMEZONE_INDEX) + val endTimeZone = cursor.getString(Cst.EVENT_PROJECTION_END_TIMEZONE_INDEX) + val availability = parseAvailability(cursor.getInt(Cst.EVENT_PROJECTION_AVAILABILITY_INDEX)) + val eventStatus = parseEventStatus(cursor.getInt(Cst.EVENT_PROJECTION_STATUS_INDEX)) val event = Event() event.eventTitle = title ?: "New Event" event.eventId = eventId.toString() @@ -761,64 +961,79 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { if (recurrenceRuleString == null) { return null } - - val rfcRecurrenceRule = org.dmfs.rfc5545.recur.RecurrenceRule(recurrenceRuleString) + val rfcRecurrenceRule = Rrule(recurrenceRuleString) val frequency = when (rfcRecurrenceRule.freq) { - Freq.YEARLY -> RecurrenceFrequency.YEARLY - Freq.MONTHLY -> RecurrenceFrequency.MONTHLY - Freq.WEEKLY -> RecurrenceFrequency.WEEKLY - Freq.DAILY -> RecurrenceFrequency.DAILY + RruleFreq.YEARLY -> RruleFreq.YEARLY + RruleFreq.MONTHLY -> RruleFreq.MONTHLY + RruleFreq.WEEKLY -> RruleFreq.WEEKLY + RruleFreq.DAILY -> RruleFreq.DAILY else -> null - } + } ?: return null + //Avoid handling HOURLY/MINUTELY/SECONDLY frequencies for now - val recurrenceRule = RecurrenceRule(frequency!!) - if (rfcRecurrenceRule.count != null) { - recurrenceRule.totalOccurrences = rfcRecurrenceRule.count - } + val recurrenceRule = RecurrenceRule(frequency) + recurrenceRule.count = rfcRecurrenceRule.count recurrenceRule.interval = rfcRecurrenceRule.interval - if (rfcRecurrenceRule.until != null) { - recurrenceRule.endDate = rfcRecurrenceRule.until.timestamp + + val until = rfcRecurrenceRule.until + if (until != null) { + recurrenceRule.until = formatDateTime(dateTime = until) } - when (rfcRecurrenceRule.freq) { - Freq.WEEKLY, Freq.MONTHLY, Freq.YEARLY -> { - recurrenceRule.daysOfWeek = rfcRecurrenceRule.byDayPart?.mapNotNull { - DayOfWeek.values().find { dayOfWeek -> dayOfWeek.ordinal == it.weekday.ordinal } - }?.toMutableList() + recurrenceRule.sourceRruleString = recurrenceRuleString + + //TODO: Force set to Monday (atm RRULE package only seem to support Monday) + recurrenceRule.wkst = /*rfcRecurrenceRule.weekStart.name*/Weekday.MO.name + recurrenceRule.byday = rfcRecurrenceRule.byDayPart?.mapNotNull { + it.toString() + }?.toMutableList() + recurrenceRule.bymonthday = rfcRecurrenceRule.getByPart(Rrule.Part.BYMONTHDAY) + recurrenceRule.byyearday = rfcRecurrenceRule.getByPart(Rrule.Part.BYYEARDAY) + recurrenceRule.byweekno = rfcRecurrenceRule.getByPart(Rrule.Part.BYWEEKNO) + + // Below adjustment of byMonth ints is necessary as the library somehow gives a wrong int + // See also [buildRecurrenceRuleParams] where 1 is subtracted. + val oldByMonth = rfcRecurrenceRule.getByPart(Rrule.Part.BYMONTH) + if (oldByMonth != null) { + val newByMonth = mutableListOf() + for (month in oldByMonth) { + newByMonth.add(month + 1) } + recurrenceRule.bymonth = newByMonth + } else { + recurrenceRule.bymonth = rfcRecurrenceRule.getByPart(Rrule.Part.BYMONTH) } - val rfcRecurrenceRuleString = rfcRecurrenceRule.toString() - if (rfcRecurrenceRule.freq == Freq.MONTHLY || rfcRecurrenceRule.freq == Freq.YEARLY) { - // Get week number value from BYSETPOS - recurrenceRule.weekOfMonth = convertCalendarPartToNumericValues(rfcRecurrenceRuleString, BYSETPOS_PART) + recurrenceRule.bysetpos = rfcRecurrenceRule.getByPart(Rrule.Part.BYSETPOS) - // If value is not found in BYSETPOS and not repeating by nth day or nth month - // Get the week number value from the BYDAY position - if (recurrenceRule.weekOfMonth == null && rfcRecurrenceRule.byDayPart != null) { - recurrenceRule.weekOfMonth = rfcRecurrenceRule.byDayPart.first().pos - } + return recurrenceRule + } - recurrenceRule.dayOfMonth = convertCalendarPartToNumericValues(rfcRecurrenceRuleString, BYMONTHDAY_PART) + private fun formatDateTime(dateTime: DateTime): String { + assert(dateTime.year in 0..9999) - if (rfcRecurrenceRule.freq == Freq.YEARLY) { - recurrenceRule.monthOfYear = convertCalendarPartToNumericValues(rfcRecurrenceRuleString, BYMONTH_PART) - } + fun twoDigits(n: Int): String { + return if (n < 10) "0$n" else "$n" } - return recurrenceRule - } - - private fun convertCalendarPartToNumericValues(rfcRecurrenceRuleString: String, partName: String): Int? { - val partIndex = rfcRecurrenceRuleString.indexOf(partName) - if (partIndex == -1) { - return null + fun fourDigits(n: Int): String { + val absolute = n.absoluteValue + val sign = if (n < 0) "-" else "" + if (absolute >= 1000) return "$n" + if (absolute >= 100) return "${sign}0$absolute" + if (absolute >= 10) return "${sign}00$absolute" + return "${sign}000$absolute" } - return rfcRecurrenceRuleString.substring(partIndex).split(";").firstOrNull()?.split("=")?.lastOrNull()?.split(",")?.map { - it.toInt() - }?.firstOrNull() + val year = fourDigits(dateTime.year) + val month = twoDigits(dateTime.month.plus(1)) + val day = twoDigits(dateTime.dayOfMonth) + val hour = twoDigits(dateTime.hours) + val minute = twoDigits(dateTime.minutes) + val second = twoDigits(dateTime.seconds) + val utcSuffix = if (dateTime.timeZone == UTC) 'Z' else "" + return "$year-$month-${day}T$hour:$minute:$second$utcSuffix" } private fun parseAttendeeRow(calendar: Calendar, cursor: Cursor?): Attendee? { @@ -826,14 +1041,14 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return null } - val emailAddress = cursor.getString(ATTENDEE_EMAIL_INDEX) + val emailAddress = cursor.getString(Cst.ATTENDEE_EMAIL_INDEX) return Attendee( emailAddress, - cursor.getString(ATTENDEE_NAME_INDEX), - cursor.getInt(ATTENDEE_TYPE_INDEX), - cursor.getInt(ATTENDEE_STATUS_INDEX), - cursor.getInt(ATTENDEE_RELATIONSHIP_INDEX) == CalendarContract.Attendees.RELATIONSHIP_ORGANIZER, + cursor.getString(Cst.ATTENDEE_NAME_INDEX), + cursor.getInt(Cst.ATTENDEE_TYPE_INDEX), + cursor.getInt(Cst.ATTENDEE_STATUS_INDEX), + cursor.getInt(Cst.ATTENDEE_RELATIONSHIP_INDEX) == CalendarContract.Attendees.RELATIONSHIP_ORGANIZER, emailAddress == calendar.ownerAccount ) } @@ -843,7 +1058,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return null } - return Reminder(cursor.getInt(REMINDER_MINUTES_INDEX)) + return Reminder(cursor.getInt(Cst.REMINDER_MINUTES_INDEX)) } private fun isCalendarReadOnly(accessLevel: Int): Boolean { @@ -858,10 +1073,20 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun retrieveAttendees(calendar: Calendar, eventId: String, contentResolver: ContentResolver?): MutableList { + private fun retrieveAttendees( + calendar: Calendar, + eventId: String, + contentResolver: ContentResolver? + ): MutableList { val attendees: MutableList = mutableListOf() val attendeesQuery = "(${CalendarContract.Attendees.EVENT_ID} = ${eventId})" - val attendeesCursor = contentResolver?.query(CalendarContract.Attendees.CONTENT_URI, ATTENDEE_PROJECTION, attendeesQuery, null, null) + val attendeesCursor = contentResolver?.query( + CalendarContract.Attendees.CONTENT_URI, + Cst.ATTENDEE_PROJECTION, + attendeesQuery, + null, + null + ) attendeesCursor.use { cursor -> if (cursor?.moveToFirst() == true) { do { @@ -875,10 +1100,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun retrieveReminders(eventId: String, contentResolver: ContentResolver?): MutableList { + private fun retrieveReminders( + eventId: String, + contentResolver: ContentResolver? + ): MutableList { val reminders: MutableList = mutableListOf() val remindersQuery = "(${CalendarContract.Reminders.EVENT_ID} = ${eventId})" - val remindersCursor = contentResolver?.query(CalendarContract.Reminders.CONTENT_URI, REMINDER_PROJECTION, remindersQuery, null, null) + val remindersCursor = contentResolver?.query( + CalendarContract.Reminders.CONTENT_URI, + Cst.REMINDER_PROJECTION, + remindersQuery, + null, + null + ) remindersCursor.use { cursor -> if (cursor?.moveToFirst() == true) { do { @@ -906,13 +1140,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { clearCachedParameters(pendingChannelResult) } - private fun finishWithError(errorCode: String, errorMessage: String?, pendingChannelResult: MethodChannel.Result) { + private fun finishWithError( + errorCode: String, + errorMessage: String?, + pendingChannelResult: MethodChannel.Result + ) { pendingChannelResult.error(errorCode, errorMessage, null) clearCachedParameters(pendingChannelResult) } private fun clearCachedParameters(pendingChannelResult: MethodChannel.Result) { - val cachedParameters = _cachedParametersMap.values.filter { it.pendingChannelResult == pendingChannelResult }.toList() + val cachedParameters = + _cachedParametersMap.values.filter { it.pendingChannelResult == pendingChannelResult } + .toList() for (cachedParameter in cachedParameters) { if (_cachedParametersMap.containsKey(cachedParameter.ownCacheKey)) { _cachedParametersMap.remove(cachedParameter.ownCacheKey) @@ -921,71 +1161,92 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } private fun atLeastAPI(api: Int): Boolean { - return api <= android.os.Build.VERSION.SDK_INT + return api <= Build.VERSION.SDK_INT } - private fun buildRecurrenceRuleParams(recurrenceRule: RecurrenceRule): String { - val frequencyParam = when (recurrenceRule.recurrenceFrequency) { - RecurrenceFrequency.DAILY -> Freq.DAILY - RecurrenceFrequency.WEEKLY -> Freq.WEEKLY - RecurrenceFrequency.MONTHLY -> Freq.MONTHLY - RecurrenceFrequency.YEARLY -> Freq.YEARLY - } - val rr = org.dmfs.rfc5545.recur.RecurrenceRule(frequencyParam) + private fun buildRecurrenceRuleParams(recurrenceRule: RecurrenceRule): String? { + val frequencyParam = when (recurrenceRule.freq) { + RruleFreq.DAILY -> RruleFreq.DAILY + RruleFreq.WEEKLY -> RruleFreq.WEEKLY + RruleFreq.MONTHLY -> RruleFreq.MONTHLY + RruleFreq.YEARLY -> RruleFreq.YEARLY + else -> null + } ?: return null + + val rr = Rrule(frequencyParam) if (recurrenceRule.interval != null) { rr.interval = recurrenceRule.interval!! } - if (recurrenceRule.recurrenceFrequency == RecurrenceFrequency.WEEKLY || - recurrenceRule.weekOfMonth != null && (recurrenceRule.recurrenceFrequency == RecurrenceFrequency.MONTHLY || recurrenceRule.recurrenceFrequency == RecurrenceFrequency.YEARLY)) { - rr.byDayPart = buildByDayPart(recurrenceRule) + if (recurrenceRule.count != null) { + rr.count = recurrenceRule.count!! + } else if (recurrenceRule.until != null) { + var untilString: String = recurrenceRule.until!! + if (!untilString.endsWith("Z")) { + untilString += "Z" + } + rr.until = parseDateTime(untilString) } - if (recurrenceRule.totalOccurrences != null) { - rr.count = recurrenceRule.totalOccurrences!! - } else if (recurrenceRule.endDate != null) { - val calendar = java.util.Calendar.getInstance() - calendar.timeInMillis = recurrenceRule.endDate!! - val dateFormat = SimpleDateFormat("yyyyMMdd") - dateFormat.timeZone = calendar.timeZone - rr.until = DateTime(calendar.timeZone, recurrenceRule.endDate!!) + if (recurrenceRule.wkst != null) { + rr.weekStart = Weekday.valueOf(recurrenceRule.wkst!!) } - var rrString = rr.toString() - - if (recurrenceRule.monthOfYear != null && recurrenceRule.recurrenceFrequency == RecurrenceFrequency.YEARLY) { - rrString = rrString.addPartWithValues(BYMONTH_PART, recurrenceRule.monthOfYear) + if (recurrenceRule.byday != null) { + rr.byDayPart = recurrenceRule.byday?.mapNotNull { + WeekdayNum.valueOf(it) + }?.toMutableList() } - if (recurrenceRule.recurrenceFrequency == RecurrenceFrequency.MONTHLY || recurrenceRule.recurrenceFrequency == RecurrenceFrequency.YEARLY) { - if (recurrenceRule.weekOfMonth == null) { - rrString = rrString.addPartWithValues(BYMONTHDAY_PART, recurrenceRule.dayOfMonth) - } + if (recurrenceRule.bymonthday != null) { + rr.setByPart(Rrule.Part.BYMONTHDAY, recurrenceRule.bymonthday!!) } - return rrString - } - - private fun buildByDayPart(recurrenceRule: RecurrenceRule): List? { - if (recurrenceRule.daysOfWeek?.isEmpty() == true) { - return null + if (recurrenceRule.byyearday != null) { + rr.setByPart(Rrule.Part.BYYEARDAY, recurrenceRule.byyearday!!) } - return recurrenceRule.daysOfWeek?.mapNotNull { dayOfWeek -> - Weekday.values().firstOrNull { - it.ordinal == dayOfWeek.ordinal + if (recurrenceRule.byweekno != null) { + rr.setByPart(Rrule.Part.BYWEEKNO, recurrenceRule.byweekno!!) + } + // Below adjustment of byMonth ints is necessary as the library somehow gives a wrong int + // See also [parseRecurrenceRuleString] where +1 is added. + if (recurrenceRule.bymonth != null) { + val byMonth = recurrenceRule.bymonth!! + val newMonth = mutableListOf() + byMonth.forEach { + newMonth.add(it - 1) } - }?.map { - org.dmfs.rfc5545.recur.RecurrenceRule.WeekdayNum(recurrenceRule.weekOfMonth ?: 0, it) + rr.setByPart(Rrule.Part.BYMONTH, newMonth) } - } - private fun String.addPartWithValues(partName: String, values: Int?): String { - if (values != null) { - return this + PART_TEMPLATE.format(partName) + values + if (recurrenceRule.bysetpos != null) { + rr.setByPart(Rrule.Part.BYSETPOS, recurrenceRule.bysetpos!!) } + return rr.toString() + } - return this + private fun parseDateTime(string: String): DateTime { + val year = Regex("""(?\d{4})""").pattern + val month = Regex("""(?\d{2})""").pattern + val day = Regex("""(?\d{2})""").pattern + val hour = Regex("""(?\d{2})""").pattern + val minute = Regex("""(?\d{2})""").pattern + val second = Regex("""(?\d{2})""").pattern + + val regEx = Regex("^$year-$month-${day}T$hour:$minute:${second}Z?\$") + + val match = regEx.matchEntire(string) + + return DateTime( + UTC, + match?.groups?.get(1)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(2)?.value?.toIntOrNull()?.minus(1) ?: 0, + match?.groups?.get(3)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(4)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(5)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(6)?.value?.toIntOrNull() ?: 0 + ) } private fun parseAvailability(availability: Int): Availability? = when (availability) { diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt deleted file mode 100644 index b2bbce05..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.builttoroam.devicecalendar - -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.google.gson.* -import java.lang.reflect.Type - -class DayOfWeekSerializer: JsonSerializer { - override fun serialize(src: DayOfWeek?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { - return JsonPrimitive(src.ordinal) - } - return JsonObject() - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index d2aef019..c1f14533 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -4,8 +4,6 @@ import android.app.Activity import android.content.Context import androidx.annotation.NonNull import com.builttoroam.devicecalendar.common.Constants -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.builttoroam.devicecalendar.common.RecurrenceFrequency import com.builttoroam.devicecalendar.models.* import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -13,11 +11,63 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result +import org.dmfs.rfc5545.recur.Freq const val CHANNEL_NAME = "plugins.builttoroam.com/device_calendar" -class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { +// Methods +private const val REQUEST_PERMISSIONS_METHOD = "requestPermissions" +private const val HAS_PERMISSIONS_METHOD = "hasPermissions" +private const val RETRIEVE_CALENDARS_METHOD = "retrieveCalendars" +private const val RETRIEVE_EVENTS_METHOD = "retrieveEvents" +private const val DELETE_EVENT_METHOD = "deleteEvent" +private const val DELETE_EVENT_INSTANCE_METHOD = "deleteEventInstance" +private const val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent" +private const val CREATE_CALENDAR_METHOD = "createCalendar" +private const val DELETE_CALENDAR_METHOD = "deleteCalendar" + +// Method arguments +private const val CALENDAR_ID_ARGUMENT = "calendarId" +private const val CALENDAR_NAME_ARGUMENT = "calendarName" +private const val START_DATE_ARGUMENT = "startDate" +private const val END_DATE_ARGUMENT = "endDate" +private const val EVENT_IDS_ARGUMENT = "eventIds" +private const val EVENT_ID_ARGUMENT = "eventId" +private const val EVENT_TITLE_ARGUMENT = "eventTitle" +private const val EVENT_LOCATION_ARGUMENT = "eventLocation" +private const val EVENT_URL_ARGUMENT = "eventURL" +private const val EVENT_DESCRIPTION_ARGUMENT = "eventDescription" +private const val EVENT_ALL_DAY_ARGUMENT = "eventAllDay" +private const val EVENT_START_DATE_ARGUMENT = "eventStartDate" +private const val EVENT_END_DATE_ARGUMENT = "eventEndDate" +private const val EVENT_START_TIMEZONE_ARGUMENT = "eventStartTimeZone" +private const val EVENT_END_TIMEZONE_ARGUMENT = "eventEndTimeZone" +private const val RECURRENCE_RULE_ARGUMENT = "recurrenceRule" +private const val FREQUENCY_ARGUMENT = "freq" +private const val COUNT_ARGUMENT = "count" +private const val UNTIL_ARGUMENT = "until" +private const val INTERVAL_ARGUMENT = "interval" +private const val BY_WEEK_DAYS_ARGUMENT = "byday" +private const val BY_MONTH_DAYS_ARGUMENT = "bymonthday" +private const val BY_YEAR_DAYS_ARGUMENT = "byyearday" +private const val BY_WEEKS_ARGUMENT = "byweekno" +private const val BY_MONTH_ARGUMENT = "bymonth" +private const val BY_SET_POSITION_ARGUMENT = "bysetpos" + +private const val ATTENDEES_ARGUMENT = "attendees" +private const val EMAIL_ADDRESS_ARGUMENT = "emailAddress" +private const val NAME_ARGUMENT = "name" +private const val ROLE_ARGUMENT = "role" +private const val REMINDERS_ARGUMENT = "reminders" +private const val MINUTES_ARGUMENT = "minutes" +private const val FOLLOWING_INSTANCES = "followingInstances" +private const val CALENDAR_COLOR_ARGUMENT = "calendarColor" +private const val LOCAL_ACCOUNT_NAME_ARGUMENT = "localAccountName" +private const val EVENT_AVAILABILITY_ARGUMENT = "availability" +private const val ATTENDANCE_STATUS_ARGUMENT = "attendanceStatus" +private const val EVENT_STATUS_ARGUMENT = "eventStatus" + +class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { /// The MethodChannel that will the communication between Flutter and native Android /// @@ -27,54 +77,6 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { private var context: Context? = null private var activity: Activity? = null - // Methods - private val REQUEST_PERMISSIONS_METHOD = "requestPermissions" - private val HAS_PERMISSIONS_METHOD = "hasPermissions" - private val RETRIEVE_CALENDARS_METHOD = "retrieveCalendars" - private val RETRIEVE_EVENTS_METHOD = "retrieveEvents" - private val DELETE_EVENT_METHOD = "deleteEvent" - private val DELETE_EVENT_INSTANCE_METHOD = "deleteEventInstance" - private val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent" - private val CREATE_CALENDAR_METHOD = "createCalendar" - private val DELETE_CALENDAR_METHOD = "deleteCalendar" - - // Method arguments - private val CALENDAR_ID_ARGUMENT = "calendarId" - private val CALENDAR_NAME_ARGUMENT = "calendarName" - private val START_DATE_ARGUMENT = "startDate" - private val END_DATE_ARGUMENT = "endDate" - private val EVENT_IDS_ARGUMENT = "eventIds" - private val EVENT_ID_ARGUMENT = "eventId" - private val EVENT_TITLE_ARGUMENT = "eventTitle" - private val EVENT_LOCATION_ARGUMENT = "eventLocation" - private val EVENT_URL_ARGUMENT = "eventURL" - private val EVENT_DESCRIPTION_ARGUMENT = "eventDescription" - 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" - private val INTERVAL_ARGUMENT = "interval" - private val DAYS_OF_WEEK_ARGUMENT = "daysOfWeek" - private val DAY_OF_MONTH_ARGUMENT = "dayOfMonth" - private val MONTH_OF_YEAR_ARGUMENT = "monthOfYear" - private val WEEK_OF_MONTH_ARGUMENT = "weekOfMonth" - private val ATTENDEES_ARGUMENT = "attendees" - private val EMAIL_ADDRESS_ARGUMENT = "emailAddress" - private val NAME_ARGUMENT = "name" - private val ROLE_ARGUMENT = "role" - private val REMINDERS_ARGUMENT = "reminders" - private val MINUTES_ARGUMENT = "minutes" - private val FOLLOWING_INSTANCES = "followingInstances" - private val CALENDAR_COLOR_ARGUMENT = "calendarColor" - private val LOCAL_ACCOUNT_NAME_ARGUMENT = "localAccountName" - private val EVENT_AVAILABILITY_ARGUMENT = "availability" - private val ATTENDANCE_STATUS_ARGUMENT = "attendanceStatus" - private val EVENT_STATUS_ARGUMENT = "eventStatus" - private lateinit var _calendarDelegate: CalendarDelegate override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { @@ -108,7 +110,7 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { activity = null } - override fun onMethodCall(call: MethodCall, result: Result) { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { REQUEST_PERMISSIONS_METHOD -> { _calendarDelegate.requestPermissions(result) @@ -124,13 +126,11 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { val startDate = call.argument(START_DATE_ARGUMENT) val endDate = call.argument(END_DATE_ARGUMENT) val eventIds = call.argument>(EVENT_IDS_ARGUMENT) ?: listOf() - _calendarDelegate.retrieveEvents(calendarId!!, startDate, endDate, eventIds, result) } CREATE_OR_UPDATE_EVENT_METHOD -> { val calendarId = call.argument(CALENDAR_ID_ARGUMENT) val event = parseEventArgs(call, calendarId) - _calendarDelegate.createOrUpdateEvent(calendarId!!, event, result) } DELETE_EVENT_METHOD -> { @@ -146,18 +146,30 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { val endDate = call.argument(EVENT_END_DATE_ARGUMENT) val followingInstances = call.argument(FOLLOWING_INSTANCES) - _calendarDelegate.deleteEvent(calendarId!!, eventId!!, result, startDate, endDate, followingInstances) + _calendarDelegate.deleteEvent( + calendarId!!, + eventId!!, + result, + startDate, + endDate, + followingInstances + ) } CREATE_CALENDAR_METHOD -> { val calendarName = call.argument(CALENDAR_NAME_ARGUMENT) val calendarColor = call.argument(CALENDAR_COLOR_ARGUMENT) val localAccountName = call.argument(LOCAL_ACCOUNT_NAME_ARGUMENT) - _calendarDelegate.createCalendar(calendarName!!, calendarColor, localAccountName!!, result) + _calendarDelegate.createCalendar( + calendarName!!, + calendarColor, + localAccountName!!, + result + ) } DELETE_CALENDAR_METHOD -> { val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - _calendarDelegate.deleteCalendar(calendarId!!,result) + _calendarDelegate.deleteCalendar(calendarId!!, result) } else -> { result.notImplemented() @@ -181,67 +193,91 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { event.availability = parseAvailability(call.argument(EVENT_AVAILABILITY_ARGUMENT)) event.eventStatus = parseEventStatus(call.argument(EVENT_STATUS_ARGUMENT)) - if (call.hasArgument(RECURRENCE_RULE_ARGUMENT) && call.argument>(RECURRENCE_RULE_ARGUMENT) != null) { + if (call.hasArgument(RECURRENCE_RULE_ARGUMENT) && call.argument>( + RECURRENCE_RULE_ARGUMENT + ) != null + ) { val recurrenceRule = parseRecurrenceRuleArgs(call) event.recurrenceRule = recurrenceRule } - if (call.hasArgument(ATTENDEES_ARGUMENT) && call.argument>>(ATTENDEES_ARGUMENT) != null) { + if (call.hasArgument(ATTENDEES_ARGUMENT) && call.argument>>( + ATTENDEES_ARGUMENT + ) != null + ) { event.attendees = mutableListOf() val attendeesArgs = call.argument>>(ATTENDEES_ARGUMENT)!! for (attendeeArgs in attendeesArgs) { - event.attendees.add(Attendee( + event.attendees.add( + Attendee( attendeeArgs[EMAIL_ADDRESS_ARGUMENT] as String, attendeeArgs[NAME_ARGUMENT] as String?, attendeeArgs[ROLE_ARGUMENT] as Int, attendeeArgs[ATTENDANCE_STATUS_ARGUMENT] as Int?, - null, null)) + null, null + ) + ) } } - if (call.hasArgument(REMINDERS_ARGUMENT) && call.argument>>(REMINDERS_ARGUMENT) != null) { + if (call.hasArgument(REMINDERS_ARGUMENT) && call.argument>>( + REMINDERS_ARGUMENT + ) != null + ) { event.reminders = mutableListOf() val remindersArgs = call.argument>>(REMINDERS_ARGUMENT)!! for (reminderArgs in remindersArgs) { event.reminders.add(Reminder(reminderArgs[MINUTES_ARGUMENT] as Int)) } } - return event } private fun parseRecurrenceRuleArgs(call: MethodCall): RecurrenceRule { val recurrenceRuleArgs = call.argument>(RECURRENCE_RULE_ARGUMENT)!! - val recurrenceFrequencyIndex = recurrenceRuleArgs[RECURRENCE_FREQUENCY_ARGUMENT] as Int - val recurrenceRule = RecurrenceRule(RecurrenceFrequency.values()[recurrenceFrequencyIndex]) - if (recurrenceRuleArgs.containsKey(TOTAL_OCCURRENCES_ARGUMENT)) { - recurrenceRule.totalOccurrences = recurrenceRuleArgs[TOTAL_OCCURRENCES_ARGUMENT] as Int + val recurrenceFrequencyString = recurrenceRuleArgs[FREQUENCY_ARGUMENT] as String + val recurrenceFrequency = Freq.valueOf(recurrenceFrequencyString) + val recurrenceRule = RecurrenceRule(recurrenceFrequency) + + if (recurrenceRuleArgs.containsKey(COUNT_ARGUMENT)) { + recurrenceRule.count = recurrenceRuleArgs[COUNT_ARGUMENT] as Int? } if (recurrenceRuleArgs.containsKey(INTERVAL_ARGUMENT)) { recurrenceRule.interval = recurrenceRuleArgs[INTERVAL_ARGUMENT] as Int } - if (recurrenceRuleArgs.containsKey(END_DATE_ARGUMENT)) { - recurrenceRule.endDate = recurrenceRuleArgs[END_DATE_ARGUMENT] as Long + if (recurrenceRuleArgs.containsKey(UNTIL_ARGUMENT)) { + recurrenceRule.until = recurrenceRuleArgs[UNTIL_ARGUMENT] as String? } - if (recurrenceRuleArgs.containsKey(DAYS_OF_WEEK_ARGUMENT)) { - recurrenceRule.daysOfWeek = recurrenceRuleArgs[DAYS_OF_WEEK_ARGUMENT].toListOf()?.map { DayOfWeek.values()[it] }?.toMutableList() + if (recurrenceRuleArgs.containsKey(BY_WEEK_DAYS_ARGUMENT)) { + recurrenceRule.byday = + recurrenceRuleArgs[BY_WEEK_DAYS_ARGUMENT].toListOf()?.toMutableList() } - if (recurrenceRuleArgs.containsKey(DAY_OF_MONTH_ARGUMENT)) { - recurrenceRule.dayOfMonth = recurrenceRuleArgs[DAY_OF_MONTH_ARGUMENT] as Int + if (recurrenceRuleArgs.containsKey(BY_MONTH_DAYS_ARGUMENT)) { + recurrenceRule.bymonthday = + recurrenceRuleArgs[BY_MONTH_DAYS_ARGUMENT] as MutableList? } - if (recurrenceRuleArgs.containsKey(MONTH_OF_YEAR_ARGUMENT)) { - recurrenceRule.monthOfYear = recurrenceRuleArgs[MONTH_OF_YEAR_ARGUMENT] as Int + if (recurrenceRuleArgs.containsKey(BY_YEAR_DAYS_ARGUMENT)) { + recurrenceRule.byyearday = + recurrenceRuleArgs[BY_YEAR_DAYS_ARGUMENT] as MutableList? } - if (recurrenceRuleArgs.containsKey(WEEK_OF_MONTH_ARGUMENT)) { - recurrenceRule.weekOfMonth = recurrenceRuleArgs[WEEK_OF_MONTH_ARGUMENT] as Int + if (recurrenceRuleArgs.containsKey(BY_WEEKS_ARGUMENT)) { + recurrenceRule.byweekno = recurrenceRuleArgs[BY_WEEKS_ARGUMENT] as MutableList? } + if (recurrenceRuleArgs.containsKey(BY_MONTH_ARGUMENT)) { + recurrenceRule.bymonth = recurrenceRuleArgs[BY_MONTH_ARGUMENT] as MutableList? + } + + if (recurrenceRuleArgs.containsKey(BY_SET_POSITION_ARGUMENT)) { + recurrenceRule.bysetpos = + recurrenceRuleArgs[BY_SET_POSITION_ARGUMENT] as MutableList? + } return recurrenceRule } @@ -249,10 +285,6 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { return (this as List<*>?)?.filterIsInstance()?.toList() } - private inline fun Any?.toMutableListOf(): MutableList? { - return this?.toListOf()?.toMutableList() - } - private fun parseAvailability(value: String?): Availability? = if (value == null || value == Constants.AVAILABILITY_UNAVAILABLE) { null diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt deleted file mode 100644 index c4374353..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.builttoroam.devicecalendar - -import com.builttoroam.devicecalendar.common.RecurrenceFrequency -import com.google.gson.* -import java.lang.reflect.Type - -class RecurrenceFrequencySerializer: JsonSerializer { - override fun serialize(src: RecurrenceFrequency?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { - return JsonPrimitive(src.ordinal) - } - return JsonObject() - } - -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt index 95b083a7..9d136ed5 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -15,26 +15,26 @@ class Constants { // API 17 or higher val CALENDAR_PROJECTION: Array = arrayOf( - CalendarContract.Calendars._ID, // 0 - CalendarContract.Calendars.ACCOUNT_NAME, // 1 - CalendarContract.Calendars.ACCOUNT_TYPE, // 2 - CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 - CalendarContract.Calendars.OWNER_ACCOUNT, // 4 - CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 - CalendarContract.Calendars.CALENDAR_COLOR, // 6 - CalendarContract.Calendars.IS_PRIMARY // 7 + CalendarContract.Calendars._ID, // 0 + CalendarContract.Calendars.ACCOUNT_NAME, // 1 + CalendarContract.Calendars.ACCOUNT_TYPE, // 2 + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 + CalendarContract.Calendars.OWNER_ACCOUNT, // 4 + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 + CalendarContract.Calendars.CALENDAR_COLOR, // 6 + CalendarContract.Calendars.IS_PRIMARY // 7 ) // API 16 or lower val CALENDAR_PROJECTION_OLDER_API: Array = arrayOf( - CalendarContract.Calendars._ID, // 0 - CalendarContract.Calendars.ACCOUNT_NAME, // 1 - CalendarContract.Calendars.ACCOUNT_TYPE, // 2 - CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 - CalendarContract.Calendars.OWNER_ACCOUNT, // 4 - CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 - CalendarContract.Calendars.CALENDAR_COLOR // 6 + CalendarContract.Calendars._ID, // 0 + CalendarContract.Calendars.ACCOUNT_NAME, // 1 + CalendarContract.Calendars.ACCOUNT_TYPE, // 2 + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 + CalendarContract.Calendars.OWNER_ACCOUNT, // 4 + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 + CalendarContract.Calendars.CALENDAR_COLOR // 6 ) const val EVENT_PROJECTION_ID_INDEX: Int = 0 @@ -76,11 +76,11 @@ class Constants { const val EVENT_INSTANCE_DELETION_END_INDEX: Int = 4 val EVENT_INSTANCE_DELETION: Array = arrayOf( - CalendarContract.Instances.EVENT_ID, - CalendarContract.Events.RRULE, - CalendarContract.Events.LAST_DATE, - CalendarContract.Instances.BEGIN, - CalendarContract.Instances.END + CalendarContract.Instances.EVENT_ID, + CalendarContract.Events.RRULE, + CalendarContract.Events.LAST_DATE, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END ) const val ATTENDEE_ID_INDEX: Int = 0 @@ -92,19 +92,19 @@ class Constants { const val ATTENDEE_STATUS_INDEX: Int = 6 val ATTENDEE_PROJECTION: Array = arrayOf( - CalendarContract.Attendees._ID, - CalendarContract.Attendees.EVENT_ID, - CalendarContract.Attendees.ATTENDEE_NAME, - CalendarContract.Attendees.ATTENDEE_EMAIL, - CalendarContract.Attendees.ATTENDEE_TYPE, - CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, - CalendarContract.Attendees.ATTENDEE_STATUS + CalendarContract.Attendees._ID, + CalendarContract.Attendees.EVENT_ID, + CalendarContract.Attendees.ATTENDEE_NAME, + CalendarContract.Attendees.ATTENDEE_EMAIL, + CalendarContract.Attendees.ATTENDEE_TYPE, + CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.ATTENDEE_STATUS ) const val REMINDER_MINUTES_INDEX = 1 val REMINDER_PROJECTION: Array = arrayOf( - CalendarContract.Reminders.EVENT_ID, - CalendarContract.Reminders.MINUTES + CalendarContract.Reminders.EVENT_ID, + CalendarContract.Reminders.MINUTES ) const val AVAILABILITY_UNAVAILABLE = "UNAVAILABLE" diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt deleted file mode 100644 index f6c04838..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.builttoroam.devicecalendar.common - -enum class DayOfWeek { - SUNDAY, - MONDAY, - TUESDAY, - WEDNESDAY, - THURSDAY, - FRIDAY, - SATURDAY -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt index e8a8c82c..e8486baa 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt @@ -2,10 +2,15 @@ package com.builttoroam.devicecalendar.common class ErrorMessages { companion object { - const val CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE: String = "Calendar ID is not a number" - const val EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE: String = "Event ID cannot be null on deletion" - const val RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE: String = "Provided arguments (i.e. start, end and event ids) are null or empty" - const val CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE: String = "Some of the event arguments are not valid" - const val NOT_AUTHORIZED_MESSAGE: String = "The user has not allowed this application to modify their calendar(s)" + const val CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE: String = + "Calendar ID is not a number" + const val EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE: String = + "Event ID cannot be null on deletion" + const val RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE: String = + "Provided arguments (i.e. start, end and event ids) are null or empty" + const val CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE: String = + "Some of the event arguments are not valid" + const val NOT_AUTHORIZED_MESSAGE: String = + "The user has not allowed this application to modify their calendar(s)" } } diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt deleted file mode 100644 index 5e9551c9..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.builttoroam.devicecalendar.common - -enum class RecurrenceFrequency { - DAILY, WEEKLY, MONTHLY, YEARLY -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt index 1336d362..825ca964 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt @@ -1,3 +1,10 @@ package com.builttoroam.devicecalendar.models -class Attendee(val emailAddress: String, val name: String?, val role: Int, val attendanceStatus: Int?, val isOrganizer: Boolean?, val isCurrentUser: Boolean?) \ No newline at end of file +class Attendee( + val emailAddress: String, + val name: String?, + val role: Int, + val attendanceStatus: Int?, + val isOrganizer: Boolean?, + val isCurrentUser: Boolean? +) \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt index da17b0d3..09380c22 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -1,6 +1,13 @@ package com.builttoroam.devicecalendar.models -class Calendar(val id: String, val name: String, val color : Int, val accountName: String, val accountType: String, val ownerAccount: String) { +class Calendar( + val id: String, + val name: String, + val color: Int, + val accountName: String, + val accountType: String, + val ownerAccount: String +) { var isReadOnly: Boolean = false var isDefault: Boolean = false } \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt index 955c4768..22bb4c4b 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt @@ -2,13 +2,15 @@ package com.builttoroam.devicecalendar.models import io.flutter.plugin.common.MethodChannel -class CalendarMethodsParametersCacheModel(val pendingChannelResult: MethodChannel.Result, - val calendarDelegateMethodCode: Int, - var calendarId: String = "", - var calendarEventsStartDate: Long? = null, - var calendarEventsEndDate: Long? = null, - var calendarEventsIds: List = listOf(), - var eventId: String = "", - var event: Event? = null) { +class CalendarMethodsParametersCacheModel( + val pendingChannelResult: MethodChannel.Result, + val calendarDelegateMethodCode: Int, + var calendarId: String = "", + var calendarEventsStartDate: Long? = null, + var calendarEventsEndDate: Long? = null, + var calendarEventsIds: List = listOf(), + var eventId: String = "", + var event: Event? = null +) { var ownCacheKey: Int? = null } \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt index 67152245..456e549f 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt @@ -1,6 +1,5 @@ package com.builttoroam.devicecalendar.models - class Event { var eventTitle: String? = null var eventId: String? = null diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt index 659afe52..1da83111 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt @@ -1,15 +1,17 @@ package com.builttoroam.devicecalendar.models -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.builttoroam.devicecalendar.common.RecurrenceFrequency +import org.dmfs.rfc5545.recur.Freq - -class RecurrenceRule(val recurrenceFrequency : RecurrenceFrequency) { - var totalOccurrences: Int? = null +class RecurrenceRule(val freq: Freq) { + var count: Int? = null var interval: Int? = null - var endDate: Long? = null - var daysOfWeek: MutableList? = null - var dayOfMonth: Int? = null - var monthOfYear: Int? = null - var weekOfMonth: Int? = null + var until: String? = null + var sourceRruleString: String? = null + var wkst: String? = null + var byday: MutableList? = null + var bymonthday: MutableList? = null + var byyearday: MutableList? = null + var byweekno: MutableList? = null + var bymonth: MutableList? = null + var bysetpos: MutableList? = null } diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index d4fcc1ad..88d793de 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1,3 @@ -include: package:pedantic/analysis_options.yaml \ No newline at end of file +include: package:pedantic/analysis_options.yaml +#include: package:flutter_lints/flutter.yaml +# TODO: change to flutter lints (https://pub.dev/packages/flutter_lints) \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 09b26adb..f58f9b39 100755 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -31,4 +31,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d3740c426905916d1f2ada0ddfce28cc99f7b7af -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 345447bf..7d88e2f9 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -166,7 +166,7 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 5X4222W8C2; + DevelopmentTeam = PG8Q9ZR89L; LastSwiftMigration = 1130; ProvisioningStyle = Automatic; }; @@ -423,7 +423,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 5X4222W8C2; + DEVELOPMENT_TEAM = PG8Q9ZR89L; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -455,7 +455,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 5X4222W8C2; + DEVELOPMENT_TEAM = PG8Q9ZR89L; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index 2d9c182d..bc002fd9 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -1,11 +1,12 @@ import 'dart:io'; + import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:intl/intl.dart'; +import 'package:timezone/timezone.dart'; import 'recurring_event_dialog.dart'; -import 'package:timezone/timezone.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; class EventItem extends StatefulWidget { final Event? _calendarEvent; @@ -39,7 +40,7 @@ class _EventItemState extends State { @override void initState() { super.initState(); - setCurentLocation(); + WidgetsBinding.instance.addPostFrameCallback((_) => setCurentLocation()); } @override @@ -222,8 +223,7 @@ class _EventItemState extends State { ), Expanded( child: Text( - widget._calendarEvent?.status?.enumToString ?? - '', + widget._calendarEvent?.status?.enumToString ?? '', overflow: TextOverflow.ellipsis, ), ) @@ -316,7 +316,7 @@ class _EventItemState extends State { try { timezone = await FlutterNativeTimezone.getLocalTimezone(); } catch (e) { - print('Could not get the local timezone'); + debugPrint('Could not get the local timezone'); } timezone ??= 'Etc/UTC'; _currentLocation = timeZoneDatabase.locations[timezone]; diff --git a/example/lib/presentation/pages/calendar_add.dart b/example/lib/presentation/pages/calendar_add.dart index abce3788..b54dacfb 100644 --- a/example/lib/presentation/pages/calendar_add.dart +++ b/example/lib/presentation/pages/calendar_add.dart @@ -117,7 +117,6 @@ class _CalendarAddPageState extends State { void showInSnackBar(String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); - // _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(value))); } } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 8dcc3cd0..116cbec0 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -4,23 +4,25 @@ import 'package:collection/collection.dart'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:intl/intl.dart'; +import 'package:timezone/timezone.dart'; import '../date_time_picker.dart'; import '../recurring_event_dialog.dart'; import 'event_attendee.dart'; import 'event_reminders.dart'; -import 'package:timezone/timezone.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; enum RecurrenceRuleEndType { Indefinite, MaxOccurrences, SpecifiedEndDate } class CalendarEventPage extends StatefulWidget { - late final Calendar _calendar; + final Calendar _calendar; final Event? _event; final RecurringEventDialog? _recurringEventDialog; - CalendarEventPage(this._calendar, [this._event, this._recurringEventDialog]); + const CalendarEventPage(this._calendar, + [this._event, this._recurringEventDialog, Key? key]) + : super(key: key); @override _CalendarEventPageState createState() { @@ -34,9 +36,13 @@ class _CalendarEventPageState extends State { final Calendar _calendar; Event? _event; - late DeviceCalendarPlugin _deviceCalendarPlugin; + late final DeviceCalendarPlugin _deviceCalendarPlugin; final RecurringEventDialog? _recurringEventDialog; + DateTime get nowDate => DateTime.now(); + + // TimeOfDay get nowTime => TimeOfDay(hour: nowDate.hour, minute: nowDate.hour); + TZDateTime? _startDate; TimeOfDay? _startTime; @@ -44,26 +50,18 @@ class _CalendarEventPageState extends State { TimeOfDay? _endTime; AutovalidateMode _autovalidate = AutovalidateMode.disabled; - DayOfWeekGroup? _dayOfWeekGroup = DayOfWeekGroup.None; + DayOfWeekGroup _dayOfWeekGroup = DayOfWeekGroup.None; + + RecurrenceRuleEndType _recurrenceRuleEndType = + RecurrenceRuleEndType.Indefinite; + RecurrenceRule? _rrule; - bool _isRecurringEvent = false; - bool _isByDayOfMonth = false; - RecurrenceRuleEndType? _recurrenceRuleEndType; - int? _totalOccurrences; - int? _interval; - late DateTime _recurrenceEndDate; - RecurrenceFrequency? _recurrenceFrequency = RecurrenceFrequency.Daily; - List _daysOfWeek = []; - int? _dayOfMonth; final List _validDaysOfMonth = []; - MonthOfYear? _monthOfYear; - WeekNumber? _weekOfMonth; - DayOfWeek? _selectedDayOfWeek = DayOfWeek.Monday; + Availability _availability = Availability.Busy; EventStatus? _eventStatus; - - List _attendees = []; - List _reminders = []; + List? _attendees; + List? _reminders; String _timezone = 'Etc/UTC'; _CalendarEventPageState( @@ -75,95 +73,89 @@ class _CalendarEventPageState extends State { try { _timezone = await FlutterNativeTimezone.getLocalTimezone(); } catch (e) { - print('Could not get the local timezone'); + debugPrint('Could not get the local timezone'); } _deviceCalendarPlugin = DeviceCalendarPlugin(); - _attendees = []; - _reminders = []; - _recurrenceRuleEndType = RecurrenceRuleEndType.Indefinite; - - if (_event == null) { - print('calendar_event _timezone ------------------------- $_timezone'); - var currentLocation = timeZoneDatabase.locations[_timezone]; + final event = _event; + if (event == null) { + debugPrint( + 'calendar_event _timezone ------------------------- $_timezone'); + final currentLocation = timeZoneDatabase.locations[_timezone]; if (currentLocation != null) { - _startDate = TZDateTime.now(currentLocation); - _endDate = TZDateTime.now(currentLocation).add(Duration(hours: 1)); + final now = TZDateTime.now(currentLocation); + _startDate = now; + _startTime = TimeOfDay(hour: now.hour, minute: now.minute); + final oneHourLater = now.add(const Duration(hours: 1)); + _endDate = oneHourLater; + _endTime = + TimeOfDay(hour: oneHourLater.hour, minute: oneHourLater.minute); } else { var fallbackLocation = timeZoneDatabase.locations['Etc/UTC']; - _startDate = TZDateTime.now(fallbackLocation!); - _endDate = TZDateTime.now(fallbackLocation).add(Duration(hours: 1)); + final now = TZDateTime.now(fallbackLocation!); + _startDate = now; + _startTime = TimeOfDay(hour: now.hour, minute: now.minute); + final oneHourLater = now.add(const Duration(hours: 1)); + _endDate = oneHourLater; + _endTime = + TimeOfDay(hour: oneHourLater.hour, minute: oneHourLater.minute); } - _event = Event(_calendar.id, start: _startDate, end: _endDate); + _event = Event(_calendar.id, + start: _startDate, end: _endDate, availability: _availability); - print('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); + debugPrint('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); - _recurrenceEndDate = _endDate as DateTime; - _dayOfMonth = 1; - _monthOfYear = MonthOfYear.January; - _weekOfMonth = WeekNumber.First; - _availability = Availability.Busy; _eventStatus = EventStatus.None; } else { - _startDate = _event!.start!; - _endDate = _event!.end!; - _isRecurringEvent = _event!.recurrenceRule != null; - - if (_event!.attendees!.isNotEmpty) { - _attendees.addAll(_event!.attendees! as Iterable); + final start = event.start; + final end = event.end; + if (start != null && end != null) { + _startDate = start; + _startTime = TimeOfDay(hour: start.hour, minute: start.minute); + _endDate = end; + _endTime = TimeOfDay(hour: end.hour, minute: end.minute); } - if (_event!.reminders!.isNotEmpty) { - _reminders.addAll(_event!.reminders!); + final attendees = event.attendees; + if (attendees != null && attendees.isNotEmpty) { + _attendees = []; + _attendees?.addAll(attendees as Iterable); } - if (_isRecurringEvent) { - _interval = _event!.recurrenceRule!.interval!; - _totalOccurrences = _event!.recurrenceRule!.totalOccurrences; - _recurrenceFrequency = _event!.recurrenceRule!.recurrenceFrequency; + final reminders = event.reminders; + if (reminders != null && reminders.isNotEmpty) { + _reminders = []; + _reminders?.addAll(reminders); + } - if (_totalOccurrences != null) { + final rrule = event.recurrenceRule; + if (rrule != null) { + // debugPrint('OLD_RRULE: ${rrule.toString()}'); + _rrule = rrule; + if (rrule.count != null) { _recurrenceRuleEndType = RecurrenceRuleEndType.MaxOccurrences; } - - if (_event!.recurrenceRule!.endDate != null) { + if (rrule.until != null) { _recurrenceRuleEndType = RecurrenceRuleEndType.SpecifiedEndDate; - _recurrenceEndDate = _event!.recurrenceRule!.endDate!; - } - - _isByDayOfMonth = _event?.recurrenceRule?.weekOfMonth == null; - _daysOfWeek = _event?.recurrenceRule?.daysOfWeek ?? []; - _monthOfYear = - _event?.recurrenceRule?.monthOfYear ?? MonthOfYear.January; - _weekOfMonth = _event?.recurrenceRule?.weekOfMonth ?? WeekNumber.First; - _selectedDayOfWeek = - _daysOfWeek.isNotEmpty ? _daysOfWeek.first : DayOfWeek.Monday; - _dayOfMonth = _event?.recurrenceRule?.dayOfMonth ?? 1; - - if (_daysOfWeek.isNotEmpty) { - _updateDaysOfWeekGroup(); } } - _availability = _event!.availability; - _eventStatus = _event!.status; + _availability = event.availability; + _eventStatus = event.status; } - _startTime = TimeOfDay(hour: _startDate!.hour, minute: _startDate!.minute); - _endTime = TimeOfDay(hour: _endDate!.hour, minute: _endDate!.minute); - // Getting days of the current month (or a selected month for the yearly recurrence) as a default - _getValidDaysOfMonth(_recurrenceFrequency); + _getValidDaysOfMonth(_rrule?.frequency); setState(() {}); } void printAttendeeDetails(Attendee attendee) { - print( + debugPrint( 'attendee name: ${attendee.name}, email address: ${attendee.emailAddress}, type: ${attendee.role?.enumToString}'); - print( + debugPrint( 'ios specifics - status: ${attendee.iosAttendeeDetails?.attendanceStatus}, type: ${attendee.iosAttendeeDetails?.attendanceStatus?.enumToString}'); - print( + debugPrint( 'android specifics - status ${attendee.androidAttendeeDetails?.attendanceStatus}, type: ${attendee.androidAttendeeDetails?.attendanceStatus?.enumToString}'); } @@ -178,126 +170,126 @@ class _CalendarEventPageState extends State { ? 'View event ${_event?.title}' : 'Edit event ${_event?.title}'), ), - body: SingleChildScrollView( - child: AbsorbPointer( - absorbing: _calendar.isReadOnly ?? false, - child: Column( - children: [ - Form( - autovalidateMode: _autovalidate, - key: _formKey, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - key: Key('titleField'), - initialValue: _event?.title, - decoration: const InputDecoration( - labelText: 'Title', - hintText: 'Meeting with Gloria...'), - validator: _validateTitle, - onSaved: (String? value) { - _event?.title = value; - }, - ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.description, - decoration: const InputDecoration( - labelText: 'Description', - hintText: 'Remember to buy flowers...'), - onSaved: (String? value) { - _event?.description = value; - }, - ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.location, - decoration: const InputDecoration( - labelText: 'Location', - hintText: 'Sydney, Australia'), - onSaved: (String? value) { - _event?.location = value; - }, + body: SafeArea( + child: SingleChildScrollView( + child: AbsorbPointer( + absorbing: _calendar.isReadOnly ?? false, + child: Column( + children: [ + Form( + autovalidateMode: _autovalidate, + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + key: const Key('titleField'), + initialValue: _event?.title, + decoration: const InputDecoration( + labelText: 'Title', + hintText: 'Meeting with Gloria...'), + validator: _validateTitle, + onSaved: (String? value) { + _event?.title = value; + }, + ), ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.url?.data?.contentText ?? '', - decoration: const InputDecoration( - labelText: 'URL', hintText: 'https://google.com'), - onSaved: (String? value) { - if (value != null) { - var uri = Uri.dataFromString(value); - _event?.url = uri; - } - }, + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.description, + decoration: const InputDecoration( + labelText: 'Description', + hintText: 'Remember to buy flowers...'), + onSaved: (String? value) { + _event?.description = value; + }, + ), ), - ), - ListTile( - leading: Text( - 'Availability', - style: TextStyle(fontSize: 16), + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.location, + decoration: const InputDecoration( + labelText: 'Location', + hintText: 'Sydney, Australia'), + onSaved: (String? value) { + _event?.location = value; + }, + ), ), - trailing: DropdownButton( - value: _availability, - onChanged: (Availability? newValue) { - setState(() { - if (newValue != null) { - _availability = newValue; - _event?.availability = newValue; + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.url?.data?.contentText ?? '', + decoration: const InputDecoration( + labelText: 'URL', hintText: 'https://google.com'), + onSaved: (String? value) { + if (value != null) { + var uri = Uri.dataFromString(value); + _event?.url = uri; } - }); - }, - items: Availability.values - .map>( - (Availability value) { - return DropdownMenuItem( - value: value, - child: Text(value.enumToString), - ); - }).toList(), + }, + ), ), - ), - if(Platform.isAndroid) ListTile( - leading: Text( - 'Status', + leading: const Text( + 'Availability', style: TextStyle(fontSize: 16), ), - trailing: DropdownButton( - value: _eventStatus, - onChanged: (EventStatus? newValue) { + trailing: DropdownButton( + value: _availability, + onChanged: (Availability? newValue) { setState(() { if (newValue != null) { - _eventStatus = newValue; - _event?.status = newValue; + _availability = newValue; + _event?.availability = newValue; } }); }, - items: EventStatus.values - .map>( - (EventStatus value) { - return DropdownMenuItem( + items: Availability.values + .map>( + (Availability value) { + return DropdownMenuItem( value: value, child: Text(value.enumToString), ); }).toList(), ), ), - SwitchListTile( - value: _event?.allDay ?? false, - onChanged: (value) => - setState(() => _event?.allDay = value), - title: Text('All Day'), - ), - if (_startDate != null) + if (Platform.isAndroid) + ListTile( + leading: Text( + 'Status', + style: TextStyle(fontSize: 16), + ), + trailing: DropdownButton( + value: _eventStatus, + onChanged: (EventStatus? newValue) { + setState(() { + if (newValue != null) { + _eventStatus = newValue; + _event?.status = newValue; + } + }); + }, + items: EventStatus.values + .map>( + (EventStatus value) { + return DropdownMenuItem( + value: value, + child: Text(value.enumToString), + ); + }).toList(), + ), + ), + SwitchListTile( + value: _event?.allDay ?? false, + onChanged: (value) => + setState(() => _event?.allDay = value), + title: Text('All Day'), + ), Padding( padding: const EdgeInsets.all(10.0), child: DateTimePicker( @@ -328,150 +320,127 @@ class _CalendarEventPageState extends State { }, ), ), - if ((_event?.allDay == false) && Platform.isAndroid) - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.start?.location.name, - decoration: const InputDecoration( - labelText: 'Start date time zone', - hintText: 'Australia/Sydney'), - onSaved: (String? value) { - _event?.updateStartLocation(value); - }, + if ((_event?.allDay == false) && Platform.isAndroid) + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.start?.location.name, + decoration: const InputDecoration( + labelText: 'Start date time zone', + hintText: 'Australia/Sydney'), + onSaved: (String? value) { + _event?.updateStartLocation(value); + }, + ), ), - ), - // Only add the 'To' Date for non-allDay events on all - // platforms except Android (which allows multiple-day allDay events) - if (_event?.allDay == false || Platform.isAndroid) - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'To', - selectedDate: _endDate, - selectedTime: _endTime, - enableTime: _event?.allDay == false, - selectDate: (DateTime date) { - setState( - () { - var currentLocation = - timeZoneDatabase.locations[_timezone]; - if (currentLocation != null) { - _endDate = - TZDateTime.from(date, currentLocation); + // Only add the 'To' Date for non-allDay events on all + // platforms except Android (which allows multiple-day allDay events) + if (_event?.allDay == false || Platform.isAndroid) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'To', + selectedDate: _endDate, + selectedTime: _endTime, + enableTime: _event?.allDay == false, + selectDate: (DateTime date) { + setState( + () { + var currentLocation = + timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + _endDate = + TZDateTime.from(date, currentLocation); + _event?.end = _combineDateWithTime( + _endDate, _endTime); + } + }, + ); + }, + selectTime: (TimeOfDay time) { + setState( + () { + _endTime = time; _event?.end = _combineDateWithTime(_endDate, _endTime); - } - }, - ); - }, - selectTime: (TimeOfDay time) { - setState( - () { - _endTime = time; - _event?.end = - _combineDateWithTime(_endDate, _endTime); - }, - ); - }, + }, + ); + }, + ), ), - ), - if (_event?.allDay == false && Platform.isAndroid) - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - initialValue: _event?.end?.location.name, - decoration: InputDecoration( - labelText: 'End date time zone', - hintText: 'Australia/Sydney'), - onSaved: (String? value) => - _event?.updateEndLocation(value), + if (_event?.allDay == false && Platform.isAndroid) + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + initialValue: _event?.end?.location.name, + decoration: InputDecoration( + labelText: 'End date time zone', + hintText: 'Australia/Sydney'), + onSaved: (String? value) => + _event?.updateEndLocation(value), + ), ), - ), - ListTile( - onTap: _calendar.isReadOnly == false - ? () async { - var result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - EventAttendeePage())); - if (result != null) { - setState(() { - _attendees.add(result); - }); - } - } - : null, - leading: Icon(Icons.people), - title: Text(_calendar.isReadOnly == false - ? 'Add Attendees' - : 'Attendees'), - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: _attendees.length, - itemBuilder: (context, index) { - return Container( - color: _attendees[index].isOrganiser - ? Colors.greenAccent[100] - : Colors.transparent, - child: ListTile( - onTap: () async { - var result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => EventAttendeePage( - attendee: _attendees[index], - eventId: _event?.eventId))); - if (result != null) { - return setState(() { - _attendees[index] = result; - }); + ListTile( + onTap: _calendar.isReadOnly == false + ? () async { + var result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EventAttendeePage())); + if (result != null) { + if (_attendees == null) { + _attendees = []; + } + setState(() { + _attendees?.add(result); + }); + } } - }, - title: Padding( - padding: - const EdgeInsets.symmetric(vertical: 10.0), - child: Text( - '${_attendees[index].name} (${_attendees[index].emailAddress})'), - ), - subtitle: Wrap( - spacing: 10, - direction: Axis.horizontal, - alignment: WrapAlignment.end, - children: [ - Visibility( - visible: _attendees[index] - .androidAttendeeDetails != - null, - child: Container( - margin: const EdgeInsets.symmetric( - vertical: 10.0), - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - border: Border.all( - color: Colors.blueAccent)), - child: Text( - 'Android: ${_attendees[index].androidAttendeeDetails?.attendanceStatus?.enumToString}')), - ), - Visibility( - visible: - _attendees[index].iosAttendeeDetails != - null, - child: Container( - margin: const EdgeInsets.symmetric( - vertical: 10.0), - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - border: Border.all( - color: Colors.blueAccent)), - child: Text( - 'iOS: ${_attendees[index].iosAttendeeDetails?.attendanceStatus?.enumToString}')), - ), - Visibility( - visible: _attendees[index].isCurrentUser, + : null, + leading: Icon(Icons.people), + title: Text(_calendar.isReadOnly == false + ? 'Add Attendees' + : 'Attendees'), + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: (_attendees ?? []).length, + itemBuilder: (context, index) { + return Container( + color: (_attendees?[index].isOrganiser ?? false) + ? Colors.greenAccent[100] + : Colors.transparent, + child: ListTile( + onTap: () async { + var result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EventAttendeePage( + attendee: _attendees?[index], + eventId: _event?.eventId))); + if (result != null) { + return setState(() { + _attendees?[index] = result; + }); + } + }, + title: Padding( + padding: + const EdgeInsets.symmetric(vertical: 10.0), + child: Text( + '${_attendees?[index].name} (${_attendees?[index].emailAddress})'), + ), + subtitle: Wrap( + spacing: 10, + direction: Axis.horizontal, + alignment: WrapAlignment.end, + children: [ + Visibility( + visible: _attendees?[index] + .androidAttendeeDetails != + null, child: Container( margin: const EdgeInsets.symmetric( vertical: 10.0), @@ -479,9 +448,13 @@ class _CalendarEventPageState extends State { decoration: BoxDecoration( border: Border.all( color: Colors.blueAccent)), - child: Text('current user'))), - Visibility( - visible: _attendees[index].isOrganiser, + child: Text( + 'Android: ${_attendees?[index].androidAttendeeDetails?.attendanceStatus?.enumToString}')), + ), + Visibility( + visible: + _attendees?[index].iosAttendeeDetails != + null, child: Container( margin: const EdgeInsets.symmetric( vertical: 10.0), @@ -489,516 +462,597 @@ class _CalendarEventPageState extends State { decoration: BoxDecoration( border: Border.all( color: Colors.blueAccent)), - child: Text('Organiser'))), - Container( - margin: const EdgeInsets.symmetric( - vertical: 10.0), - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - border: - Border.all(color: Colors.blueAccent)), - child: Text( - '${_attendees[index].role?.enumToString}'), - ), - IconButton( - padding: const EdgeInsets.all(0), - onPressed: () { - setState(() { - _attendees.removeAt(index); - }); - }, - icon: Icon( - Icons.remove_circle, - color: Colors.redAccent, + child: Text( + 'iOS: ${_attendees?[index].iosAttendeeDetails?.attendanceStatus?.enumToString}')), + ), + Visibility( + visible: + _attendees?[index].isCurrentUser ?? + false, + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 10.0), + padding: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.blueAccent)), + child: Text('current user'))), + Visibility( + visible: _attendees?[index].isOrganiser ?? + false, + child: Container( + margin: const EdgeInsets.symmetric( + vertical: 10.0), + padding: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.blueAccent)), + child: Text('Organiser'))), + Container( + margin: const EdgeInsets.symmetric( + vertical: 10.0), + padding: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + border: Border.all( + color: Colors.blueAccent)), + child: Text( + '${_attendees?[index].role?.enumToString}'), ), - ) + IconButton( + padding: const EdgeInsets.all(0), + onPressed: () { + setState(() { + _attendees?.removeAt(index); + }); + }, + icon: Icon( + Icons.remove_circle, + color: Colors.redAccent, + ), + ) + ], + ), + ), + ); + }, + ), + GestureDetector( + onTap: () async { + var result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EventRemindersPage(_reminders ?? []))); + if (result == null) { + return; + } + _reminders = result; + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Align( + alignment: Alignment.centerLeft, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 10.0, + children: [ + const Icon(Icons.alarm), + if (_reminders?.isEmpty ?? false) + Text(_calendar.isReadOnly == false + ? 'Add reminders' + : 'Reminders'), + for (var reminder in _reminders ?? []) + Text('${reminder.minutes} minutes before; ') ], ), ), - ); - }, - ), - GestureDetector( - onTap: () async { - var result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - EventRemindersPage(_reminders))); - if (result == null) { - return; - } - _reminders = result; - }, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Align( - alignment: Alignment.centerLeft, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 10.0, - children: [ - Icon(Icons.alarm), - if (_reminders.isEmpty) - Text(_calendar.isReadOnly == false - ? 'Add reminders' - : 'Reminders'), - for (var reminder in _reminders) - Text('${reminder.minutes} minutes before; ') - ], - ), ), ), - ), - CheckboxListTile( - value: _isRecurringEvent, - title: Text('Is recurring'), - onChanged: (isChecked) { - setState(() { - _isRecurringEvent = isChecked ?? false; - }); - }, - ), - if (_isRecurringEvent) ...[ - ListTile( - leading: Text('Select a Recurrence Type'), - trailing: DropdownButton( - onChanged: (selectedFrequency) { + CheckboxListTile( + value: _rrule != null, + title: const Text('Is recurring'), + onChanged: (isChecked) { + if (isChecked != null) { setState(() { - _recurrenceFrequency = selectedFrequency; - _getValidDaysOfMonth(_recurrenceFrequency); + if (isChecked) { + _rrule = + RecurrenceRule(frequency: Frequency.daily); + } else { + _rrule = null; + } }); - }, - value: _recurrenceFrequency, - items: RecurrenceFrequency.values - .map((frequency) => DropdownMenuItem( - value: frequency, - child: - _recurrenceFrequencyToText(frequency), - )) - .toList(), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), - child: Row( - children: [ - Text('Repeat Every '), - Flexible( - child: TextFormField( - initialValue: _interval?.toString() ?? '1', - decoration: - const InputDecoration(hintText: '1'), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(2) - ], - validator: _validateInterval, - textAlign: TextAlign.right, - onSaved: (String? value) { - if (value != null) { - _interval = int.tryParse(value); - } - }, - ), - ), - _recurrenceFrequencyToIntervalText( - _recurrenceFrequency), - ], - ), + } + }, ), - if (_recurrenceFrequency == - RecurrenceFrequency.Weekly) ...[ - Column( - children: [ - ...DayOfWeek.values.map((day) { - return CheckboxListTile( - title: Text(day.enumToString), - value: _daysOfWeek.any((dow) => dow == day), - onChanged: (selected) { - setState(() { - if (selected == true) { - _daysOfWeek.add(day); - } else { - _daysOfWeek.remove(day); - } - _updateDaysOfWeekGroup(selectedDay: day); - }); - }, - ); - }), - Divider(color: Colors.black), - ...DayOfWeekGroup.values.map((group) { - return RadioListTile( - title: Text(group.enumToString), - value: group, - groupValue: _dayOfWeekGroup, - onChanged: (selected) { - setState(() { - _dayOfWeekGroup = - selected as DayOfWeekGroup; - _updateDaysOfWeek(); - }); - }, - controlAffinity: - ListTileControlAffinity.trailing); - }), - ], - ) - ], - if (_recurrenceFrequency == RecurrenceFrequency.Monthly || - _recurrenceFrequency == - RecurrenceFrequency.Yearly) ...[ - SwitchListTile( - value: _isByDayOfMonth, - onChanged: (value) => - setState(() => _isByDayOfMonth = value), - title: Text('By day of the month'), - ) - ], - if (_recurrenceFrequency == RecurrenceFrequency.Yearly && - _isByDayOfMonth) ...[ - ListTile( - leading: Text('Month of the year'), - trailing: DropdownButton( - onChanged: (value) { - setState(() { - _monthOfYear = value; - _getValidDaysOfMonth(_recurrenceFrequency); - }); - }, - value: _monthOfYear, - items: MonthOfYear.values - .map((month) => DropdownMenuItem( - value: month, - child: Text(month.enumToString), - )) - .toList(), - ), - ), - ], - if (_isByDayOfMonth && - (_recurrenceFrequency == - RecurrenceFrequency.Monthly || - _recurrenceFrequency == - RecurrenceFrequency.Yearly)) ...[ + if (_rrule != null) ...[ ListTile( - leading: Text('Day of the month'), - trailing: DropdownButton( - onChanged: (value) { + leading: const Text('Select a Recurrence Type'), + trailing: DropdownButton( + onChanged: (selectedFrequency) { setState(() { - _dayOfMonth = value; + _onFrequencyChange( + selectedFrequency ?? Frequency.daily); + _getValidDaysOfMonth(selectedFrequency); }); }, - value: _dayOfMonth, - items: _validDaysOfMonth - .map((day) => DropdownMenuItem( - value: day, - child: Text(day.toString()), + value: _rrule?.frequency, + items: [ + // Frequency.secondly, + // Frequency.minutely, + // Frequency.hourly, + Frequency.daily, + Frequency.weekly, + Frequency.monthly, + Frequency.yearly, + ] + .map((frequency) => DropdownMenuItem( + value: frequency, + child: + _recurrenceFrequencyToText(frequency), )) .toList(), ), ), - ], - if (!_isByDayOfMonth && - (_recurrenceFrequency == - RecurrenceFrequency.Monthly || - _recurrenceFrequency == - RecurrenceFrequency.Yearly)) ...[ - Padding( - padding: const EdgeInsets.fromLTRB(15, 10, 15, 10), - child: Align( - alignment: Alignment.centerLeft, - child: _recurrenceFrequencyToText( - _recurrenceFrequency) - .data != - null - ? Text(_recurrenceFrequencyToText( - _recurrenceFrequency) - .data! + - ' on the ') - : Text('')), - ), - Padding( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: DropdownButton( - onChanged: (value) { - setState(() { - _weekOfMonth = value; - }); - }, - value: _weekOfMonth ?? WeekNumber.First, - items: WeekNumber.values - .map((weekNum) => DropdownMenuItem( - value: weekNum, - child: Text(weekNum.enumToString), - )) - .toList(), - ), - ), - Flexible( - child: DropdownButton( - onChanged: (value) { - setState(() { - _selectedDayOfWeek = value; - }); - }, - value: _selectedDayOfWeek != null - ? DayOfWeek - .values[_selectedDayOfWeek!.index] - : DayOfWeek.values[0], - items: DayOfWeek.values - .map((day) => DropdownMenuItem( - value: day, - child: Text(day.enumToString), - )) - .toList(), - ), - ), - if (_recurrenceFrequency == - RecurrenceFrequency.Yearly) ...[ - Text('of'), - Flexible( - child: DropdownButton( - onChanged: (value) { - setState(() { - _monthOfYear = value; - }); - }, - value: _monthOfYear, - items: MonthOfYear.values - .map((month) => DropdownMenuItem( - value: month, - child: Text(month.enumToString), - )) - .toList(), - ), - ), - ] - ], - ), - ), - ], - ListTile( - leading: Text('Event ends'), - trailing: DropdownButton( - onChanged: (value) { - setState(() { - _recurrenceRuleEndType = value; - }); - }, - value: _recurrenceRuleEndType, - items: RecurrenceRuleEndType.values - .map((frequency) => DropdownMenuItem( - value: frequency, - child: - _recurrenceRuleEndTypeToText(frequency), - )) - .toList(), - ), - ), - if (_recurrenceRuleEndType == - RecurrenceRuleEndType.MaxOccurrences) Padding( padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), child: Row( children: [ - Text('For the next '), + const Text('Repeat Every '), Flexible( child: TextFormField( - initialValue: - _totalOccurrences?.toString() ?? '1', + initialValue: '${_rrule?.interval ?? 1}', decoration: const InputDecoration(hintText: '1'), keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(3), + LengthLimitingTextInputFormatter(2) ], - validator: _validateTotalOccurrences, + validator: _validateInterval, textAlign: TextAlign.right, onSaved: (String? value) { if (value != null) { - _totalOccurrences = int.tryParse(value); + _rrule = _rrule?.copyWith( + interval: int.tryParse(value)); } }, ), ), - Text(' occurrences'), + _recurrenceFrequencyToIntervalText( + _rrule?.frequency), ], ), ), - if (_recurrenceRuleEndType == - RecurrenceRuleEndType.SpecifiedEndDate) - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'Date', - enableTime: false, - selectedDate: _recurrenceEndDate, - selectDate: (DateTime date) { + if (_rrule?.frequency == Frequency.weekly) ...[ + Column( + children: [ + ...DayOfWeek.values.map((day) { + return CheckboxListTile( + title: Text(day.enumToString), + value: _rrule?.byWeekDays + .contains(ByWeekDayEntry(day.index + 1)), + onChanged: (selected) { + setState(() { + if (selected == true) { + _rrule?.byWeekDays + .add(ByWeekDayEntry(day.index + 1)); + } else { + _rrule?.byWeekDays.remove( + ByWeekDayEntry(day.index + 1)); + } + _updateDaysOfWeekGroup(selectedDay: day); + }); + }, + ); + }), + const Divider(color: Colors.black), + ...DayOfWeekGroup.values.map((group) { + return RadioListTile( + title: Text(group.enumToString), + value: group, + groupValue: _dayOfWeekGroup, + onChanged: (DayOfWeekGroup? selected) { + if (selected != null) { + setState(() { + _dayOfWeekGroup = selected; + _updateDaysOfWeek(); + }); + } + }, + controlAffinity: + ListTileControlAffinity.trailing); + }), + ], + ) + ], + if (_rrule?.frequency == Frequency.monthly || + _rrule?.frequency == Frequency.yearly) ...[ + SwitchListTile( + value: _rrule?.hasByMonthDays ?? false, + onChanged: (value) { setState(() { - _recurrenceEndDate = date; + if (value) { + _rrule = _rrule?.copyWith( + byMonthDays: {1}, byWeekDays: {}); + } else { + _rrule = _rrule?.copyWith( + byMonthDays: {}, + byWeekDays: {ByWeekDayEntry(1, 1)}); + } }); }, + title: const Text('By day of the month'), + ) + ], + if (_rrule?.frequency == Frequency.yearly && + (_rrule?.hasByMonthDays ?? false)) ...[ + ListTile( + leading: const Text('Month of the year'), + trailing: DropdownButton( + onChanged: (value) { + if (value != null) { + setState(() { + _rrule = _rrule + ?.copyWith(byMonths: {value.index + 1}); + _getValidDaysOfMonth(_rrule?.frequency); + }); + } + }, + value: MonthOfYear.values.toList()[ + (_rrule?.hasByMonths ?? false) + ? _rrule!.byMonths.first - 1 + : 0], + items: MonthOfYear.values + .map((month) => DropdownMenuItem( + value: month, + child: Text(month.enumToString), + )) + .toList(), + ), + ), + ], + if ((_rrule?.hasByMonthDays ?? false) && + (_rrule?.frequency == Frequency.monthly || + _rrule?.frequency == Frequency.yearly)) ...[ + ListTile( + leading: const Text('Day of the month'), + trailing: DropdownButton( + onChanged: (value) { + if (value != null) { + setState(() { + _rrule = + _rrule?.copyWith(byMonthDays: {value}); + }); + } + }, + value: (_rrule?.hasByMonthDays ?? false) + ? _rrule!.byMonthDays.first + : 1, + items: _validDaysOfMonth + .map((day) => DropdownMenuItem( + value: day, + child: Text(day.toString()), + )) + .toList(), + ), + ), + ], + if (!(_rrule?.hasByMonthDays ?? false) && + (_rrule?.frequency == Frequency.monthly || + _rrule?.frequency == Frequency.yearly)) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(15, 10, 15, 10), + child: Align( + alignment: Alignment.centerLeft, + child: _recurrenceFrequencyToText( + _rrule?.frequency) + .data != + null + ? Text(_recurrenceFrequencyToText( + _rrule?.frequency) + .data! + + ' on the ') + : const Text('')), + ), + Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: DropdownButton( + onChanged: (value) { + if (value != null) { + final weekDay = + _rrule?.byWeekDays.first.day ?? 1; + setState(() { + _rrule = _rrule?.copyWith( + byWeekDays: { + ByWeekDayEntry( + weekDay, value.index + 1) + }); + }); + } + }, + value: WeekNumber.values.toList()[ + (_rrule?.hasByWeekDays ?? false) + ? _weekNumFromWeekDayOccurence( + _rrule!.byWeekDays) + : 0], + items: WeekNumber.values + .map((weekNum) => DropdownMenuItem( + value: weekNum, + child: Text(weekNum.enumToString), + )) + .toList(), + ), + ), + Flexible( + child: DropdownButton( + onChanged: (value) { + if (value != null) { + final weekNo = _rrule + ?.byWeekDays.first.occurrence ?? + 1; + setState(() { + _rrule = _rrule?.copyWith( + byWeekDays: { + ByWeekDayEntry( + value.index + 1, weekNo) + }); + }); + } + }, + value: (_rrule?.hasByWeekDays ?? false) && + _rrule?.byWeekDays.first + .occurrence != + null + ? DayOfWeek.values[ + _rrule!.byWeekDays.first.day - 1] + : DayOfWeek.values[0], + items: DayOfWeek.values + .map((day) => DropdownMenuItem( + value: day, + child: Text(day.enumToString), + )) + .toList(), + ), + ), + if (_rrule?.frequency == Frequency.yearly) ...[ + const Text('of'), + Flexible( + child: DropdownButton( + onChanged: (value) { + if (value != null) { + setState(() { + _rrule = _rrule?.copyWith( + byMonths: {value.index + 1}); + }); + } + }, + value: MonthOfYear.values.toList()[ + (_rrule?.hasByMonths ?? false) + ? _rrule!.byMonths.first - 1 + : 0], + items: MonthOfYear.values + .map((month) => DropdownMenuItem( + value: month, + child: Text(month.enumToString), + )) + .toList(), + ), + ), + ] + ], + ), + ), + ], + ListTile( + leading: const Text('Event ends'), + trailing: DropdownButton( + onChanged: (value) { + setState(() { + if (value != null) { + _recurrenceRuleEndType = value; + } + }); + }, + value: _recurrenceRuleEndType, + items: RecurrenceRuleEndType.values + .map((frequency) => DropdownMenuItem( + value: frequency, + child: _recurrenceRuleEndTypeToText( + frequency), + )) + .toList(), ), ), + if (_recurrenceRuleEndType == + RecurrenceRuleEndType.MaxOccurrences) + Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), + child: Row( + children: [ + const Text('For the next '), + Flexible( + child: TextFormField( + initialValue: '${_rrule?.count ?? 1}', + decoration: + const InputDecoration(hintText: '1'), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(3), + ], + validator: _validateTotalOccurrences, + textAlign: TextAlign.right, + onSaved: (String? value) { + if (value != null) { + _rrule = _rrule?.copyWith( + count: int.tryParse(value)); + } + }, + ), + ), + const Text(' occurrences'), + ], + ), + ), + if (_recurrenceRuleEndType == + RecurrenceRuleEndType.SpecifiedEndDate) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'Date', + enableTime: false, + selectedDate: _rrule?.until ?? DateTime.now(), + selectDate: (DateTime date) { + setState(() { + _rrule = _rrule?.copyWith( + until: DateTime( + date.year, + date.month, + date.day, + _endTime?.hour ?? nowDate.hour, + _endTime?.minute ?? + nowDate.minute) + .toUtc()); + }); + }, + ), + ), + ], + ...[ + // TODO: on iPhone (e.g. 8) this seems neccesary to be able to access UI below the FAB + const SizedBox(height: 75), + ] ], - ], + ), ), - ), - if (_calendar.isReadOnly == false && - (_event?.eventId?.isNotEmpty ?? false)) ...[ - ElevatedButton( - key: Key('deleteEventButton'), - style: ElevatedButton.styleFrom( - primary: Colors.red, onPrimary: Colors.white), - onPressed: () async { - bool? result = true; - if (!_isRecurringEvent) { - await _deviceCalendarPlugin.deleteEvent( - _calendar.id, _event?.eventId); - } else { - result = await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return _recurringEventDialog != null - ? _recurringEventDialog as Widget - : SizedBox(); - }); - } + if (_calendar.isReadOnly == false && + (_event?.eventId?.isNotEmpty ?? false)) ...[ + ElevatedButton( + key: const Key('deleteEventButton'), + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: Colors.red), + onPressed: () async { + bool? result = true; + if (!(_rrule != null)) { + await _deviceCalendarPlugin.deleteEvent( + _calendar.id, _event?.eventId); + } else { + result = await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return _recurringEventDialog != null + ? _recurringEventDialog as Widget + : const SizedBox.shrink(); + }); + } - if (result == true) { - Navigator.pop(context, true); - } - }, - child: Text('Delete'), - ), - ] - ], + if (result == true) { + Navigator.pop(context, true); + } + }, + child: const Text('Delete'), + ), + ], + ], + ), ), ), ), floatingActionButton: Visibility( visible: _calendar.isReadOnly == false, child: FloatingActionButton( - key: Key('saveEventButton'), + key: const Key('saveEventButton'), onPressed: () async { final form = _formKey.currentState; if (form?.validate() == false) { _autovalidate = AutovalidateMode.always; // Start validating on every change. - showInSnackBar('Please fix the errors in red before submitting.'); + showInSnackBar( + context, 'Please fix the errors in red before submitting.'); } else { form?.save(); - if (_isRecurringEvent) { - if (!_isByDayOfMonth && - (_recurrenceFrequency == RecurrenceFrequency.Monthly || - _recurrenceFrequency == RecurrenceFrequency.Yearly)) { - // Setting day of the week parameters for WeekNumber to avoid clashing with the weekly recurrence values - _daysOfWeek.clear(); - if (_selectedDayOfWeek != null) { - _daysOfWeek.add(_selectedDayOfWeek as DayOfWeek); - } - } else { - _weekOfMonth = null; - } - - _event?.recurrenceRule = RecurrenceRule(_recurrenceFrequency, - interval: _interval, - totalOccurrences: (_recurrenceRuleEndType == - RecurrenceRuleEndType.MaxOccurrences) - ? _totalOccurrences - : null, - endDate: _recurrenceRuleEndType == - RecurrenceRuleEndType.SpecifiedEndDate - ? _recurrenceEndDate - : null, - daysOfWeek: _daysOfWeek, - dayOfMonth: _dayOfMonth, - monthOfYear: _monthOfYear, - weekOfMonth: _weekOfMonth); - } - _event?.attendees = _attendees; - _event?.reminders = _reminders; - _event?.availability = _availability; - _event?.status = _eventStatus; - var createEventResult = - await _deviceCalendarPlugin.createOrUpdateEvent(_event); - if (createEventResult?.isSuccess == true) { - Navigator.pop(context, true); - } else { - showInSnackBar(createEventResult?.errors - .map((err) => '[${err.errorCode}] ${err.errorMessage}') - .join(' | ') as String); - } + _adjustStartEnd(); + _event?.recurrenceRule = _rrule; + // debugPrint('FINAL_RRULE: ${_rrule.toString()}'); + } + _event?.attendees = _attendees; + _event?.reminders = _reminders; + _event?.availability = _availability; + _event?.status = _eventStatus; + var createEventResult = + await _deviceCalendarPlugin.createOrUpdateEvent(_event); + if (createEventResult?.isSuccess == true) { + Navigator.pop(context, true); + } else { + showInSnackBar( + context, + createEventResult?.errors + .map((err) => '[${err.errorCode}] ${err.errorMessage}') + .join(' | ') as String); } }, - child: Icon(Icons.check), + child: const Icon(Icons.check), ), ), ); } - Text _recurrenceFrequencyToText(RecurrenceFrequency? recurrenceFrequency) { - switch (recurrenceFrequency) { - case RecurrenceFrequency.Daily: - return Text('Daily'); - case RecurrenceFrequency.Weekly: - return Text('Weekly'); - case RecurrenceFrequency.Monthly: - return Text('Monthly'); - case RecurrenceFrequency.Yearly: - return Text('Yearly'); - default: - return Text(''); + Text _recurrenceFrequencyToText(Frequency? recurrenceFrequency) { + if (recurrenceFrequency == Frequency.daily) { + return const Text('Daily'); + } else if (recurrenceFrequency == Frequency.weekly) { + return const Text('Weekly'); + } else if (recurrenceFrequency == Frequency.monthly) { + return const Text('Monthly'); + } else if (recurrenceFrequency == Frequency.yearly) { + return const Text('Yearly'); + } else { + return const Text(''); } } - Text _recurrenceFrequencyToIntervalText( - RecurrenceFrequency? recurrenceFrequency) { - switch (recurrenceFrequency) { - case RecurrenceFrequency.Daily: - return Text(' Day(s)'); - case RecurrenceFrequency.Weekly: - return Text(' Week(s) on'); - case RecurrenceFrequency.Monthly: - return Text(' Month(s)'); - case RecurrenceFrequency.Yearly: - return Text(' Year(s)'); - default: - return Text(''); + Text _recurrenceFrequencyToIntervalText(Frequency? recurrenceFrequency) { + if (recurrenceFrequency == Frequency.daily) { + return const Text(' Day(s)'); + } else if (recurrenceFrequency == Frequency.weekly) { + return const Text(' Week(s) on'); + } else if (recurrenceFrequency == Frequency.monthly) { + return const Text(' Month(s)'); + } else if (recurrenceFrequency == Frequency.yearly) { + return const Text(' Year(s)'); + } else { + return const Text(''); } } Text _recurrenceRuleEndTypeToText(RecurrenceRuleEndType endType) { switch (endType) { case RecurrenceRuleEndType.Indefinite: - return Text('Indefinitely'); + return const Text('Indefinitely'); case RecurrenceRuleEndType.MaxOccurrences: - return Text('After a set number of times'); + return const Text('After a set number of times'); case RecurrenceRuleEndType.SpecifiedEndDate: - return Text('Continues until a specified date'); + return const Text('Continues until a specified date'); default: - return Text(''); + return const Text(''); } } // Get total days of a month - void _getValidDaysOfMonth(RecurrenceFrequency? frequency) { + void _getValidDaysOfMonth(Frequency? frequency) { _validDaysOfMonth.clear(); var totalDays = 0; // Year frequency: Get total days of the selected month - if (frequency == RecurrenceFrequency.Yearly) { + if (frequency == Frequency.yearly) { totalDays = DateTime(DateTime.now().year, - _monthOfYear?.value != null ? _monthOfYear!.value + 1 : 1, 0) + (_rrule?.hasByMonths ?? false) ? _rrule!.byMonths.first : 1, 0) .day; } else { // Otherwise, get total days of the current month @@ -1012,49 +1066,153 @@ class _CalendarEventPageState extends State { } void _updateDaysOfWeek() { - if (_dayOfWeekGroup == null) return; - var days = _dayOfWeekGroup!.getDays; - switch (_dayOfWeekGroup) { case DayOfWeekGroup.Weekday: + _rrule = _rrule?.copyWith(byWeekDays: { + ByWeekDayEntry(1), + ByWeekDayEntry(2), + ByWeekDayEntry(3), + ByWeekDayEntry(4), + ByWeekDayEntry(5), + }); + break; case DayOfWeekGroup.Weekend: + _rrule = _rrule?.copyWith(byWeekDays: { + ByWeekDayEntry(6), + ByWeekDayEntry(7), + }); + break; case DayOfWeekGroup.AllDays: - _daysOfWeek.clear(); - _daysOfWeek.addAll(days.where((a) => _daysOfWeek.every((b) => a != b))); + _rrule = _rrule?.copyWith(byWeekDays: { + ByWeekDayEntry(1), + ByWeekDayEntry(2), + ByWeekDayEntry(3), + ByWeekDayEntry(4), + ByWeekDayEntry(5), + ByWeekDayEntry(6), + ByWeekDayEntry(7), + }); break; case DayOfWeekGroup.None: - _daysOfWeek.clear(); - break; default: - _daysOfWeek.clear(); + _rrule?.byWeekDays.clear(); + break; } + // () => setState(() => {}); } void _updateDaysOfWeekGroup({DayOfWeek? selectedDay}) { - var deepEquality = const DeepCollectionEquality.unordered().equals; - - // If _daysOfWeek contains nothing - if (_daysOfWeek.isEmpty && _dayOfWeekGroup != DayOfWeekGroup.None) { - _dayOfWeekGroup = DayOfWeekGroup.None; - } - // If _daysOfWeek contains Monday to Friday - else if (deepEquality(_daysOfWeek, DayOfWeekGroup.Weekday.getDays) && - _dayOfWeekGroup != DayOfWeekGroup.Weekday) { - _dayOfWeekGroup = DayOfWeekGroup.Weekday; + final byWeekDays = _rrule?.byWeekDays; + if (byWeekDays != null) { + if (byWeekDays.length == 7 && + byWeekDays.every((p0) => + p0.day == 1 || + p0.day == 2 || + p0.day == 3 || + p0.day == 4 || + p0.day == 5 || + p0.day == 6 || + p0.day == 7)) { + _dayOfWeekGroup = DayOfWeekGroup.AllDays; + } else if (byWeekDays.length == 5 && + byWeekDays.every((p0) => + p0.day == 1 || + p0.day == 2 || + p0.day == 3 || + p0.day == 4 || + p0.day == 5) && + byWeekDays.none((p0) => p0.day == 6 || p0.day == 7)) { + _dayOfWeekGroup = DayOfWeekGroup.Weekday; + } else if (byWeekDays.length == 2 && + byWeekDays.every((p0) => p0.day == 6 || p0.day == 7) && + byWeekDays.none((p0) => + p0.day == 1 || + p0.day == 2 || + p0.day == 3 || + p0.day == 4 || + p0.day == 5)) { + _dayOfWeekGroup = DayOfWeekGroup.Weekend; + } else { + _dayOfWeekGroup = DayOfWeekGroup.None; + } } - // If _daysOfWeek contains Saturday and Sunday - else if (deepEquality(_daysOfWeek, DayOfWeekGroup.Weekend.getDays) && - _dayOfWeekGroup != DayOfWeekGroup.Weekend) { - _dayOfWeekGroup = DayOfWeekGroup.Weekend; + } + + int _weekNumFromWeekDayOccurence(Set weekdays) { + final weekNum = weekdays.first.occurrence; + if (weekNum != null) { + return weekNum - 1; + } else { + return 0; } - // If _daysOfWeek contains all days - else if (deepEquality(_daysOfWeek, DayOfWeekGroup.AllDays.getDays) && - _dayOfWeekGroup != DayOfWeekGroup.AllDays) { - _dayOfWeekGroup = DayOfWeekGroup.AllDays; + } + + void _onFrequencyChange(Frequency freq) { + final rrule = _rrule; + if (rrule != null) { + final hasByWeekDays = rrule.hasByWeekDays; + final hasByMonthDays = rrule.hasByMonthDays; + final hasByMonths = rrule.hasByMonths; + if (freq == Frequency.daily || freq == Frequency.weekly) { + if (hasByWeekDays) { + rrule.byWeekDays.clear(); + } + if (hasByMonths) { + rrule.byMonths.clear(); + } + _rrule = rrule.copyWith(frequency: freq); + } + if (freq == Frequency.monthly) { + if (hasByMonths) { + rrule.byMonths.clear(); + } + if (!hasByWeekDays && !hasByMonthDays) { + _rrule = rrule + .copyWith(frequency: freq, byWeekDays: {ByWeekDayEntry(1, 1)}); + } else { + _rrule = rrule.copyWith(frequency: freq); + } + } + if (freq == Frequency.yearly) { + if (!hasByWeekDays || !hasByMonths) { + _rrule = rrule.copyWith( + frequency: freq, + byWeekDays: {ByWeekDayEntry(1, 1)}, + byMonths: {1}); + } else { + _rrule = rrule.copyWith(frequency: freq); + } + } } - // Otherwise null - else { - _dayOfWeekGroup = null; + } + + /// In order to avoid an event instance to appear outside of the recurrence + /// rrule, the start and end date have to be adjusted to match the first + /// instance. + void _adjustStartEnd() { + final start = _event?.start; + final end = _event?.end; + final rrule = _rrule; + if (start != null && end != null && rrule != null) { + final allDay = _event?.allDay ?? false; + final duration = end.difference(start); + final instances = rrule.getAllInstances( + start: allDay + ? DateTime.utc(start.year, start.month, start.day) + : DateTime(start.year, start.month, start.day, start.hour, + start.minute) + .toUtc(), + before: rrule.count == null && rrule.until == null + ? DateTime(start.year + 2, start.month, start.day, start.hour, + start.minute) + .toUtc() + : null); + if (instances.isNotEmpty) { + var newStart = TZDateTime.from(instances.first, start.location); + var newEnd = newStart.add(duration); + _event?.start = newStart; + _event?.end = newEnd; + } } } @@ -1079,7 +1237,6 @@ class _CalendarEventPageState extends State { if (value.isEmpty) { return 'Name is required.'; } - return null; } @@ -1098,7 +1255,7 @@ class _CalendarEventPageState extends State { .add(Duration(hours: time.hour, minutes: time.minute)); } - void showInSnackBar(String value) { + void showInSnackBar(BuildContext context, String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); } } diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index 0cd6f182..7255029f 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -132,12 +132,13 @@ class _CalendarEventsPageState extends State { Future _retrieveCalendarEvents() async { final startDate = DateTime.now().add(Duration(days: -30)); - final endDate = DateTime.now().add(Duration(days: 30)); + // final endDate = DateTime.now().add(Duration(days: 365 * 2)); + final endDate = DateTime.now().add(Duration(days: 365 * 10)); var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( _calendar.id, RetrieveEventsParams(startDate: startDate, endDate: endDate)); setState(() { - _calendarEvents = calendarEventsResult.data as List; + _calendarEvents = calendarEventsResult.data ?? []; _isLoading = false; }); } @@ -169,7 +170,7 @@ class _CalendarEventsPageState extends State { onPressed: () async { var returnValue = await _deviceCalendarPlugin.deleteCalendar(_calendar.id!); - print( + debugPrint( 'returnValue: ${returnValue.data}, ${returnValue.errors}'); Navigator.of(context).pop(); Navigator.of(context).pop(); diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index e28dbc7f..5c21d1d4 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -1,7 +1,7 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'calendar_events.dart'; @@ -137,8 +137,8 @@ class _CalendarsPageState extends State { setState(() { _calendars = calendarsResult.data as List; }); - } on PlatformException catch (e) { - print(e); + } on PlatformException catch (e, s) { + debugPrint('RETRIEVE_CALENDARS: $e, $s'); } } diff --git a/example/lib/presentation/pages/event_attendee.dart b/example/lib/presentation/pages/event_attendee.dart index 002ba6eb..de9644a9 100644 --- a/example/lib/presentation/pages/event_attendee.dart +++ b/example/lib/presentation/pages/event_attendee.dart @@ -1,8 +1,8 @@ import 'dart:io'; +import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/common/app_routes.dart'; import 'package:flutter/material.dart'; -import 'package:device_calendar/device_calendar.dart'; late DeviceCalendarPlugin _deviceCalendarPlugin; @@ -14,7 +14,7 @@ class EventAttendeePage extends StatefulWidget { @override _EventAttendeePageState createState() => - _EventAttendeePageState(attendee, eventId); + _EventAttendeePageState(attendee, eventId ?? ''); } class _EventAttendeePageState extends State { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c6eeea43..5dad60cd 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,66 +3,25 @@ description: Demonstrates how to use the device_calendar plugin. version: 3.2.0 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - intl: - uuid: - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: + intl: ^0.17.0 + uuid: ^3.0.6 + timezone: ^0.9.0 + flutter_native_timezone: ^2.0.0 + device_calendar: + path: ../ dev_dependencies: integration_test: sdk: flutter flutter_test: sdk: flutter - test: - - device_calendar: - path: ../ + flutter_lints: ^2.0.1 -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages + uses-material-design: true \ No newline at end of file diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 59730a93..b39ee5da 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -14,8 +14,17 @@ extension EKParticipant { } } +extension String { + func match(_ regex: String) -> [[String]] { + let nsString = self as NSString + return (try? NSRegularExpression(pattern: regex, options: []))?.matches(in: self, options: [], range: NSMakeRange(0, nsString.length)).map { match in + (0.. EKSource? { - let localSources = eventStore.sources.filter { $0.sourceType == .local } - - if (!localSources.isEmpty) { - return localSources.first - } - if let defaultSource = eventStore.defaultCalendarForNewEvents?.source { - return defaultSource - } - - let iCloudSources = eventStore.sources.filter { $0.sourceType == .calDAV && $0.sourceIdentifier == "iCloud" } + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case requestPermissionsMethod: + requestPermissions(result) + case hasPermissionsMethod: + hasPermissions(result) + case retrieveCalendarsMethod: + retrieveCalendars(result) + case retrieveEventsMethod: + retrieveEvents(call, result) + case createOrUpdateEventMethod: + createOrUpdateEvent(call, result) + case deleteEventMethod: + deleteEvent(call, result) + case deleteEventInstanceMethod: + deleteEvent(call, result) + case createCalendarMethod: + createCalendar(call, result) + case deleteCalendarMethod: + deleteCalendar(call, result) + default: + result(FlutterMethodNotImplemented) + } + } - if (!iCloudSources.isEmpty) { - return iCloudSources.first - } + private func hasPermissions(_ result: FlutterResult) { + let hasPermissions = hasEventPermissions() + result(hasPermissions) + } - return nil - } + private func getSource() -> EKSource? { + let localSources = eventStore.sources.filter { $0.sourceType == .local } - private func createCalendar(_ call: FlutterMethodCall, _ result: FlutterResult) { - let arguments = call.arguments as! Dictionary - let calendar = EKCalendar.init(for: EKEntityType.event, eventStore: eventStore) - do { - calendar.title = arguments[calendarNameArgument] as! String - let calendarColor = arguments[calendarColorArgument] as? String - - if (calendarColor != nil) { - calendar.cgColor = UIColor(hex: calendarColor!)?.cgColor - } - else { - calendar.cgColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0).cgColor // Red colour as a default + if (!localSources.isEmpty) { + return localSources.first } - - guard let source = getSource() else { - result(FlutterError(code: self.genericError, message: "Local calendar was not found.", details: nil)) - return + + if let defaultSource = eventStore.defaultCalendarForNewEvents?.source { + return defaultSource } - calendar.source = source + let iCloudSources = eventStore.sources.filter { $0.sourceType == .calDAV && $0.sourceIdentifier == "iCloud" } - try eventStore.saveCalendar(calendar, commit: true) - result(calendar.calendarIdentifier) - } - catch { - eventStore.reset() - result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + if (!iCloudSources.isEmpty) { + return iCloudSources.first + } + + return nil } - } - - private func retrieveCalendars(_ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - let ekCalendars = self.eventStore.calendars(for: .event) - let defaultCalendar = self.eventStore.defaultCalendarForNewEvents - var calendars = [Calendar]() - for ekCalendar in ekCalendars { - let calendar = Calendar( - id: ekCalendar.calendarIdentifier, - name: ekCalendar.title, - isReadOnly: !ekCalendar.allowsContentModifications, - isDefault: defaultCalendar?.calendarIdentifier == ekCalendar.calendarIdentifier, - color: UIColor(cgColor: ekCalendar.cgColor).rgb()!, - accountName: ekCalendar.source.title, - accountType: getAccountType(ekCalendar.source.sourceType)) - calendars.append(calendar) - } - - self.encodeJsonAndFinish(codable: calendars, result: result) - }, result: result) - } - - private func deleteCalendar(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { + + private func createCalendar(_ call: FlutterMethodCall, _ result: FlutterResult) { let arguments = call.arguments as! Dictionary - let calendarId = arguments[calendarIdArgument] as! String - - let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) - if ekCalendar == nil { - self.finishWithCalendarNotFoundError(result: result, calendarId: calendarId) - return - } - - if !(ekCalendar!.allowsContentModifications) { - self.finishWithCalendarReadOnlyError(result: result, calendarId: calendarId) - return - } - + let calendar = EKCalendar.init(for: EKEntityType.event, eventStore: eventStore) do { - try self.eventStore.removeCalendar(ekCalendar!, commit: true) - result(true) - } catch { - self.eventStore.reset() + calendar.title = arguments[calendarNameArgument] as! String + let calendarColor = arguments[calendarColorArgument] as? String + + if (calendarColor != nil) { + calendar.cgColor = UIColor(hex: calendarColor!)?.cgColor + } + else { + calendar.cgColor = UIColor(red: 255, green: 0, blue: 0, alpha: 0).cgColor // Red colour as a default + } + + guard let source = getSource() else { + result(FlutterError(code: self.genericError, message: "Local calendar was not found.", details: nil)) + return + } + + calendar.source = source + + try eventStore.saveCalendar(calendar, commit: true) + result(calendar.calendarIdentifier) + } + catch { + eventStore.reset() result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) } - }, result: result) - } - - private func getAccountType(_ sourceType: EKSourceType) -> String { - switch (sourceType) { - case .local: - return "Local"; - case .exchange: - return "Exchange"; - case .calDAV: - return "CalDAV"; - case .mobileMe: - return "MobileMe"; - case .subscribed: - return "Subscribed"; - case .birthdays: - return "Birthdays"; - default: - return "Unknown"; } - } - - private func retrieveEvents(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - let arguments = call.arguments as! Dictionary - let calendarId = arguments[calendarIdArgument] as! String - let startDateMillisecondsSinceEpoch = arguments[startDateArgument] as? NSNumber - let endDateDateMillisecondsSinceEpoch = arguments[endDateArgument] as? NSNumber - let eventIdArgs = arguments[eventIdsArgument] as? [String] - var events = [Event]() - let specifiedStartEndDates = startDateMillisecondsSinceEpoch != nil && endDateDateMillisecondsSinceEpoch != nil - if specifiedStartEndDates { - let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch!.doubleValue / 1000.0) - let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch!.doubleValue / 1000.0) + + private func retrieveCalendars(_ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let ekCalendars = self.eventStore.calendars(for: .event) + let defaultCalendar = self.eventStore.defaultCalendarForNewEvents + var calendars = [DeviceCalendar]() + for ekCalendar in ekCalendars { + let calendar = DeviceCalendar( + id: ekCalendar.calendarIdentifier, + name: ekCalendar.title, + isReadOnly: !ekCalendar.allowsContentModifications, + isDefault: defaultCalendar?.calendarIdentifier == ekCalendar.calendarIdentifier, + color: UIColor(cgColor: ekCalendar.cgColor).rgb()!, + accountName: ekCalendar.source.title, + accountType: getAccountType(ekCalendar.source.sourceType)) + calendars.append(calendar) + } + + self.encodeJsonAndFinish(codable: calendars, result: result) + }, result: result) + } + + private func deleteCalendar(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) - if ekCalendar != nil { - let predicate = self.eventStore.predicateForEvents( - withStart: startDate, - end: endDate, - calendars: [ekCalendar!]) - let ekEvents = self.eventStore.events(matching: predicate) - for ekEvent in ekEvents { - let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent) - events.append(event) - } + if ekCalendar == nil { + self.finishWithCalendarNotFoundError(result: result, calendarId: calendarId) + return } + + if !(ekCalendar!.allowsContentModifications) { + self.finishWithCalendarReadOnlyError(result: result, calendarId: calendarId) + return + } + + do { + try self.eventStore.removeCalendar(ekCalendar!, commit: true) + result(true) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + }, result: result) + } + + private func getAccountType(_ sourceType: EKSourceType) -> String { + switch (sourceType) { + case .local: + return "Local"; + case .exchange: + return "Exchange"; + case .calDAV: + return "CalDAV"; + case .mobileMe: + return "MobileMe"; + case .subscribed: + return "Subscribed"; + case .birthdays: + return "Birthdays"; + default: + return "Unknown"; } - - guard let eventIds = eventIdArgs else { - self.encodeJsonAndFinish(codable: events, result: result) - return - } - - if specifiedStartEndDates { - events = events.filter({ (e) -> Bool in - e.calendarId == calendarId && eventIds.contains(e.eventId) - }) - - self.encodeJsonAndFinish(codable: events, result: result) - return - } - - for eventId in eventIds { - let ekEvent = self.eventStore.event(withIdentifier: eventId) - if ekEvent == nil { - continue + } + + private func retrieveEvents(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + let startDateMillisecondsSinceEpoch = arguments[startDateArgument] as? NSNumber + let endDateDateMillisecondsSinceEpoch = arguments[endDateArgument] as? NSNumber + let eventIdArgs = arguments[eventIdsArgument] as? [String] + var events = [Event]() + let specifiedStartEndDates = startDateMillisecondsSinceEpoch != nil && endDateDateMillisecondsSinceEpoch != nil + if specifiedStartEndDates { + let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch!.doubleValue / 1000.0) + let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch!.doubleValue / 1000.0) + let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) + if ekCalendar != nil { + let predicate = self.eventStore.predicateForEvents( + withStart: startDate, + end: endDate, + calendars: [ekCalendar!]) + let ekEvents = self.eventStore.events(matching: predicate) + for ekEvent in ekEvents { + let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent) + events.append(event) + } + } + } + + guard let eventIds = eventIdArgs else { + self.encodeJsonAndFinish(codable: events, result: result) + return + } + + if specifiedStartEndDates { + events = events.filter({ (e) -> Bool in + e.calendarId == calendarId && eventIds.contains(e.eventId) + }) + + self.encodeJsonAndFinish(codable: events, result: result) + return } - + + for eventId in eventIds { + let ekEvent = self.eventStore.event(withIdentifier: eventId) + if ekEvent == nil { + continue + } + let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent!) + events.append(event) } - + self.encodeJsonAndFinish(codable: events, result: result) }, result: result) } - + private func createEventFromEkEvent(calendarId: String, ekEvent: EKEvent) -> Event { var attendees = [Attendee]() if ekEvent.attendees != nil { @@ -356,18 +371,18 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele if attendee == nil { continue } - + attendees.append(attendee!) } } - + var reminders = [Reminder]() if ekEvent.alarms != nil { for alarm in ekEvent.alarms! { reminders.append(Reminder(minutes: Int(-alarm.relativeOffset / 60))) } } - + let recurrenceRule = parseEKRecurrenceRules(ekEvent) let event = Event( eventId: ekEvent.eventIdentifier, @@ -390,33 +405,33 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return event } - - private func convertEkParticipantToAttendee(ekParticipant: EKParticipant?) -> Attendee? { - if ekParticipant == nil || ekParticipant?.emailAddress == nil { - return nil + + private func convertEkParticipantToAttendee(ekParticipant: EKParticipant?) -> Attendee? { + if ekParticipant == nil || ekParticipant?.emailAddress == nil { + return nil + } + + let attendee = Attendee( + name: ekParticipant!.name, + emailAddress: ekParticipant!.emailAddress!, + role: ekParticipant!.participantRole.rawValue, + attendanceStatus: ekParticipant!.participantStatus.rawValue, + isCurrentUser: ekParticipant!.isCurrentUser + ) + + return attendee } - - let attendee = Attendee( - name: ekParticipant!.name, - emailAddress: ekParticipant!.emailAddress!, - role: ekParticipant!.participantRole.rawValue, - attendanceStatus: ekParticipant!.participantStatus.rawValue, - isCurrentUser: ekParticipant!.isCurrentUser - ) - - return attendee - } - + private func convertEkEventAvailability(ekEventAvailability: EKEventAvailability?) -> Availability? { switch ekEventAvailability { case .busy: - return Availability.BUSY + return Availability.BUSY case .free: return Availability.FREE - case .tentative: - return Availability.TENTATIVE - case .unavailable: - return Availability.UNAVAILABLE + case .tentative: + return Availability.TENTATIVE + case .unavailable: + return Availability.UNAVAILABLE default: return nil } @@ -441,167 +456,217 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele var recurrenceRule: RecurrenceRule? if ekEvent.hasRecurrenceRules { let ekRecurrenceRule = ekEvent.recurrenceRules![0] - var frequency: Int + var frequency: String switch ekRecurrenceRule.frequency { case EKRecurrenceFrequency.daily: - frequency = 0 + frequency = "DAILY" case EKRecurrenceFrequency.weekly: - frequency = 1 + frequency = "WEEKLY" case EKRecurrenceFrequency.monthly: - frequency = 2 + frequency = "MONTHLY" case EKRecurrenceFrequency.yearly: - frequency = 3 + frequency = "YEARLY" default: - frequency = 0 + frequency = "DAILY" } - - var totalOccurrences: Int? - var endDate: Int64? + + var count: Int? + var endDate: String? if(ekRecurrenceRule.recurrenceEnd?.occurrenceCount != nil && ekRecurrenceRule.recurrenceEnd?.occurrenceCount != 0) { - totalOccurrences = ekRecurrenceRule.recurrenceEnd?.occurrenceCount - } - - let endDateMs = ekRecurrenceRule.recurrenceEnd?.endDate?.millisecondsSinceEpoch - if(endDateMs != nil) { - endDate = Int64(exactly: endDateMs!) - } - - var weekOfMonth = ekRecurrenceRule.setPositions?.first?.intValue - - var daysOfWeek: [Int]? - if ekRecurrenceRule.daysOfTheWeek != nil && !ekRecurrenceRule.daysOfTheWeek!.isEmpty { - daysOfWeek = [] - for dayOfWeek in ekRecurrenceRule.daysOfTheWeek! { - daysOfWeek!.append(dayOfWeek.dayOfTheWeek.rawValue - 1) - - if weekOfMonth == nil { - weekOfMonth = dayOfWeek.weekNumber - } - } + count = ekRecurrenceRule.recurrenceEnd?.occurrenceCount + } + + let endDateRaw = ekRecurrenceRule.recurrenceEnd?.endDate + if(endDateRaw != nil) { + endDate = formateDateTime(dateTime: endDateRaw!) } - - // For recurrence of nth day of nth month every year, no calendar parameters are given - // So we need to explicitly set them from event start date - var dayOfMonth = ekRecurrenceRule.daysOfTheMonth?.first?.intValue - var monthOfYear = ekRecurrenceRule.monthsOfTheYear?.first?.intValue - if (ekRecurrenceRule.frequency == EKRecurrenceFrequency.yearly - && weekOfMonth == nil && dayOfMonth == nil && monthOfYear == nil) { - let dateFormatter = DateFormatter() - - // Setting day of the month - dateFormatter.dateFormat = "d" - dayOfMonth = Int(dateFormatter.string(from: ekEvent.startDate)) - - // Setting month of the year - dateFormatter.dateFormat = "M" - monthOfYear = Int(dateFormatter.string(from: ekEvent.startDate)) - } - + + let byWeekDays = ekRecurrenceRule.daysOfTheWeek + let byMonthDays = ekRecurrenceRule.daysOfTheMonth + let byYearDays = ekRecurrenceRule.daysOfTheYear + let byWeeks = ekRecurrenceRule.weeksOfTheYear + let byMonths = ekRecurrenceRule.monthsOfTheYear + let bySetPositions = ekRecurrenceRule.setPositions + recurrenceRule = RecurrenceRule( - recurrenceFrequency: frequency, - totalOccurrences: totalOccurrences, + freq: frequency, + count: count, interval: ekRecurrenceRule.interval, - endDate: endDate, - daysOfWeek: daysOfWeek, - dayOfMonth: dayOfMonth, - monthOfYear: monthOfYear, - weekOfMonth: weekOfMonth) + until: endDate, + byday: byWeekDays?.map {weekDayToString($0)}, + bymonthday: byMonthDays?.map {Int(truncating: $0)}, + byyearday: byYearDays?.map {Int(truncating: $0)}, + byweekno: byWeeks?.map {Int(truncating: $0)}, + bymonth: byMonths?.map {Int(truncating: $0)}, + bysetpos: bySetPositions?.map {Int(truncating: $0)}, + sourceRruleString: rruleStringFromEKRRule(ekRecurrenceRule) + ) } - + //print("RECURRENCERULE_RESULT: \(recurrenceRule as AnyObject)") return recurrenceRule } - + + private func weekDayToString(_ entry : EKRecurrenceDayOfWeek) -> String { + let weekNumber = entry.weekNumber + let day = dayValueToString(entry.dayOfTheWeek.rawValue) + if (weekNumber == 0) { + return "\(day)" + } else { + return "\(weekNumber)\(day)" + } + } + + private func dayValueToString(_ day: Int) -> String { + switch day { + case 1: return "SU" + case 2: return "MO" + case 3: return "TU" + case 4: return "WE" + case 5: return "TH" + case 6: return "FR" + case 7: return "SA" + default: return "SU" + } + } + + private func formateDateTime(dateTime: Date) -> String { + var calendar = Calendar.current + calendar.timeZone = TimeZone.current + + func twoDigits(_ n: Int) -> String { + if (n < 10) {return "0\(n)"} else {return "\(n)"} + } + + func fourDigits(_ n: Int) -> String { + let absolute = abs(n) + let sign = n < 0 ? "-" : "" + if (absolute >= 1000) {return "\(n)"} + if (absolute >= 100) {return "\(sign)0\(absolute)"} + if (absolute >= 10) {return "\(sign)00\(absolute)"} + return "\(sign)000\(absolute)" + } + + let year = calendar.component(.year, from: dateTime) + let month = calendar.component(.month, from: dateTime) + let day = calendar.component(.day, from: dateTime) + let hour = calendar.component(.hour, from: dateTime) + let minutes = calendar.component(.minute, from: dateTime) + let seconds = calendar.component(.second, from: dateTime) + + assert(year >= 0 && year <= 9999) + + let yearString = fourDigits(year) + let monthString = twoDigits(month) + let dayString = twoDigits(day) + let hourString = twoDigits(hour) + let minuteString = twoDigits(minutes) + let secondString = twoDigits(seconds) + let utcSuffix = calendar.timeZone == TimeZone(identifier: "UTC") ? "Z" : "" + return "\(yearString)-\(monthString)-\(dayString)T\(hourString):\(minuteString):\(secondString)\(utcSuffix)" + + } + private func createEKRecurrenceRules(_ arguments: [String : AnyObject]) -> [EKRecurrenceRule]?{ let recurrenceRuleArguments = arguments[recurrenceRuleArgument] as? Dictionary + + //print("ARGUMENTS: \(recurrenceRuleArguments as AnyObject)") + if recurrenceRuleArguments == nil { return nil } - - let recurrenceFrequencyIndex = recurrenceRuleArguments![recurrenceFrequencyArgument] as? NSInteger - let totalOccurrences = recurrenceRuleArguments![totalOccurrencesArgument] as? NSInteger + + let recurrenceFrequency = recurrenceRuleArguments![recurrenceFrequencyArgument] as? String + let totalOccurrences = recurrenceRuleArguments![countArgument] as? NSInteger let interval = recurrenceRuleArguments![intervalArgument] as? NSInteger var recurrenceInterval = 1 - let endDate = recurrenceRuleArguments![endDateArgument] as? NSNumber - let namedFrequency = validFrequencyTypes[recurrenceFrequencyIndex!] - - var recurrenceEnd:EKRecurrenceEnd? + var endDate = recurrenceRuleArguments![untilArgument] as? String + var namedFrequency: EKRecurrenceFrequency + switch recurrenceFrequency { + case "YEARLY": + namedFrequency = EKRecurrenceFrequency.yearly + case "MONTHLY": + namedFrequency = EKRecurrenceFrequency.monthly + case "WEEKLY": + namedFrequency = EKRecurrenceFrequency.weekly + case "DAILY": + namedFrequency = EKRecurrenceFrequency.daily + default: + namedFrequency = EKRecurrenceFrequency.daily + } + + var recurrenceEnd: EKRecurrenceEnd? if endDate != nil { - recurrenceEnd = EKRecurrenceEnd(end: Date.init(timeIntervalSince1970: endDate!.doubleValue / 1000)) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + + if (!endDate!.hasSuffix("Z")){ + endDate!.append("Z") + } + + let dateTime = dateFormatter.date(from: endDate!) + if dateTime != nil { + recurrenceEnd = EKRecurrenceEnd(end: dateTime!) + } } else if(totalOccurrences != nil && totalOccurrences! > 0) { recurrenceEnd = EKRecurrenceEnd(occurrenceCount: totalOccurrences!) } - + if interval != nil && interval! > 1 { recurrenceInterval = interval! } - - let daysOfWeekIndices = recurrenceRuleArguments![daysOfWeekArgument] as? [Int] - var daysOfWeek : [EKRecurrenceDayOfWeek]? - - if daysOfWeekIndices != nil && !daysOfWeekIndices!.isEmpty { - daysOfWeek = [] - for dayOfWeekIndex in daysOfWeekIndices! { - // Append week number to BYDAY for yearly or monthly with 'last' week number - if let weekOfMonth = recurrenceRuleArguments![weekOfMonthArgument] as? Int { - if namedFrequency == EKRecurrenceFrequency.yearly || weekOfMonth == -1 { - daysOfWeek!.append(EKRecurrenceDayOfWeek.init( - dayOfTheWeek: EKWeekday.init(rawValue: dayOfWeekIndex + 1)!, - weekNumber: weekOfMonth - )) - } - } else { - daysOfWeek!.append(EKRecurrenceDayOfWeek.init(EKWeekday.init(rawValue: dayOfWeekIndex + 1)!)) - } - } - } - - var dayOfMonthArray : [NSNumber]? - if let dayOfMonth = recurrenceRuleArguments![dayOfMonthArgument] as? Int { - dayOfMonthArray = [] - dayOfMonthArray!.append(NSNumber(value: dayOfMonth)) - } - - var monthOfYearArray : [NSNumber]? - if let monthOfYear = recurrenceRuleArguments![monthOfYearArgument] as? Int { - monthOfYearArray = [] - monthOfYearArray!.append(NSNumber(value: monthOfYear)) - } - - // Append BYSETPOS only on monthly (but not last), yearly's week number (and last for monthly) appends to BYDAY - var weekOfMonthArray : [NSNumber]? - if namedFrequency == EKRecurrenceFrequency.monthly { - if let weekOfMonth = recurrenceRuleArguments![weekOfMonthArgument] as? Int { - if weekOfMonth != -1 { - weekOfMonthArray = [] - weekOfMonthArray!.append(NSNumber(value: weekOfMonth)) - } + + let byWeekDaysStrings = recurrenceRuleArguments![byWeekDaysArgument] as? [String] + var byWeekDays = [EKRecurrenceDayOfWeek]() + + if (byWeekDaysStrings != nil) { + byWeekDaysStrings?.forEach { string in + let entry = recurrenceDayOfWeekFromString(recDay: string) + if entry != nil {byWeekDays.append(entry!)} } } - - return [EKRecurrenceRule( + + let byMonthDays = recurrenceRuleArguments![byMonthDaysArgument] as? [Int] + let byYearDays = recurrenceRuleArguments![byYearDaysArgument] as? [Int] + let byWeeks = recurrenceRuleArguments![byWeeksArgument] as? [Int] + let byMonths = recurrenceRuleArguments![byMonthsArgument] as? [Int] + let bySetPositions = recurrenceRuleArguments![bySetPositionsArgument] as? [Int] + + let ekrecurrenceRule = EKRecurrenceRule( recurrenceWith: namedFrequency, interval: recurrenceInterval, - daysOfTheWeek: daysOfWeek, - daysOfTheMonth: dayOfMonthArray, - monthsOfTheYear: monthOfYearArray, - weeksOfTheYear: nil, - daysOfTheYear: nil, - setPositions: weekOfMonthArray, - end: recurrenceEnd)] + daysOfTheWeek: byWeekDays.isEmpty ? nil : byWeekDays, + daysOfTheMonth: byMonthDays?.map {NSNumber(value: $0)}, + monthsOfTheYear: byMonths?.map {NSNumber(value: $0)}, + weeksOfTheYear: byWeeks?.map {NSNumber(value: $0)}, + daysOfTheYear: byYearDays?.map {NSNumber(value: $0)}, + setPositions: bySetPositions?.map {NSNumber(value: $0)}, + end: recurrenceEnd) + //print("ekrecurrenceRule: \(String(describing: ekrecurrenceRule))") + return [ekrecurrenceRule] } - + + private func rruleStringFromEKRRule(_ ekRrule: EKRecurrenceRule) -> String { + let ekRRuleAnyObject = ekRrule as AnyObject + var ekRRuleString = "\(ekRRuleAnyObject)" + if let range = ekRRuleString.range(of: "RRULE ") { + ekRRuleString = String(ekRRuleString[range.upperBound...]) + //print("EKRULE_RESULT_STRING: \(ekRRuleString)") + } + return ekRRuleString + } + private func setAttendees(_ arguments: [String : AnyObject], _ ekEvent: EKEvent?) { let attendeesArguments = arguments[attendeesArgument] as? [Dictionary] if attendeesArguments == nil { return } - + var attendees = [EKParticipant]() for attendeeArguments in attendeesArguments! { let name = attendeeArguments[nameArgument] as! String let emailAddress = attendeeArguments[emailAddressArgument] as! String let role = attendeeArguments[roleArgument] as! Int - + if (ekEvent!.attendees != nil) { let existingAttendee = ekEvent!.attendees!.first { element in return element.emailAddress == emailAddress @@ -611,40 +676,88 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele continue } } - + let attendee = createParticipant( name: name, emailAddress: emailAddress, role: role) - + if (attendee == nil) { continue } - + attendees.append(attendee!) } - + ekEvent!.setValue(attendees, forKey: "attendees") } - + private func createReminders(_ arguments: [String : AnyObject]) -> [EKAlarm]?{ let remindersArguments = arguments[remindersArgument] as? [Dictionary] if remindersArguments == nil { return nil } - + var reminders = [EKAlarm]() for reminderArguments in remindersArguments! { let minutes = reminderArguments[minutesArgument] as! Int reminders.append(EKAlarm.init(relativeOffset: 60 * Double(-minutes))) } - + return reminders } - + + private func recurrenceDayOfWeekFromString(recDay: String) -> EKRecurrenceDayOfWeek? { + let results = recDay.match("(?:(\\+|-)?([0-9]{1,2}))?([A-Za-z]{2})").first + var recurrenceDayOfWeek : EKRecurrenceDayOfWeek? + if (results != nil) { + var occurrence : Int? + let numberMatch = results![2] + if (!numberMatch.isEmpty) { + occurrence = Int(numberMatch) + if (1 > occurrence! || occurrence! > 53) { + print("OCCURRENCE_ERROR: OUT OF RANGE -> \(String(describing: occurrence))") + } + if (results![1] == "-") { + occurrence = -occurrence! + } + } + let dayMatch = results![3] + + var weekday = EKWeekday.monday + + switch dayMatch { + case "MO": + weekday = EKWeekday.monday + case "TU": + weekday = EKWeekday.tuesday + case "WE": + weekday = EKWeekday.wednesday + case "TH": + weekday = EKWeekday.thursday + case "FR": + weekday = EKWeekday.friday + case "SA": + weekday = EKWeekday.saturday + case "SU": + weekday = EKWeekday.sunday + default: + weekday = EKWeekday.sunday + } + + if occurrence != nil { + recurrenceDayOfWeek = EKRecurrenceDayOfWeek(dayOfTheWeek: weekday, weekNumber: occurrence!) + } else { + recurrenceDayOfWeek = EKRecurrenceDayOfWeek(weekday) + } + } + return recurrenceDayOfWeek + } + + private func setAvailability(_ arguments: [String : AnyObject]) -> EKEventAvailability? { - guard let availabilityValue = arguments[availabilityArgument] as? String else { - return .unavailable + guard let availabilityValue = arguments[availabilityArgument] as? String else { + return .unavailable } switch availabilityValue.uppercased() { @@ -652,15 +765,15 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return .busy case Availability.FREE.rawValue: return .free - case Availability.TENTATIVE.rawValue: - return .tentative + case Availability.TENTATIVE.rawValue: + return .tentative case Availability.UNAVAILABLE.rawValue: return .unavailable default: return nil } } - + private func createOrUpdateEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let arguments = call.arguments as! Dictionary @@ -681,12 +794,12 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele self.finishWithCalendarNotFoundError(result: result, calendarId: calendarId) return } - + if !(ekCalendar!.allowsContentModifications) { self.finishWithCalendarReadOnlyError(result: result, calendarId: calendarId) return } - + var ekEvent: EKEvent? if eventId == nil { ekEvent = EKEvent.init(eventStore: self.eventStore) @@ -697,7 +810,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return } } - + ekEvent!.title = title ekEvent!.notes = description ekEvent!.isAllDay = isAllDay @@ -705,7 +818,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele if (isAllDay) { ekEvent!.endDate = startDate } else { ekEvent!.endDate = endDate - + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone } @@ -720,15 +833,15 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele else { ekEvent!.url = nil } - + ekEvent!.recurrenceRules = createEKRecurrenceRules(arguments) setAttendees(arguments, ekEvent) ekEvent!.alarms = createReminders(arguments) - + if let availability = setAvailability(arguments) { ekEvent!.availability = availability } - + do { try self.eventStore.save(ekEvent!, span: .futureEvents) result(ekEvent!.eventIdentifier) @@ -738,7 +851,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } }, result: result) } - + private func createParticipant(name: String, emailAddress: String, role: Int) -> EKParticipant? { let ekAttendeeClass: AnyClass? = NSClassFromString("EKAttendee") if let type = ekAttendeeClass as? NSObject.Type { @@ -750,7 +863,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } return nil } - + private func deleteEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let arguments = call.arguments as! Dictionary @@ -759,25 +872,25 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let startDateNumber = arguments[eventStartDateArgument] as? NSNumber let endDateNumber = arguments[eventEndDateArgument] as? NSNumber let followingInstances = arguments[followingInstancesArgument] as? Bool - + let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) if ekCalendar == nil { self.finishWithCalendarNotFoundError(result: result, calendarId: calendarId) return } - + if !(ekCalendar!.allowsContentModifications) { self.finishWithCalendarReadOnlyError(result: result, calendarId: calendarId) return } - + if (startDateNumber == nil && endDateNumber == nil && followingInstances == nil) { let ekEvent = self.eventStore.event(withIdentifier: eventId) if ekEvent == nil { self.finishWithEventNotFoundError(result: result, eventId: eventId) return } - + do { try self.eventStore.remove(ekEvent!, span: .futureEvents) result(true) @@ -789,17 +902,17 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele else { let startDate = Date (timeIntervalSince1970: startDateNumber!.doubleValue / 1000.0) let endDate = Date (timeIntervalSince1970: endDateNumber!.doubleValue / 1000.0) - + let predicate = self.eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: nil) let foundEkEvents = self.eventStore.events(matching: predicate) as [EKEvent]? - + if foundEkEvents == nil || foundEkEvents?.count == 0 { self.finishWithEventNotFoundError(result: result, eventId: eventId) return } - + let ekEvent = foundEkEvents!.first(where: {$0.eventIdentifier == eventId}) - + do { if (!followingInstances!) { try self.eventStore.remove(ekEvent!, span: .thisEvent, commit: true) @@ -807,7 +920,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele else { try self.eventStore.remove(ekEvent!, span: .futureEvents, commit: true) } - + result(true) } catch { self.eventStore.reset() @@ -817,80 +930,80 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele }, result: result) } - private func showEventModal(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - let arguments = call.arguments as! Dictionary - let eventId = arguments[eventIdArgument] as! String - let event = self.eventStore.event(withIdentifier: eventId) - - if event != nil { - let eventController = EKEventViewController() - eventController.event = event! - eventController.delegate = self - eventController.allowsEditing = true - eventController.allowsCalendarPreview = true - - let flutterViewController = getTopMostViewController() - let navigationController = UINavigationController(rootViewController: eventController) - - navigationController.toolbar.isTranslucent = false - navigationController.toolbar.tintColor = .blue - navigationController.toolbar.backgroundColor = .white - - flutterViewController.present(navigationController, animated: true, completion: nil) - - - } else { - result(FlutterError(code: self.genericError, message: self.eventNotFoundErrorMessageFormat, details: nil)) - } - }, result: result) - } - - public func eventViewController(_ controller: EKEventViewController, didCompleteWith action: EKEventViewAction) { - controller.dismiss(animated: true, completion: nil) - - if flutterResult != nil { - switch action { - case .done: - flutterResult!(nil) - case .responded: - flutterResult!(nil) - case .deleted: - flutterResult!(nil) - @unknown default: - flutterResult!(nil) + private func showEventModal(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let arguments = call.arguments as! Dictionary + let eventId = arguments[eventIdArgument] as! String + let event = self.eventStore.event(withIdentifier: eventId) + + if event != nil { + let eventController = EKEventViewController() + eventController.event = event! + eventController.delegate = self + eventController.allowsEditing = true + eventController.allowsCalendarPreview = true + + let flutterViewController = getTopMostViewController() + let navigationController = UINavigationController(rootViewController: eventController) + + navigationController.toolbar.isTranslucent = false + navigationController.toolbar.tintColor = .blue + navigationController.toolbar.backgroundColor = .white + + flutterViewController.present(navigationController, animated: true, completion: nil) + + + } else { + result(FlutterError(code: self.genericError, message: self.eventNotFoundErrorMessageFormat, details: nil)) + } + }, result: result) + } + + public func eventViewController(_ controller: EKEventViewController, didCompleteWith action: EKEventViewAction) { + controller.dismiss(animated: true, completion: nil) + + if flutterResult != nil { + switch action { + case .done: + flutterResult!(nil) + case .responded: + flutterResult!(nil) + case .deleted: + flutterResult!(nil) + @unknown default: + flutterResult!(nil) + } } } - } - - private func getTopMostViewController() -> UIViewController { - var topController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController - while ((topController?.presentedViewController) != nil) { - topController = topController?.presentedViewController - } - - return topController! - } - + + private func getTopMostViewController() -> UIViewController { + var topController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController + while ((topController?.presentedViewController) != nil) { + topController = topController?.presentedViewController + } + + return topController! + } + private func finishWithUnauthorizedError(result: @escaping FlutterResult) { result(FlutterError(code:self.unauthorizedErrorCode, message: self.unauthorizedErrorMessage, details: nil)) } - + private func finishWithCalendarNotFoundError(result: @escaping FlutterResult, calendarId: String) { let errorMessage = String(format: self.calendarNotFoundErrorMessageFormat, calendarId) result(FlutterError(code:self.notFoundErrorCode, message: errorMessage, details: nil)) } - + private func finishWithCalendarReadOnlyError(result: @escaping FlutterResult, calendarId: String) { let errorMessage = String(format: self.calendarReadOnlyErrorMessageFormat, calendarId) result(FlutterError(code:self.notAllowed, message: errorMessage, details: nil)) } - + private func finishWithEventNotFoundError(result: @escaping FlutterResult, eventId: String) { let errorMessage = String(format: self.eventNotFoundErrorMessageFormat, eventId) result(FlutterError(code:self.notFoundErrorCode, message: errorMessage, details: nil)) } - + private func encodeJsonAndFinish(codable: T, result: @escaping FlutterResult) { do { let jsonEncoder = JSONEncoder() @@ -901,7 +1014,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele result(FlutterError(code: genericError, message: error.localizedDescription, details: nil)) } } - + private func checkPermissionsThenExecute(permissionsGrantedAction: () -> Void, result: @escaping FlutterResult) { if hasEventPermissions() { permissionsGrantedAction() @@ -909,7 +1022,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } self.finishWithUnauthorizedError(result: result) } - + private func requestPermissions(completion: @escaping (Bool) -> Void) { if hasEventPermissions() { completion(true) @@ -920,12 +1033,12 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele completion(accessGranted) }) } - + private func hasEventPermissions() -> Bool { let status = EKEventStore.authorizationStatus(for: .event) return status == EKAuthorizationStatus.authorized } - + private func requestPermissions(_ result: @escaping FlutterResult) { if hasEventPermissions() { result(true) @@ -964,7 +1077,7 @@ extension UIColor { return nil } } - + public convenience init?(hex: String) { let r, g, b, a: CGFloat @@ -981,7 +1094,7 @@ extension UIColor { r = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 g = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 b = CGFloat((hexNumber & 0x000000ff)) / 255 - + self.init(red: r, green: g, blue: b, alpha: a) return } @@ -990,5 +1103,5 @@ extension UIColor { return nil } -} +} diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index 15d51182..b25b1f29 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -1,14 +1,14 @@ library device_calendar; export 'src/common/calendar_enums.dart'; -export 'src/common/recurrence_frequency.dart'; export 'src/models/attendee.dart'; export 'src/models/calendar.dart'; export 'src/models/result.dart'; export 'src/models/reminder.dart'; export 'src/models/event.dart'; export 'src/models/retrieve_events_params.dart'; -export 'src/models/recurrence_rule.dart'; +export 'package:rrule/rrule.dart'; +export 'package:rrule/src/frequency.dart'; export 'src/models/platform_specifics/ios/attendee_details.dart'; export 'src/models/platform_specifics/ios/attendance_status.dart'; export 'src/models/platform_specifics/android/attendee_details.dart'; diff --git a/lib/src/common/calendar_enums.dart b/lib/src/common/calendar_enums.dart index 2e7cd246..aa77ec02 100644 --- a/lib/src/common/calendar_enums.dart +++ b/lib/src/common/calendar_enums.dart @@ -86,6 +86,7 @@ extension DayOfWeekExtension on DayOfWeek { } int get value => _value(this); + String get enumToString => _enumToString(this); } @@ -122,6 +123,7 @@ extension DaysOfWeekGroupExtension on DayOfWeekGroup { } List get getDays => _getDays(this); + String get enumToString => _enumToString(this); } @@ -162,6 +164,7 @@ extension MonthOfYearExtension on MonthOfYear { } int get value => _value(this); + String get enumToString => _enumToString(this); } @@ -188,6 +191,7 @@ extension WeekNumberExtension on WeekNumber { } int get value => _value(this); + String get enumToString => _enumToString(this); } @@ -262,7 +266,9 @@ extension IntExtensions on int { } DayOfWeek get getDayOfWeekEnumValue => _getDayOfWeekEnumValue(this); + MonthOfYear get getMonthOfYearEnumValue => _getMonthOfYearEnumValue(this); + WeekNumber get getWeekNumberEnumValue => _getWeekNumberEnumValue(this); } @@ -306,4 +312,4 @@ extension EventStatusExtensions on EventStatus { } String get enumToString => _enumToString(this); -} \ No newline at end of file +} diff --git a/lib/src/common/recurrence_frequency.dart b/lib/src/common/recurrence_frequency.dart deleted file mode 100644 index 176cf3de..00000000 --- a/lib/src/common/recurrence_frequency.dart +++ /dev/null @@ -1,6 +0,0 @@ -enum RecurrenceFrequency { - Daily, - Weekly, - Monthly, - Yearly, -} diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index eb1d9db1..709aec2a 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:sprintf/sprintf.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/timezone.dart'; @@ -80,42 +79,48 @@ class DeviceCalendarPlugin { String? calendarId, RetrieveEventsParams? retrieveEventsParams, ) async { - return _invokeChannelMethod( - ChannelConstants.methodNameRetrieveEvents, - assertParameters: (result) { - _validateCalendarIdParameter( - result, - calendarId, - ); - - _assertParameter( - result, - !((retrieveEventsParams?.eventIds?.isEmpty ?? true) && - ((retrieveEventsParams?.startDate == null || - retrieveEventsParams?.endDate == null) || - (retrieveEventsParams?.startDate != null && - retrieveEventsParams?.endDate != null && - (retrieveEventsParams != null && - retrieveEventsParams.startDate! - .isAfter(retrieveEventsParams.endDate!))))), - ErrorCodes.invalidArguments, - ErrorMessages.invalidRetrieveEventsParams, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarId: calendarId, - ChannelConstants.parameterNameStartDate: - retrieveEventsParams?.startDate?.millisecondsSinceEpoch, - ChannelConstants.parameterNameEndDate: - retrieveEventsParams?.endDate?.millisecondsSinceEpoch, - ChannelConstants.parameterNameEventIds: retrieveEventsParams?.eventIds, - }, - evaluateResponse: (rawData) => UnmodifiableListView( + return _invokeChannelMethod(ChannelConstants.methodNameRetrieveEvents, + assertParameters: (result) { + _validateCalendarIdParameter( + result, + calendarId, + ); + + _assertParameter( + result, + !((retrieveEventsParams?.eventIds?.isEmpty ?? true) && + ((retrieveEventsParams?.startDate == null || + retrieveEventsParams?.endDate == null) || + (retrieveEventsParams?.startDate != null && + retrieveEventsParams?.endDate != null && + (retrieveEventsParams != null && + retrieveEventsParams.startDate! + .isAfter(retrieveEventsParams.endDate!))))), + ErrorCodes.invalidArguments, + ErrorMessages.invalidRetrieveEventsParams, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarId: calendarId, + ChannelConstants.parameterNameStartDate: + retrieveEventsParams?.startDate?.millisecondsSinceEpoch, + ChannelConstants.parameterNameEndDate: + retrieveEventsParams?.endDate?.millisecondsSinceEpoch, + ChannelConstants.parameterNameEventIds: + retrieveEventsParams?.eventIds, + }, + /*evaluateResponse: (rawData) => UnmodifiableListView( json .decode(rawData) .map((decodedEvent) => Event.fromJson(decodedEvent)), - ), - ); + ),*/ + evaluateResponse: (rawData) => UnmodifiableListView( + json.decode(rawData).map((decodedEvent) { + // debugPrint( + // "JSON_RRULE: ${decodedEvent['recurrenceRule']}, ${(decodedEvent['recurrenceRule']['byday'])}"); + return Event.fromJson(decodedEvent); + }), + )); } /// Deletes an event from a calendar. For a recurring event, this will delete all instances of it.\ @@ -214,9 +219,9 @@ class DeviceCalendarPlugin { // allDay events on Android need to be at midnight UTC event.start = Platform.isAndroid ? TZDateTime.utc(event.start!.year, event.start!.month, - event.start!.day, 0, 0, 0) + event.start!.day, 0, 0, 0) : TZDateTime.from(dateStart, - timeZoneDatabase.locations[event.start!.location.name]!); + timeZoneDatabase.locations[event.start!.location.name]!); } if (event.end != null) { var dateEnd = DateTime( @@ -226,10 +231,10 @@ class DeviceCalendarPlugin { // Jan 1 and 2, should be from Jan 1 00:00:00 to Jan 3 00:00:00 event.end = Platform.isAndroid ? TZDateTime.utc(event.end!.year, event.end!.month, - event.end!.day, 0, 0, 0) - .add(Duration(days: 1)) + event.end!.day, 0, 0, 0) + .add(Duration(days: 1)) : TZDateTime.from(dateEnd, - timeZoneDatabase.locations[event.end!.location.name]!); + timeZoneDatabase.locations[event.end!.location.name]!); } } @@ -322,7 +327,7 @@ class DeviceCalendarPlugin { /// Displays a native iOS view [EKEventViewController] /// https://developer.apple.com/documentation/eventkitui/ekeventviewcontroller - /// + /// /// Allows to change the event's attendance status /// Works only on iOS /// Returns after dismissing EKEventViewController's dialog @@ -337,7 +342,6 @@ class DeviceCalendarPlugin { ); } - Future> _invokeChannelMethod( String channelMethodName, { Function(Result)? assertParameters, @@ -364,8 +368,15 @@ class DeviceCalendarPlugin { } else { result.data = rawData; } - } catch (e) { - _parsePlatformExceptionAndUpdateResult(e as Exception?, result); + } catch (e, s) { + if (e is ArgumentError) { + debugPrint( + "INVOKE_CHANNEL_METHOD_ERROR! Name: ${e.name}, InvalidValue: ${e.invalidValue}, Message: ${e.message}, ${e.toString()}"); + } else if (e is PlatformException) { + debugPrint('INVOKE_CHANNEL_METHOD_ERROR: $e\n$s'); + } else { + _parsePlatformExceptionAndUpdateResult(e as Exception?, result); + } } return result; @@ -375,7 +386,7 @@ class DeviceCalendarPlugin { Exception? exception, Result result) { if (exception == null) { result.errors.add( - ResultError( + const ResultError( ErrorCodes.unknown, ErrorMessages.unknownDeviceIssue, ), @@ -383,22 +394,20 @@ class DeviceCalendarPlugin { return; } - print(exception); + debugPrint('$exception'); if (exception is PlatformException) { result.errors.add( ResultError( ErrorCodes.platformSpecific, - sprintf(ErrorMessages.unknownDeviceExceptionTemplate, - [exception.code, exception.message]), + '${ErrorMessages.unknownDeviceExceptionTemplate}, Code: ${exception.code}, Exception: ${exception.message}', ), ); } else { result.errors.add( ResultError( ErrorCodes.generic, - sprintf(ErrorMessages.unknownDeviceGenericExceptionTemplate, - [exception.toString()]), + '${ErrorMessages.unknownDeviceGenericExceptionTemplate} ${exception.toString}', ), ); } @@ -410,6 +419,9 @@ class DeviceCalendarPlugin { int errorCode, String errorMessage, ) { + if (result.data != null) { + debugPrint("RESULT of _assertParameter: ${result.data}"); + } if (!predicate) { result.errors.add( ResultError(errorCode, errorMessage), diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index bc3e2f2d..93529c38 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -1,9 +1,10 @@ import 'dart:io'; +import 'package:collection/collection.dart'; +import 'package:timezone/timezone.dart'; + import '../../device_calendar.dart'; import '../common/error_messages.dart'; -import 'package:timezone/timezone.dart'; -import 'package:collection/collection.dart'; /// An event associated with a calendar class Event { @@ -48,7 +49,7 @@ class Event { /// Indicates if this event is of confirmed, canceled, tentative or none status EventStatus? status; - + ///Note for development: /// ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. @@ -82,6 +83,7 @@ class Event { if (json == null) { throw ArgumentError(ErrorMessages.fromJsonMapIsNull); } + String? foundUrl; String? startLocationName; String? endLocationName; @@ -170,7 +172,42 @@ class Event { } if (json['recurrenceRule'] != null) { + // debugPrint( + // "EVENT_MODEL: $title; START: $start, END: $end RRULE = ${json['recurrenceRule']}"); + + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'byday'') + if (json['recurrenceRule']['byday'] != null) { + json['recurrenceRule']['byday'] = + json['recurrenceRule']['byday'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'bymonthday'') + if (json['recurrenceRule']['bymonthday'] != null) { + json['recurrenceRule']['bymonthday'] = + json['recurrenceRule']['bymonthday'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'byyearday'') + if (json['recurrenceRule']['byyearday'] != null) { + json['recurrenceRule']['byyearday'] = + json['recurrenceRule']['byyearday'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'byweekno'') + if (json['recurrenceRule']['byweekno'] != null) { + json['recurrenceRule']['byweekno'] = + json['recurrenceRule']['byweekno'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'bymonth'') + if (json['recurrenceRule']['bymonth'] != null) { + json['recurrenceRule']['bymonth'] = + json['recurrenceRule']['bymonth'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'bysetpos'') + if (json['recurrenceRule']['bysetpos'] != null) { + json['recurrenceRule']['bysetpos'] = + json['recurrenceRule']['bysetpos'].cast(); + } + // debugPrint("EVENT_MODEL: $title; RRULE = ${json['recurrenceRule']}"); recurrenceRule = RecurrenceRule.fromJson(json['recurrenceRule']); + // debugPrint("EVENT_MODEL_recurrenceRule: ${recurrenceRule.toString()}"); } if (json['reminders'] != null) { @@ -214,12 +251,13 @@ class Event { if (recurrenceRule != null) { data['recurrenceRule'] = recurrenceRule?.toJson(); + // print("EVENT_TO_JSON_RRULE: ${recurrenceRule?.toJson()}"); } if (reminders != null) { data['reminders'] = reminders?.map((r) => r.toJson()).toList(); } - + // debugPrint("EVENT_TO_JSON: $data"); return data; } @@ -250,6 +288,7 @@ class Event { case 'NONE': return EventStatus.None; } + return null; } bool updateStartLocation(String? newStartLocation) { diff --git a/lib/src/models/recurrence_rule.dart b/lib/src/models/recurrence_rule.dart deleted file mode 100644 index 02825262..00000000 --- a/lib/src/models/recurrence_rule.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:device_calendar/src/common/calendar_enums.dart'; - -import '../common/error_messages.dart'; -import '../common/recurrence_frequency.dart'; - -class RecurrenceRule { - int? totalOccurrences; - - /// The interval between instances of a recurring event - int? interval; - - /// The date a series of recurring events should end - DateTime? endDate; - - /// The frequency of recurring events - RecurrenceFrequency? recurrenceFrequency; - - /// The days of the week that this event occurs on. Only applicable to recurrence rules with a weekly, monthly or yearly frequency - List? daysOfWeek = []; - - /// A day of the month that this event occurs on. Only applicable to recurrence rules with a monthly or yearly frequency - int? dayOfMonth; - - /// A month of the year that the event occurs on. Only applicable to recurrence rules with a yearly frequency - MonthOfYear? monthOfYear; - - /// Filters which recurrences to include in the recurrence rule’s frequency. Only applicable when _isByDayOfMonth is false - WeekNumber? weekOfMonth; - - final String _totalOccurrencesKey = 'totalOccurrences'; - final String _recurrenceFrequencyKey = 'recurrenceFrequency'; - final String _intervalKey = 'interval'; - final String _endDateKey = 'endDate'; - final String _daysOfWeekKey = 'daysOfWeek'; - final String _dayOfMonthKey = 'dayOfMonth'; - final String _monthOfYearKey = 'monthOfYear'; - final String _weekOfMonthKey = 'weekOfMonth'; - - RecurrenceRule(this.recurrenceFrequency, - {this.totalOccurrences, - this.interval, - this.endDate, - this.daysOfWeek, - this.dayOfMonth, - this.monthOfYear, - this.weekOfMonth}) - : assert(!(endDate != null && totalOccurrences != null), - 'Cannot specify both an end date and total occurrences for a recurring event'); - - RecurrenceRule.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - - int? recurrenceFrequencyIndex = json[_recurrenceFrequencyKey]; - if (recurrenceFrequencyIndex == null || - recurrenceFrequencyIndex >= RecurrenceFrequency.values.length) { - throw ArgumentError(ErrorMessages.invalidRecurrencyFrequency); - } - recurrenceFrequency = RecurrenceFrequency.values[recurrenceFrequencyIndex]; - - totalOccurrences = json[_totalOccurrencesKey]; - - interval = json[_intervalKey]; - - int? endDateMillisecondsSinceEpoch = json[_endDateKey]; - if (endDateMillisecondsSinceEpoch != null) { - endDate = - DateTime.fromMillisecondsSinceEpoch(endDateMillisecondsSinceEpoch); - } - - List? daysOfWeekValues = json[_daysOfWeekKey]; - if (daysOfWeekValues != null && daysOfWeekValues is! List) { - daysOfWeek = daysOfWeekValues - .cast() - .map((value) => value.getDayOfWeekEnumValue) - .toList(); - } - - dayOfMonth = json[_dayOfMonthKey]; - monthOfYear = - convertDynamicToInt(json[_monthOfYearKey])?.getMonthOfYearEnumValue; - weekOfMonth = - convertDynamicToInt(json[_weekOfMonthKey])?.getWeekNumberEnumValue; - } - - int? convertDynamicToInt(dynamic value) { - value = value?.toString(); - return value != null ? int.tryParse(value) : null; - } - - Map toJson() { - final data = {}; - - if (totalOccurrences != null) { - data[_totalOccurrencesKey] = totalOccurrences; - } - - if (interval != null) { - data[_intervalKey] = interval; - } - - if (endDate != null) { - data[_endDateKey] = endDate!.millisecondsSinceEpoch; - } - - data[_recurrenceFrequencyKey] = recurrenceFrequency?.index; - - if (daysOfWeek?.isNotEmpty == true) { - data[_daysOfWeekKey] = daysOfWeek!.map((d) => d.value).toList(); - } - - if (monthOfYear != null && - recurrenceFrequency == RecurrenceFrequency.Yearly) { - data[_monthOfYearKey] = monthOfYear!.value; - } - - if (recurrenceFrequency == RecurrenceFrequency.Monthly || - recurrenceFrequency == RecurrenceFrequency.Yearly) { - if (weekOfMonth != null) { - data[_weekOfMonthKey] = weekOfMonth!.value; - } else { - // Days of the month should not be added to the recurrence parameter when WeekOfMonth is used - if (dayOfMonth != null) { - data[_dayOfMonthKey] = dayOfMonth; - } - } - } - - return data; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 19b57aeb..42aae1ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,13 +10,14 @@ dependencies: sprintf: ^6.0.2 timezone: ^0.9.0 flutter_native_timezone: ^2.0.0 + intl: ^0.17.0 + rrule: ^0.2.7 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.1 -# The following section is specific to Flutter. flutter: plugin: platforms: @@ -28,4 +29,4 @@ flutter: environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13" + flutter: ">=1.20.0" diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index 6c105c0a..acc26441 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -198,7 +198,7 @@ void main() { emailAddress: 'test@t.com', role: AttendeeRole.Required, isOrganiser: true); - final recurrence = RecurrenceRule(RecurrenceFrequency.Daily); + final recurrence = RecurrenceRule(frequency: Frequency.daily); final reminder = Reminder(minutes: 10); var event = Event('calendarId', eventId: 'eventId', @@ -231,8 +231,8 @@ void main() { expect(newEvent.attendees, isNotNull); expect(newEvent.attendees?.length, equals(1)); expect(newEvent.recurrenceRule, isNotNull); - expect(newEvent.recurrenceRule?.recurrenceFrequency, - equals(event.recurrenceRule?.recurrenceFrequency)); + expect(newEvent.recurrenceRule?.frequency, + equals(event.recurrenceRule?.frequency)); expect(newEvent.reminders, isNotNull); expect(newEvent.reminders?.length, equals(1)); expect(newEvent.availability, equals(event.availability)); From f7e30e366be651556af67d928edcc9ee9072da0b Mon Sep 17 00:00:00 2001 From: thomassth Date: Fri, 30 Sep 2022 16:38:21 -0400 Subject: [PATCH 02/46] fix for ownerAccount null --- .../com/builttoroam/devicecalendar/models/Calendar.kt | 9 +-------- example/android/app/build.gradle | 2 +- example/android/build.gradle | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt index 09380c22..c7e3f6bf 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -1,13 +1,6 @@ package com.builttoroam.devicecalendar.models -class Calendar( - val id: String, - val name: String, - val color: Int, - val accountName: String, - val accountType: String, - val ownerAccount: String -) { +class Calendar(val id: String, val name: String, val color : Int, val accountName: String, val accountType: String, val ownerAccount: String?) { var isReadOnly: Boolean = false var isDefault: Boolean = false } \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 4af82323..dd924715 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 32 ndkVersion '22.1.7171670' sourceSets { diff --git a/example/android/build.gradle b/example/android/build.gradle index c85ba2ad..d135914c 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.1.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } From 0b9bc9803670371bb6ac557b633b31d0d816d597 Mon Sep 17 00:00:00 2001 From: thomassth Date: Fri, 30 Sep 2022 16:55:45 -0400 Subject: [PATCH 03/46] more calendar data displayed --- example/lib/presentation/pages/calendars.dart | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 5c21d1d4..1113480d 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -70,12 +70,20 @@ class _CalendarsPageState extends State { child: Row( children: [ Expanded( - flex: 1, - child: Text( - _calendars[index].name!, - style: Theme.of(context).textTheme.subtitle1, - ), - ), + flex: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${_calendars[index].id}: ${_calendars[index].name!}", + style: + Theme.of(context).textTheme.subtitle1, + ), + Text( + "Account: ${_calendars[index].accountName!}"), + Text( + "type: ${_calendars[index].accountType}"), + ])), Container( width: 15, height: 15, @@ -84,13 +92,14 @@ class _CalendarsPageState extends State { color: Color(_calendars[index].color!)), ), SizedBox(width: 10), - Container( - margin: const EdgeInsets.fromLTRB(0, 0, 5.0, 0), - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.blueAccent)), - child: Text('Default'), - ), + if (_calendars[index].isDefault!) + Container( + margin: const EdgeInsets.fromLTRB(0, 0, 5.0, 0), + padding: const EdgeInsets.all(3.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.blueAccent)), + child: Text('Default'), + ), Icon(_calendars[index].isReadOnly == true ? Icons.lock : Icons.lock_open) From 10c8d3a8a2b771fa5c0805242f532ea3f3faa5c7 Mon Sep 17 00:00:00 2001 From: thomassth Date: Sat, 1 Oct 2022 01:19:38 -0400 Subject: [PATCH 04/46] example app dark mode --- example/lib/main.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 043898fc..f24e43ba 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -14,6 +14,9 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { return MaterialApp( + theme: ThemeData(), + themeMode: ThemeMode.system, + darkTheme: ThemeData.dark(), routes: { AppRoutes.calendars: (context) { return CalendarsPage(key: Key('calendarsPage')); From ce09d123d296436b8390bd77e25f3082682387a3 Mon Sep 17 00:00:00 2001 From: thomassth Date: Thu, 6 Oct 2022 01:54:24 -0400 Subject: [PATCH 05/46] activate flutter lint --- analysis_options.yaml | 30 ++++++++ example/analysis_options.yaml | 33 ++++++++- example/integration_test/app_test.dart | 4 +- .../integration_test/integration_test.dart | 2 +- example/lib/common/app_routes.dart | 2 +- example/lib/main.dart | 6 +- .../lib/presentation/date_time_picker.dart | 4 +- example/lib/presentation/event_item.dart | 68 +++++++++---------- .../lib/presentation/pages/calendar_add.dart | 12 ++-- .../presentation/pages/calendar_event.dart | 26 +++---- .../presentation/pages/calendar_events.dart | 24 +++---- example/lib/presentation/pages/calendars.dart | 14 ++-- .../presentation/pages/event_attendee.dart | 8 +-- .../presentation/pages/event_reminders.dart | 10 +-- .../presentation/recurring_event_dialog.dart | 12 ++-- example/pubspec.yaml | 1 + lib/src/common/error_messages.dart | 4 +- lib/src/device_calendar.dart | 2 +- lib/src/models/event.dart | 4 +- test/device_calendar_test.dart | 37 +++++----- 20 files changed, 180 insertions(+), 123 deletions(-) create mode 100644 analysis_options.yaml diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..68a79339 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + constant_identifier_names: false # TODO: use lowerCamelCases consistently + avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options \ No newline at end of file diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 88d793de..68a79339 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,3 +1,30 @@ -include: package:pedantic/analysis_options.yaml -#include: package:flutter_lints/flutter.yaml -# TODO: change to flutter lints (https://pub.dev/packages/flutter_lints) \ No newline at end of file +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + constant_identifier_names: false # TODO: use lowerCamelCases consistently + avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options \ No newline at end of file diff --git a/example/integration_test/app_test.dart b/example/integration_test/app_test.dart index 503a8951..6e4a1908 100644 --- a/example/integration_test/app_test.dart +++ b/example/integration_test/app_test.dart @@ -10,7 +10,7 @@ import 'package:device_calendar_example/main.dart' as app; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Calendar plugin example', () { - final eventTitle = Uuid().v1(); + final eventTitle = const Uuid().v1(); final saveEventButtonFinder = find.byKey(const Key('saveEventButton')); final eventTitleFinder = find.text(eventTitle); final firstWritableCalendarFinder = @@ -27,7 +27,7 @@ void main() { testWidgets('select first writable calendar', (WidgetTester tester) async { app.main(); - await tester.pumpAndSettle(Duration(milliseconds: 500)); + await tester.pumpAndSettle(const Duration(milliseconds: 500)); expect(firstWritableCalendarFinder, findsOneWidget); }); testWidgets('go to add event page', (WidgetTester tester) async { diff --git a/example/integration_test/integration_test.dart b/example/integration_test/integration_test.dart index d061d76e..ca6e9ef3 100644 --- a/example/integration_test/integration_test.dart +++ b/example/integration_test/integration_test.dart @@ -2,7 +2,7 @@ import 'package:integration_test/integration_test_driver.dart'; /// Instruction for iOS: /// See `ios.sh` -/// Instruction for android: +/// Instruction for android: /// See `integration_test_android.dart` Future main() => integrationDriver(); diff --git a/example/lib/common/app_routes.dart b/example/lib/common/app_routes.dart index 176a028b..991a9d70 100644 --- a/example/lib/common/app_routes.dart +++ b/example/lib/common/app_routes.dart @@ -1,3 +1,3 @@ class AppRoutes { - static final calendars = '/'; + static const calendars = '/'; } diff --git a/example/lib/main.dart b/example/lib/main.dart index f24e43ba..ffb03586 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,9 +3,11 @@ import 'package:flutter/material.dart'; import 'common/app_routes.dart'; import 'presentation/pages/calendars.dart'; -void main() => runApp(MyApp()); +void main() => runApp(const MyApp()); class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + @override _MyAppState createState() => _MyAppState(); } @@ -19,7 +21,7 @@ class _MyAppState extends State { darkTheme: ThemeData.dark(), routes: { AppRoutes.calendars: (context) { - return CalendarsPage(key: Key('calendarsPage')); + return const CalendarsPage(key: Key('calendarsPage')); } }, ); diff --git a/example/lib/presentation/date_time_picker.dart b/example/lib/presentation/date_time_picker.dart index cf1692e7..dc11e8d9 100644 --- a/example/lib/presentation/date_time_picker.dart +++ b/example/lib/presentation/date_time_picker.dart @@ -23,7 +23,7 @@ class DateTimePicker extends StatelessWidget { final ValueChanged? selectTime; final bool enableTime; - Future _selectDate(BuildContext context) async { + Future _selectDate(BuildContext context) async { final picked = await showDatePicker( context: context, initialDate: selectedDate != null @@ -36,7 +36,7 @@ class DateTimePicker extends StatelessWidget { } } - Future _selectTime(BuildContext context) async { + Future _selectTime(BuildContext context) async { if (selectedTime == null) return; final picked = await showTimePicker(context: context, initialTime: selectedTime!); diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index bc002fd9..a18613a6 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -17,7 +17,7 @@ class EventItem extends StatefulWidget { final VoidCallback _onLoadingStarted; final Function(bool) _onDeleteFinished; - EventItem( + const EventItem( this._calendarEvent, this._deviceCalendarPlugin, this._onLoadingStarted, @@ -55,15 +55,15 @@ class _EventItemState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), + const Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), child: FlutterLogo(), ), ListTile( title: Text(widget._calendarEvent?.title ?? ''), subtitle: Text(widget._calendarEvent?.description ?? '')), Container( - padding: EdgeInsets.symmetric(horizontal: 16.0), + padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( children: [ if (_currentLocation != null) @@ -71,9 +71,9 @@ class _EventItemState extends State { alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Starts'), + child: const Text('Starts'), ), Text( widget._calendarEvent == null @@ -85,7 +85,7 @@ class _EventItemState extends State { ], ), ), - Padding( + const Padding( padding: EdgeInsets.symmetric(vertical: 5.0), ), if (_currentLocation != null) @@ -93,9 +93,9 @@ class _EventItemState extends State { alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Ends'), + child: const Text('Ends'), ), Text( widget._calendarEvent?.end == null @@ -107,16 +107,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('All day?'), + child: const Text('All day?'), ), Text(widget._calendarEvent?.allDay != null && widget._calendarEvent?.allDay == true @@ -125,16 +125,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Location'), + child: const Text('Location'), ), Expanded( child: Text( @@ -145,16 +145,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('URL'), + child: const Text('URL'), ), Expanded( child: Text( @@ -165,16 +165,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Attendees'), + child: const Text('Attendees'), ), Expanded( child: Text( @@ -189,16 +189,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Availability'), + child: const Text('Availability'), ), Expanded( child: Text( @@ -210,16 +210,16 @@ class _EventItemState extends State { ], ), ), - SizedBox( + const SizedBox( height: 10.0, ), Align( alignment: Alignment.topLeft, child: Row( children: [ - Container( + SizedBox( width: _eventFieldNameWidth, - child: Text('Status'), + child: const Text('Status'), ), Expanded( child: Text( @@ -242,7 +242,7 @@ class _EventItemState extends State { widget._onTapped(widget._calendarEvent as Event); } }, - icon: Icon(Icons.edit), + icon: const Icon(Icons.edit), ), IconButton( onPressed: () async { @@ -252,14 +252,14 @@ class _EventItemState extends State { builder: (BuildContext context) { if (widget._calendarEvent?.recurrenceRule == null) { return AlertDialog( - title: Text( + title: const Text( 'Are you sure you want to delete this event?'), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, - child: Text('Cancel'), + child: const Text('Cancel'), ), TextButton( onPressed: () async { @@ -274,13 +274,13 @@ class _EventItemState extends State { deleteResult.isSuccess && deleteResult.data != null); }, - child: Text('Delete'), + child: const Text('Delete'), ), ], ); } else { if (widget._calendarEvent == null) { - return SizedBox(); + return const SizedBox(); } return RecurringEventDialog( widget._deviceCalendarPlugin, @@ -291,7 +291,7 @@ class _EventItemState extends State { }, ); }, - icon: Icon(Icons.delete), + icon: const Icon(Icons.delete), ), ] else ...[ IconButton( @@ -300,7 +300,7 @@ class _EventItemState extends State { widget._onTapped(widget._calendarEvent!); } }, - icon: Icon(Icons.remove_red_eye), + icon: const Icon(Icons.remove_red_eye), ), ] ], diff --git a/example/lib/presentation/pages/calendar_add.dart b/example/lib/presentation/pages/calendar_add.dart index b54dacfb..7d6d8820 100644 --- a/example/lib/presentation/pages/calendar_add.dart +++ b/example/lib/presentation/pages/calendar_add.dart @@ -4,6 +4,8 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; class CalendarAddPage extends StatefulWidget { + const CalendarAddPage({Key? key}) : super(key: key); + @override _CalendarAddPageState createState() { return _CalendarAddPageState(); @@ -29,13 +31,13 @@ class _CalendarAddPageState extends State { return Scaffold( key: _scaffoldKey, appBar: AppBar( - title: Text('Create Calendar'), + title: const Text('Create Calendar'), ), body: Form( autovalidateMode: _autovalidate, key: _formKey, child: Container( - padding: EdgeInsets.all(10), + padding: const EdgeInsets.all(10), child: Column( children: [ TextFormField( @@ -46,11 +48,11 @@ class _CalendarAddPageState extends State { validator: _validateCalendarName, onSaved: (String? value) => _calendarName = value ?? '', ), - SizedBox(height: 10), + const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Calendar Color'), + const Text('Calendar Color'), DropdownButton( onChanged: (selectedColor) { setState(() => _colorChoice = selectedColor); @@ -101,7 +103,7 @@ class _CalendarAddPageState extends State { } } }, - child: Icon(Icons.check), + child: const Icon(Icons.check), ), ); } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 116cbec0..1faa8dbb 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -260,7 +260,7 @@ class _CalendarEventPageState extends State { ), if (Platform.isAndroid) ListTile( - leading: Text( + leading: const Text( 'Status', style: TextStyle(fontSize: 16), ), @@ -288,7 +288,7 @@ class _CalendarEventPageState extends State { value: _event?.allDay ?? false, onChanged: (value) => setState(() => _event?.allDay = value), - title: Text('All Day'), + title: const Text('All Day'), ), Padding( padding: const EdgeInsets.all(10.0), @@ -373,7 +373,7 @@ class _CalendarEventPageState extends State { padding: const EdgeInsets.all(10.0), child: TextFormField( initialValue: _event?.end?.location.name, - decoration: InputDecoration( + decoration: const InputDecoration( labelText: 'End date time zone', hintText: 'Australia/Sydney'), onSaved: (String? value) => @@ -387,18 +387,16 @@ class _CalendarEventPageState extends State { context, MaterialPageRoute( builder: (context) => - EventAttendeePage())); + const EventAttendeePage())); if (result != null) { - if (_attendees == null) { - _attendees = []; - } + _attendees ??= []; setState(() { _attendees?.add(result); }); } } : null, - leading: Icon(Icons.people), + leading: const Icon(Icons.people), title: Text(_calendar.isReadOnly == false ? 'Add Attendees' : 'Attendees'), @@ -476,7 +474,7 @@ class _CalendarEventPageState extends State { decoration: BoxDecoration( border: Border.all( color: Colors.blueAccent)), - child: Text('current user'))), + child: const Text('current user'))), Visibility( visible: _attendees?[index].isOrganiser ?? false, @@ -487,7 +485,7 @@ class _CalendarEventPageState extends State { decoration: BoxDecoration( border: Border.all( color: Colors.blueAccent)), - child: Text('Organiser'))), + child: const Text('Organiser'))), Container( margin: const EdgeInsets.symmetric( vertical: 10.0), @@ -505,7 +503,7 @@ class _CalendarEventPageState extends State { _attendees?.removeAt(index); }); }, - icon: Icon( + icon: const Icon( Icons.remove_circle, color: Colors.redAccent, ), @@ -748,10 +746,8 @@ class _CalendarEventPageState extends State { _rrule?.frequency) .data != null - ? Text(_recurrenceFrequencyToText( - _rrule?.frequency) - .data! + - ' on the ') + ? Text( + '${_recurrenceFrequencyToText(_rrule?.frequency).data!} on the ') : const Text('')), ), Padding( diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index 7255029f..d37b5bbc 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -61,19 +61,19 @@ class _CalendarEventsPageState extends State { }, ), if (_isLoading) - Center( + const Center( child: CircularProgressIndicator(), ) ], ) - : Center(child: Text('No events found')), + : const Center(child: Text('No events found')), floatingActionButton: _getAddEventButton(context)); } Widget? _getAddEventButton(BuildContext context) { if (_calendar.isReadOnly == false || _calendar.isReadOnly == null) { return FloatingActionButton( - key: Key('addEventButton'), + key: const Key('addEventButton'), onPressed: () async { final refreshEvents = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { @@ -83,7 +83,7 @@ class _CalendarEventsPageState extends State { await _retrieveCalendarEvents(); } }, - child: Icon(Icons.add), + child: const Icon(Icons.add), ); } else { return null; @@ -100,7 +100,7 @@ class _CalendarEventsPageState extends State { if (deleteSucceeded) { await _retrieveCalendarEvents(); } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Oops, we ran into an issue deleting the event'), backgroundColor: Colors.red, duration: Duration(seconds: 5), @@ -131,9 +131,9 @@ class _CalendarEventsPageState extends State { } Future _retrieveCalendarEvents() async { - final startDate = DateTime.now().add(Duration(days: -30)); + final startDate = DateTime.now().add(const Duration(days: -30)); // final endDate = DateTime.now().add(Duration(days: 365 * 2)); - final endDate = DateTime.now().add(Duration(days: 365 * 10)); + final endDate = DateTime.now().add(const Duration(days: 365 * 10)); var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( _calendar.id, RetrieveEventsParams(startDate: startDate, endDate: endDate)); @@ -145,7 +145,7 @@ class _CalendarEventsPageState extends State { Widget _getDeleteButton() { return IconButton( - icon: Icon(Icons.delete), + icon: const Icon(Icons.delete), onPressed: () async { await _showDeleteDialog(); }); @@ -156,10 +156,10 @@ class _CalendarEventsPageState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: Text('Warning'), + title: const Text('Warning'), content: SingleChildScrollView( child: ListBody( - children: [ + children: const [ Text('This will delete this calendar'), Text('Are you sure?'), ], @@ -175,13 +175,13 @@ class _CalendarEventsPageState extends State { Navigator.of(context).pop(); Navigator.of(context).pop(); }, - child: Text('Delete!'), + child: const Text('Delete!'), ), TextButton( onPressed: () { Navigator.of(context).pop(); }, - child: Text('Cancel'), + child: const Text('Cancel'), ), ], ); diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 1113480d..bbd62c32 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import 'calendar_events.dart'; class CalendarsPage extends StatefulWidget { - CalendarsPage({Key? key}) : super(key: key); + const CalendarsPage({Key? key}) : super(key: key); @override _CalendarsPageState createState() { @@ -37,7 +37,7 @@ class _CalendarsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Calendars'), + title: const Text('Calendars'), actions: [_getRefreshButton()], ), body: Column( @@ -62,7 +62,7 @@ class _CalendarsPageState extends State { await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { return CalendarEventsPage(_calendars[index], - key: Key('calendarEventsPage')); + key: const Key('calendarEventsPage')); })); }, child: Padding( @@ -91,14 +91,14 @@ class _CalendarsPageState extends State { shape: BoxShape.circle, color: Color(_calendars[index].color!)), ), - SizedBox(width: 10), + const SizedBox(width: 10), if (_calendars[index].isDefault!) Container( margin: const EdgeInsets.fromLTRB(0, 0, 5.0, 0), padding: const EdgeInsets.all(3.0), decoration: BoxDecoration( border: Border.all(color: Colors.blueAccent)), - child: Text('Default'), + child: const Text('Default'), ), Icon(_calendars[index].isReadOnly == true ? Icons.lock @@ -123,7 +123,7 @@ class _CalendarsPageState extends State { _retrieveCalendars(); } }, - child: Icon(Icons.add), + child: const Icon(Icons.add), ), ); } @@ -153,7 +153,7 @@ class _CalendarsPageState extends State { Widget _getRefreshButton() { return IconButton( - icon: Icon(Icons.refresh), + icon: const Icon(Icons.refresh), onPressed: () async { _retrieveCalendars(); }); diff --git a/example/lib/presentation/pages/event_attendee.dart b/example/lib/presentation/pages/event_attendee.dart index de9644a9..7936812c 100644 --- a/example/lib/presentation/pages/event_attendee.dart +++ b/example/lib/presentation/pages/event_attendee.dart @@ -90,7 +90,7 @@ class _EventAttendeePageState extends State { ), ), ListTile( - leading: Text('Role'), + leading: const Text('Role'), trailing: DropdownButton( onChanged: (value) { setState(() { @@ -118,14 +118,14 @@ class _EventAttendeePageState extends State { context, ModalRoute.withName(AppRoutes.calendars)); //TODO: finish calling and getting attendee details from iOS }, - leading: Icon(Icons.edit), - title: Text('View / edit iOS attendance details'), + leading: const Icon(Icons.edit), + title: const Text('View / edit iOS attendance details'), ), ), Visibility( visible: Platform.isAndroid, child: ListTile( - leading: Text('Android attendee status'), + leading: const Text('Android attendee status'), trailing: DropdownButton( onChanged: (value) { setState(() { diff --git a/example/lib/presentation/pages/event_reminders.dart b/example/lib/presentation/pages/event_reminders.dart index 11619285..4b0a11f3 100644 --- a/example/lib/presentation/pages/event_reminders.dart +++ b/example/lib/presentation/pages/event_reminders.dart @@ -3,7 +3,7 @@ import 'package:device_calendar/device_calendar.dart'; class EventRemindersPage extends StatefulWidget { final List _reminders; - EventRemindersPage(this._reminders, {Key? key}) : super(key: key); + const EventRemindersPage(this._reminders, {Key? key}) : super(key: key); @override _EventRemindersPageState createState() => @@ -29,7 +29,7 @@ class _EventRemindersPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('Reminders'), + title: const Text('Reminders'), ), body: Column( children: [ @@ -64,7 +64,7 @@ class _EventRemindersPageState extends State { }); } }, - child: Text('Add'), + child: const Text('Add'), ), ], ), @@ -83,7 +83,7 @@ class _EventRemindersPageState extends State { (a) => a.minutes == _reminders[index].minutes); }); }, - child: Text('Delete'), + child: const Text('Delete'), ), ); }, @@ -93,7 +93,7 @@ class _EventRemindersPageState extends State { onPressed: () { Navigator.pop(context, _reminders); }, - child: Text('Done'), + child: const Text('Done'), ) ], ), diff --git a/example/lib/presentation/recurring_event_dialog.dart b/example/lib/presentation/recurring_event_dialog.dart index b3c63b3d..c8b8ff35 100644 --- a/example/lib/presentation/recurring_event_dialog.dart +++ b/example/lib/presentation/recurring_event_dialog.dart @@ -8,7 +8,7 @@ class RecurringEventDialog extends StatefulWidget { final VoidCallback _onLoadingStarted; final Function(bool) _onDeleteFinished; - RecurringEventDialog(this._deviceCalendarPlugin, this._calendarEvent, + const RecurringEventDialog(this._deviceCalendarPlugin, this._calendarEvent, this._onLoadingStarted, this._onDeleteFinished, {Key? key}) : super(key: key); @@ -38,7 +38,7 @@ class _RecurringEventDialogState extends State { @override Widget build(BuildContext context) { return SimpleDialog( - title: Text('Are you sure you want to delete this event?'), + title: const Text('Are you sure you want to delete this event?'), children: [ SimpleDialogOption( onPressed: () async { @@ -56,7 +56,7 @@ class _RecurringEventDialogState extends State { deleteResult.isSuccess && deleteResult.data != null); } }, - child: Text('This instance only'), + child: const Text('This instance only'), ), SimpleDialogOption( onPressed: () async { @@ -74,7 +74,7 @@ class _RecurringEventDialogState extends State { deleteResult.isSuccess && deleteResult.data != null); } }, - child: Text('This and following instances'), + child: const Text('This and following instances'), ), SimpleDialogOption( onPressed: () async { @@ -87,13 +87,13 @@ class _RecurringEventDialogState extends State { deleteResult.isSuccess && deleteResult.data != null); } }, - child: Text('All instances'), + child: const Text('All instances'), ), SimpleDialogOption( onPressed: () { Navigator.of(context).pop(false); }, - child: Text('Cancel'), + child: const Text('Cancel'), ) ], ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5dad60cd..36558716 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,7 @@ name: device_calendar_example description: Demonstrates how to use the device_calendar plugin. version: 3.2.0 +publish_to: none environment: sdk: ">=2.12.0 <3.0.0" diff --git a/lib/src/common/error_messages.dart b/lib/src/common/error_messages.dart index dc05d425..f2126006 100644 --- a/lib/src/common/error_messages.dart +++ b/lib/src/common/error_messages.dart @@ -21,7 +21,7 @@ class ErrorMessages { static const String unknownDeviceIssue = 'Device calendar plugin ran into an unknown issue'; static const String unknownDeviceExceptionTemplate = - 'Device calendar plugin ran into an issue. Platform specific exception [%s], with message :\"%s\", has been thrown.'; + 'Device calendar plugin ran into an issue. Platform specific exception [%s], with message :"%s", has been thrown.'; static const String unknownDeviceGenericExceptionTemplate = - 'Device calendar plugin ran into an issue, with message \"%s\"'; + 'Device calendar plugin ran into an issue, with message "%s"'; } diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index 709aec2a..4c1d12f1 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -232,7 +232,7 @@ class DeviceCalendarPlugin { event.end = Platform.isAndroid ? TZDateTime.utc(event.end!.year, event.end!.month, event.end!.day, 0, 0, 0) - .add(Duration(days: 1)) + .add(const Duration(days: 1)) : TZDateTime.from(dateEnd, timeZoneDatabase.locations[event.end!.location.name]!); } diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 93529c38..6c2b5697 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -140,7 +140,7 @@ class Event { end = end?.subtract(Duration(milliseconds: endOffset)); // The Event End Date for allDay events is midnight of the next day, so // subtract one day - end = end?.subtract(Duration(days: 1)); + end = end?.subtract(const Duration(days: 1)); } location = json['eventLocation']; availability = parseStringToAvailability(json['availability']); @@ -216,7 +216,7 @@ class Event { }).toList(); } if (legacyJSON) { - throw FormatException( + throw const FormatException( 'legacy JSON detected. Please update your current JSONs as they may not be supported later on.'); } } diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index acc26441..6ef40a38 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -7,8 +7,7 @@ import 'package:timezone/timezone.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final channel = - const MethodChannel('plugins.builttoroam.com/device_calendar'); + const channel = MethodChannel('plugins.builttoroam.com/device_calendar'); var deviceCalendarPlugin = DeviceCalendarPlugin(); final log = []; @@ -47,9 +46,9 @@ void main() { }); test('RetrieveCalendars_Returns_Successfully', () async { - final fakeCalendarName = 'fakeCalendarName'; + const fakeCalendarName = 'fakeCalendarName'; channel.setMockMethodCallHandler((MethodCall methodCall) async { - return '[{\"id\":\"1\",\"isReadOnly\":false,\"name\":\"$fakeCalendarName\"}]'; + return '[{"id":"1","isReadOnly":false,"name":"$fakeCalendarName"}]'; }); final result = await deviceCalendarPlugin.retrieveCalendars(); @@ -61,8 +60,8 @@ void main() { }); test('RetrieveEvents_CalendarId_IsRequired', () async { - final String? calendarId = null; - final params = RetrieveEventsParams(); + const String? calendarId = null; + const params = RetrieveEventsParams(); final result = await deviceCalendarPlugin.retrieveEvents(calendarId, params); @@ -72,8 +71,8 @@ void main() { }); test('DeleteEvent_CalendarId_IsRequired', () async { - final String? calendarId = null; - final eventId = 'fakeEventId'; + const String? calendarId = null; + const eventId = 'fakeEventId'; final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); expect(result.isSuccess, false); @@ -82,8 +81,8 @@ void main() { }); test('DeleteEvent_EventId_IsRequired', () async { - final calendarId = 'fakeCalendarId'; - final String? eventId = null; + const calendarId = 'fakeCalendarId'; + const String? eventId = null; final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); expect(result.isSuccess, false); @@ -92,8 +91,8 @@ void main() { }); test('DeleteEvent_PassesArguments_Correctly', () async { - final calendarId = 'fakeCalendarId'; - final eventId = 'fakeEventId'; + const calendarId = 'fakeCalendarId'; + const eventId = 'fakeEventId'; await deviceCalendarPlugin.deleteEvent(calendarId, eventId); expect(log, [ @@ -105,7 +104,7 @@ void main() { }); test('CreateEvent_Arguments_Invalid', () async { - final String? fakeCalendarId = null; + const String? fakeCalendarId = null; final event = Event(fakeCalendarId); final result = await deviceCalendarPlugin.createOrUpdateEvent(event); @@ -115,16 +114,16 @@ void main() { }); test('CreateEvent_Returns_Successfully', () async { - final fakeNewEventId = 'fakeNewEventId'; + const fakeNewEventId = 'fakeNewEventId'; channel.setMockMethodCallHandler((MethodCall methodCall) async { return fakeNewEventId; }); - final fakeCalendarId = 'fakeCalendarId'; + const fakeCalendarId = 'fakeCalendarId'; final event = Event(fakeCalendarId); event.title = 'fakeEventTitle'; event.start = TZDateTime.now(local); - event.end = event.start!.add(Duration(hours: 1)); + event.end = event.start!.add(const Duration(hours: 1)); final result = await deviceCalendarPlugin.createOrUpdateEvent(event); expect(result?.isSuccess, true); @@ -134,7 +133,7 @@ void main() { }); test('UpdateEvent_Returns_Successfully', () async { - final fakeNewEventId = 'fakeNewEventId'; + const fakeNewEventId = 'fakeNewEventId'; channel.setMockMethodCallHandler((MethodCall methodCall) async { final arguments = methodCall.arguments as Map; if (!arguments.containsKey('eventId') || arguments['eventId'] == null) { @@ -144,12 +143,12 @@ void main() { return fakeNewEventId; }); - final fakeCalendarId = 'fakeCalendarId'; + const fakeCalendarId = 'fakeCalendarId'; final event = Event(fakeCalendarId); event.eventId = 'fakeEventId'; event.title = 'fakeEventTitle'; event.start = TZDateTime.now(local); - event.end = event.start!.add(Duration(hours: 1)); + event.end = event.start!.add(const Duration(hours: 1)); final result = await deviceCalendarPlugin.createOrUpdateEvent(event); expect(result?.isSuccess, true); From a8a217e7a5de2e5e0a238a5c9e27955d3430ec1e Mon Sep 17 00:00:00 2001 From: thomassth Date: Thu, 6 Oct 2022 21:21:22 -0400 Subject: [PATCH 06/46] more linting fixes --- example/lib/main.dart | 2 +- example/lib/presentation/event_item.dart | 2 +- example/lib/presentation/pages/calendar_events.dart | 2 +- example/lib/presentation/pages/calendars.dart | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ffb03586..3b5d61ee 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,7 +9,7 @@ class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override - _MyAppState createState() => _MyAppState(); + State createState() => _MyAppState(); } class _MyAppState extends State { diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index a18613a6..986e15f9 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -28,7 +28,7 @@ class EventItem extends StatefulWidget { : super(key: key); @override - _EventItemState createState() { + State createState() { return _EventItemState(); } } diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index d37b5bbc..d110ffdb 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -10,7 +10,7 @@ import 'calendar_event.dart'; class CalendarEventsPage extends StatefulWidget { final Calendar _calendar; - CalendarEventsPage(this._calendar, {Key? key}) : super(key: key); + const CalendarEventsPage(this._calendar, {Key? key}) : super(key: key); @override _CalendarEventsPageState createState() { diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index bbd62c32..71c47ea5 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -116,7 +116,7 @@ class _CalendarsPageState extends State { onPressed: () async { final createCalendar = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return CalendarAddPage(); + return const CalendarAddPage(); })); if (createCalendar == true) { From 02859dc563ab03bc7c85acdbea9668fead8aedc2 Mon Sep 17 00:00:00 2001 From: Thomas Kam Date: Thu, 6 Oct 2022 23:58:59 -0400 Subject: [PATCH 07/46] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 36 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..ffebd121 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +In a few sentence, briefly describe of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +Optional, add screenshots if it helps explain your problem. + +**Device(s) tested** +This can be very important as not all device vendors do calendar in the same way. + - Device: [e.g. Pixel 6] + - OS: [e.g. Android 12.0] + - Plugin version [e.g. 4.2.0 Release] + +**Flutter doctor** +Run a `flutter doctor` so we can rule out env issues + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..11fc491e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From aa31c46cd694cf468dbb983bb9bbc9d75315cd50 Mon Sep 17 00:00:00 2001 From: thomassth Date: Wed, 12 Oct 2022 01:31:00 -0400 Subject: [PATCH 08/46] packages cleanup --- example/lib/presentation/event_item.dart | 1 - example/lib/presentation/pages/calendar_event.dart | 1 - example/pubspec.yaml | 1 - lib/device_calendar.dart | 1 + lib/src/models/event.dart | 2 -- pubspec.yaml | 3 --- 6 files changed, 1 insertion(+), 8 deletions(-) diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index 986e15f9..f91bb7de 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -4,7 +4,6 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:intl/intl.dart'; -import 'package:timezone/timezone.dart'; import 'recurring_event_dialog.dart'; diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 1faa8dbb..750d7236 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -6,7 +6,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:intl/intl.dart'; -import 'package:timezone/timezone.dart'; import '../date_time_picker.dart'; import '../recurring_event_dialog.dart'; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 36558716..b32d2fb0 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,7 +12,6 @@ dependencies: sdk: flutter intl: ^0.17.0 uuid: ^3.0.6 - timezone: ^0.9.0 flutter_native_timezone: ^2.0.0 device_calendar: path: ../ diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index b25b1f29..3566d5df 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -14,3 +14,4 @@ export 'src/models/platform_specifics/ios/attendance_status.dart'; export 'src/models/platform_specifics/android/attendee_details.dart'; export 'src/models/platform_specifics/android/attendance_status.dart'; export 'src/device_calendar.dart'; +export 'package:timezone/timezone.dart'; diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 6c2b5697..94ef6217 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:timezone/timezone.dart'; import '../../device_calendar.dart'; import '../common/error_messages.dart'; @@ -83,7 +82,6 @@ class Event { if (json == null) { throw ArgumentError(ErrorMessages.fromJsonMapIsNull); } - String? foundUrl; String? startLocationName; String? endLocationName; diff --git a/pubspec.yaml b/pubspec.yaml index 42aae1ff..140720ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,10 +7,7 @@ dependencies: flutter: sdk: flutter collection: ^1.16.0 - sprintf: ^6.0.2 timezone: ^0.9.0 - flutter_native_timezone: ^2.0.0 - intl: ^0.17.0 rrule: ^0.2.7 dev_dependencies: From 53ac92fad21398daa4a231cbefb51e6f34aa3a19 Mon Sep 17 00:00:00 2001 From: Oleksandra Fedotova Date: Thu, 13 Oct 2022 12:11:52 +0300 Subject: [PATCH 09/46] Fix create/update events in IOS Fixed fullDay with DateRange events creation (not only one fullday events possible to create) Timezone should be set only if event is not full day --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index b39ee5da..fdad5ecc 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -815,13 +815,13 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele ekEvent!.notes = description ekEvent!.isAllDay = isAllDay ekEvent!.startDate = startDate - if (isAllDay) { ekEvent!.endDate = startDate } - else { - ekEvent!.endDate = endDate - - let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current + ekEvent!.endDate = endDate + + if (!isAllDay) { + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone - } + } + ekEvent!.calendar = ekCalendar! ekEvent!.location = location From bd290e8a0cd465099b12cc04f3bfb244706b801b Mon Sep 17 00:00:00 2001 From: Oleksandra Fedotova Date: Mon, 17 Oct 2022 12:50:37 +0300 Subject: [PATCH 10/46] Fixed formatting --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index fdad5ecc..cb102cd2 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -815,12 +815,12 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele ekEvent!.notes = description ekEvent!.isAllDay = isAllDay ekEvent!.startDate = startDate - ekEvent!.endDate = endDate + ekEvent!.endDate = endDate if (!isAllDay) { - let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone - } + } ekEvent!.calendar = ekCalendar! ekEvent!.location = location From d3d39271e8f99dafefa3cf3abd45070a84f9e22d Mon Sep 17 00:00:00 2001 From: Oleksandra Fedotova Date: Mon, 17 Oct 2022 19:33:08 +0300 Subject: [PATCH 11/46] Fixed file formatting with spaces --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index cb102cd2..ed8d6fb7 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -440,13 +440,13 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele private func convertEkEventStatus(ekEventStatus: EKEventStatus?) -> EventStatus? { switch ekEventStatus { case .confirmed: - return EventStatus.CONFIRMED + return EventStatus.CONFIRMED case .tentative: return EventStatus.TENTATIVE - case .canceled: - return EventStatus.CANCELED - case .none?: - return EventStatus.NONE + case .canceled: + return EventStatus.CANCELED + case .none?: + return EventStatus.NONE default: return nil } @@ -817,7 +817,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele ekEvent!.startDate = startDate ekEvent!.endDate = endDate - if (!isAllDay) { + if (!isAllDay) { let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone } From 5ba6b55409bc5c007511642f06ebf4f0509950fc Mon Sep 17 00:00:00 2001 From: "goldensoju@gmailcom" Date: Tue, 1 Nov 2022 18:28:07 +0900 Subject: [PATCH 12/46] iOS: Fix error when adding attendees due to UUID is nil. --- example/ios/Podfile.lock | 6 +++--- example/lib/presentation/pages/calendar_event.dart | 5 ++++- example/lib/presentation/pages/event_attendee.dart | 8 ++++---- ios/Classes/SwiftDeviceCalendarPlugin.swift | 1 + pubspec.yaml | 2 +- test/device_calendar_test.dart | 1 - 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f58f9b39..faea259c 100755 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -25,10 +25,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 - integration_test: 7db6d89f336f671dcbc7563ee27a5b08f6f8aee1 + integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 PODFILE CHECKSUM: d3740c426905916d1f2ada0ddfce28cc99f7b7af -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 750d7236..431b9b03 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -407,7 +407,10 @@ class _CalendarEventPageState extends State { itemBuilder: (context, index) { return Container( color: (_attendees?[index].isOrganiser ?? false) - ? Colors.greenAccent[100] + ? MediaQuery.of(context).platformBrightness == + Brightness.dark + ? Colors.black26 + : Colors.greenAccent[100] : Colors.transparent, child: ListTile( onTap: () async { diff --git a/example/lib/presentation/pages/event_attendee.dart b/example/lib/presentation/pages/event_attendee.dart index 7936812c..b018d8b6 100644 --- a/example/lib/presentation/pages/event_attendee.dart +++ b/example/lib/presentation/pages/event_attendee.dart @@ -64,7 +64,7 @@ class _EventAttendeePageState extends State { child: TextFormField( controller: _nameController, validator: (value) { - if (_attendee!.isCurrentUser == false && + if (_attendee?.isCurrentUser == false && (value == null || value.isEmpty)) { return 'Please enter a name'; } @@ -153,9 +153,9 @@ class _EventAttendeePageState extends State { name: _nameController.text, emailAddress: _emailAddressController.text, role: _role, - isOrganiser: _attendee!.isOrganiser, - isCurrentUser: _attendee!.isCurrentUser, - iosAttendeeDetails: _attendee!.iosAttendeeDetails, + isOrganiser: _attendee?.isOrganiser ?? false, + isCurrentUser: _attendee?.isCurrentUser ?? false, + iosAttendeeDetails: _attendee?.iosAttendeeDetails, androidAttendeeDetails: AndroidAttendeeDetails.fromJson( {'attendanceStatus': _status.index})); diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index b39ee5da..c0c724b2 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -856,6 +856,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let ekAttendeeClass: AnyClass? = NSClassFromString("EKAttendee") if let type = ekAttendeeClass as? NSObject.Type { let participant = type.init() + participant.setValue(UUID().uuidString, forKey: "UUID") participant.setValue(name, forKey: "displayName") participant.setValue(emailAddress, forKey: "emailAddress") participant.setValue(role, forKey: "participantRole") diff --git a/pubspec.yaml b/pubspec.yaml index 140720ba..014d8bcb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ dependencies: sdk: flutter collection: ^1.16.0 timezone: ^0.9.0 - rrule: ^0.2.7 + rrule: ^0.2.10 dev_dependencies: flutter_test: diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index 6ef40a38..ada9d769 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -2,7 +2,6 @@ import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar/src/common/error_codes.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:timezone/timezone.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); From 05dc04d8ab2f4ac7d59908df519fa9bff36b3566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20J=C3=A4rvstrand?= Date: Sat, 31 Dec 2022 13:23:10 +0100 Subject: [PATCH 13/46] Remove faulty requestPermissions implementation in favor of working one --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index f20bd09b..fb6c446a 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -1024,7 +1024,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele self.finishWithUnauthorizedError(result: result) } - private func requestPermissions(completion: @escaping (Bool) -> Void) { + private func requestPermissions(_ completion: @escaping (Bool) -> Void) { if hasEventPermissions() { completion(true) return @@ -1039,16 +1039,6 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let status = EKEventStore.authorizationStatus(for: .event) return status == EKAuthorizationStatus.authorized } - - private func requestPermissions(_ result: @escaping FlutterResult) { - if hasEventPermissions() { - result(true) - } - eventStore.requestAccess(to: .event, completion: { - (accessGranted: Bool, _: Error?) in - result(accessGranted) - }) - } } extension Date { From 0c85ab0d158763ca0f6c93f4a8d88b5c57b87c7f Mon Sep 17 00:00:00 2001 From: Shreyas S Date: Wed, 15 Feb 2023 11:45:23 +0530 Subject: [PATCH 14/46] parseRecurrenceRuleString() when else fix. --- .../kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 744f636e..7141da99 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -787,6 +787,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { DayOfWeek.values().find { dayOfWeek -> dayOfWeek.ordinal == it.weekday.ordinal } }?.toMutableList() } + else -> recurrenceRule.daysOfWeek = null } val rfcRecurrenceRuleString = rfcRecurrenceRule.toString() From e3799b1da6679734c46bdb41218272b3a3acaf3c Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:16:22 +0100 Subject: [PATCH 15/46] Fixed failing test (dart analyze). --- example/lib/presentation/pages/calendar_event.dart | 2 +- lib/src/models/event.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index ee9040e9..cb258367 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -416,7 +416,7 @@ class _CalendarEventPageState extends State { itemCount: _attendees.length, itemBuilder: (context, index) { return Container( - color: (_attendees?[index].isOrganiser ?? false) + color: (_attendees[index].isOrganiser) ? MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.black26 diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index c7478005..70fc9e45 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -250,6 +250,7 @@ class Event { case 'NONE': return EventStatus.None; } + return null; } bool updateStartLocation(String? newStartLocation) { From 47c851cd98ac29aeb914330fa83f86833608dc7b Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Fri, 17 Feb 2023 22:16:40 +0100 Subject: [PATCH 16/46] Bumped version and updated changelog. --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6285b62b..2fa175fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## [4.3.1](https://github.com/builttoroam/device_calendar/releases/tag/4.3.1) + +- Fixed an [issue](https://github.com/builttoroam/device_calendar/issues/470) that prevented the plugin from being used with Kotlin 1.7.10 + ## [4.3.0](https://github.com/builttoroam/device_calendar/releases/tag/4.3.0) - Updated multiple underlying dependencies diff --git a/pubspec.yaml b/pubspec.yaml index ec0732c1..45018475 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: device_calendar description: A cross platform plugin for modifying calendars on the user's device. -version: 4.3.0 +version: 4.3.1 homepage: https://github.com/builttoroam/device_calendar/tree/master dependencies: From 472905f154e93ffb6c48c056fc8ed6db9efced60 Mon Sep 17 00:00:00 2001 From: VladyslavBilomeria Date: Sat, 13 May 2023 14:15:05 +0300 Subject: [PATCH 17/46] Fix retrieving events for more than 4 years range --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 46 ++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index fb6c446a..cfcfeea1 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -322,11 +322,47 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch!.doubleValue / 1000.0) let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) if ekCalendar != nil { - let predicate = self.eventStore.predicateForEvents( - withStart: startDate, - end: endDate, - calendars: [ekCalendar!]) - let ekEvents = self.eventStore.events(matching: predicate) + var ekEvents = [EKEvent]() + let fourYearsInSeconds = 4 * 365 * 24 * 60 * 60 + var currentStartDate = startDate + // Adding 4 years to the start date + var currentEndDate = startDate.addingTimeInterval(TimeInterval(fourYearsInSeconds)) + while currentEndDate <= endDate { + let rangeSize = currentEndDate.timeIntervalSince(currentStartDate) + let roundedRangeSize = Int(rangeSize / Double(fourYearsInSeconds)) * fourYearsInSeconds + + // debugPrint("Start date of current range: \(currentStartDate)") + // debugPrint("End date of current range: \(currentEndDate.addingTimeInterval(-1))") + // debugPrint("Range size: \(roundedRangeSize / (365 * 24 * 60 * 60)) years\n") + + let predicate = self.eventStore.predicateForEvents( + withStart: currentStartDate, + end: currentEndDate.addingTimeInterval(-1), + calendars: [ekCalendar!]) + let batch = self.eventStore.events(matching: predicate) + ekEvents.append(contentsOf: batch) + + // Move the start and end dates forward by the rounded range size + currentStartDate = currentEndDate + currentEndDate = currentStartDate.addingTimeInterval(TimeInterval(roundedRangeSize)) + } + + // If the cycle doesn't end exactly on the end date + if currentStartDate <= endDate { + let finalRangeSize = endDate.timeIntervalSince(currentStartDate) + + // debugPrint("Start date of final range: \(currentStartDate)") + // debugPrint("End date of final range: \(endDate)") + // debugPrint("Range size: \(finalRangeSize / (365 * 24 * 60 * 60)) years\n") + + let predicate = self.eventStore.predicateForEvents( + withStart: currentStartDate, + end: endDate, + calendars: [ekCalendar!]) + let batch = self.eventStore.events(matching: predicate) + ekEvents.append(contentsOf: batch) + } + for ekEvent in ekEvents { let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent) events.append(event) From a41b4401fd6a91135bac5c5330747307e9a188d8 Mon Sep 17 00:00:00 2001 From: VladyslavBilomeria Date: Fri, 19 May 2023 19:45:43 +0300 Subject: [PATCH 18/46] Simplify the range size calculation algorithm --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index cfcfeea1..6908345d 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -324,13 +324,11 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele if ekCalendar != nil { var ekEvents = [EKEvent]() let fourYearsInSeconds = 4 * 365 * 24 * 60 * 60 + let fourYearsTimeInterval = TimeInterval(fourYearsInSeconds) var currentStartDate = startDate // Adding 4 years to the start date - var currentEndDate = startDate.addingTimeInterval(TimeInterval(fourYearsInSeconds)) + var currentEndDate = startDate.addingTimeInterval(fourYearsTimeInterval) while currentEndDate <= endDate { - let rangeSize = currentEndDate.timeIntervalSince(currentStartDate) - let roundedRangeSize = Int(rangeSize / Double(fourYearsInSeconds)) * fourYearsInSeconds - // debugPrint("Start date of current range: \(currentStartDate)") // debugPrint("End date of current range: \(currentEndDate.addingTimeInterval(-1))") // debugPrint("Range size: \(roundedRangeSize / (365 * 24 * 60 * 60)) years\n") @@ -342,9 +340,9 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let batch = self.eventStore.events(matching: predicate) ekEvents.append(contentsOf: batch) - // Move the start and end dates forward by the rounded range size + // Move the start and end dates forward by the [fourYearsTimeInterval] currentStartDate = currentEndDate - currentEndDate = currentStartDate.addingTimeInterval(TimeInterval(roundedRangeSize)) + currentEndDate = currentStartDate.addingTimeInterval(fourYearsTimeInterval) } // If the cycle doesn't end exactly on the end date From 7304e8200f7ab820c68032ecaf24c51998e91739 Mon Sep 17 00:00:00 2001 From: VladyslavBilomeria Date: Fri, 19 May 2023 19:49:15 +0300 Subject: [PATCH 19/46] Remove logs-related code --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 6908345d..4f56266c 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -329,10 +329,6 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele // Adding 4 years to the start date var currentEndDate = startDate.addingTimeInterval(fourYearsTimeInterval) while currentEndDate <= endDate { - // debugPrint("Start date of current range: \(currentStartDate)") - // debugPrint("End date of current range: \(currentEndDate.addingTimeInterval(-1))") - // debugPrint("Range size: \(roundedRangeSize / (365 * 24 * 60 * 60)) years\n") - let predicate = self.eventStore.predicateForEvents( withStart: currentStartDate, end: currentEndDate.addingTimeInterval(-1), @@ -347,12 +343,6 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele // If the cycle doesn't end exactly on the end date if currentStartDate <= endDate { - let finalRangeSize = endDate.timeIntervalSince(currentStartDate) - - // debugPrint("Start date of final range: \(currentStartDate)") - // debugPrint("End date of final range: \(endDate)") - // debugPrint("Range size: \(finalRangeSize / (365 * 24 * 60 * 60)) years\n") - let predicate = self.eventStore.predicateForEvents( withStart: currentStartDate, end: endDate, From 82acd9b5712ddba21d6cfe7bc94ae1930089630a Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Sat, 20 May 2023 15:04:53 +0200 Subject: [PATCH 20/46] Updated example project for iOS. --- example/ios/Flutter/AppFrameworkInfo.plist | 2 +- example/ios/Podfile | 2 +- example/ios/Podfile.lock | 4 ++-- example/ios/Runner.xcodeproj/project.pbxproj | 11 +++++++---- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- example/ios/Runner/Info.plist | 4 ++++ 6 files changed, 16 insertions(+), 9 deletions(-) diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f..9b41e7d8 100755 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 8ab33cfb..997d1cb3 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index faea259c..cb8f159b 100755 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -27,8 +27,8 @@ SPEC CHECKSUMS: device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 - integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 + integration_test: 13825b8a9334a850581300559b8839134b124670 -PODFILE CHECKSUM: d3740c426905916d1f2ada0ddfce28cc99f7b7af +PODFILE CHECKSUM: 10625bdc9b9ef8574174815aabd5b048e6e29bff COCOAPODS: 1.11.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 7d88e2f9..160e1d14 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -161,7 +161,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1240; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -229,10 +229,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -243,6 +245,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -355,7 +358,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -405,7 +408,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0254012e..14d255fd 100755 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + From 1e7ed588e2afc19d2dad069eba8a689688450598 Mon Sep 17 00:00:00 2001 From: Julius Bredemeyer <48645716+IVLIVS-III@users.noreply.github.com> Date: Sat, 20 May 2023 15:24:17 +0200 Subject: [PATCH 21/46] Fixed deprecation issues. --- test/device_calendar_test.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index ada9d769..132aad61 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -12,7 +12,8 @@ void main() { final log = []; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { print('Calling channel method ${methodCall.method}'); log.add(methodCall); @@ -23,7 +24,8 @@ void main() { }); test('HasPermissions_Returns_Successfully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return true; }); @@ -34,7 +36,8 @@ void main() { }); test('RequestPermissions_Returns_Successfully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return true; }); @@ -46,7 +49,8 @@ void main() { test('RetrieveCalendars_Returns_Successfully', () async { const fakeCalendarName = 'fakeCalendarName'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return '[{"id":"1","isReadOnly":false,"name":"$fakeCalendarName"}]'; }); @@ -114,7 +118,8 @@ void main() { test('CreateEvent_Returns_Successfully', () async { const fakeNewEventId = 'fakeNewEventId'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return fakeNewEventId; }); @@ -133,7 +138,8 @@ void main() { test('UpdateEvent_Returns_Successfully', () async { const fakeNewEventId = 'fakeNewEventId'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { final arguments = methodCall.arguments as Map; if (!arguments.containsKey('eventId') || arguments['eventId'] == null) { return null; From 379c99b07f1e0d7eb71a1b61525e65a177924354 Mon Sep 17 00:00:00 2001 From: "sangam.shrestha" Date: Mon, 19 Jun 2023 13:39:27 +0545 Subject: [PATCH 22/46] add proguard rules --- README.md | 4 ++++ android/build.gradle | 1 + android/proguard-rules.pro | 1 + example/android/build.gradle | 4 ++-- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 android/proguard-rules.pro diff --git a/README.md b/README.md index a91c6e5c..893f2f89 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ If you don't need any timezone specific features in your app, you may use `flutt ```dart import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +initializeTimeZones(); + // As an example, our default timezone is UTC. Location _currentLocation = getLocation('Etc/UTC'); @@ -78,6 +80,8 @@ The following will need to be added to the `AndroidManifest.xml` file for your a ``` ### Proguard / R8 exceptions +> NOTE: From v4.3.2 developers no longer need to add proguard rule in their app. + By default, all android apps go through R8 for file shrinking when building a release version. Currently, it interferes with some functions such as `retrieveCalendars()`. diff --git a/android/build.gradle b/android/build.gradle index 66d86743..c74125b7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -33,6 +33,7 @@ android { defaultConfig { minSdkVersion 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard-rules.pro' } lintOptions { disable 'InvalidPackage' diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 00000000..d7668e11 --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.builttoroam.devicecalendar.** { *; } diff --git a/example/android/build.gradle b/example/android/build.gradle index d135914c..d3f65307 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir -} \ No newline at end of file +} From 623f0bf86ab6f167f6a1eb418d42117822c942e5 Mon Sep 17 00:00:00 2001 From: MIYANARI Junki Date: Sun, 17 Sep 2023 05:49:33 +0900 Subject: [PATCH 23/46] Request FullAccess on iOS17+ (#497) * fix: request FullAccess on iOS17 or later * update iOS integration description * fix: iOS build test & Android build test --- .github/workflows/dart.yml | 5 ++++- README.md | 7 +++++++ android/build.gradle | 4 ++-- example/android/app/build.gradle | 2 +- ios/Classes/SwiftDeviceCalendarPlugin.swift | 21 ++++++++++++++++----- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 25401783..e839df99 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -34,8 +34,11 @@ jobs: flutter build apk test-ios: name: iOS build test - runs-on: macos-latest + runs-on: macos-13 steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.0-beta' - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: diff --git a/README.md b/README.md index 893f2f89..2af1e99d 100644 --- a/README.md +++ b/README.md @@ -111,4 +111,11 @@ For iOS 10+ support, you'll need to modify the `Info.plist` to add the following Access contacts for event attendee editing. ``` +For iOS 17+ support, add the following key/value pair as well. + +```xml +NSCalendarsFullAccessUsageDescription +Access most functions for calendar viewing and editing. +``` + Note that on iOS, this is a Swift plugin. There is a known issue being tracked [here](https://github.com/flutter/flutter/issues/16049) by the Flutter team, where adding a plugin developed in Swift to an Objective-C project causes problems. If you run into such issues, please look at the suggested workarounds there. diff --git a/android/build.gradle b/android/build.gradle index c74125b7..139fb8e5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,13 +25,13 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 16 + minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'proguard-rules.pro' } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index dd924715..776dc817 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -30,7 +30,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.builttoroam.devicecalendarexample" - minSdkVersion 16 + minSdkVersion 19 targetSdkVersion 31 versionCode 1 versionName "1.0" diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 35d242db..f37d1a5a 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -1056,15 +1056,26 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele completion(true) return } - eventStore.requestAccess(to: .event, completion: { - (accessGranted: Bool, _: Error?) in - completion(accessGranted) - }) + if #available(iOS 17, *) { + eventStore.requestFullAccessToEvents { + (accessGranted: Bool, _: Error?) in + completion(accessGranted) + } + } else { + eventStore.requestAccess(to: .event, completion: { + (accessGranted: Bool, _: Error?) in + completion(accessGranted) + }) + } } private func hasEventPermissions() -> Bool { let status = EKEventStore.authorizationStatus(for: .event) - return status == EKAuthorizationStatus.authorized + if #available(iOS 17, *) { + return status == EKAuthorizationStatus.fullAccess + } else { + return status == EKAuthorizationStatus.authorized + } } } From 6d65268132f003f0c79a23e187210cb5d6e3e379 Mon Sep 17 00:00:00 2001 From: ashwani Date: Tue, 10 Oct 2023 17:25:51 +0100 Subject: [PATCH 24/46] included namespace --- android/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/android/build.gradle b/android/build.gradle index 139fb8e5..1f5ff500 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -46,6 +46,7 @@ android { kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + namespace 'com.builttoroam.devicecalendar' } dependencies { From bddadd0cf8c8d6ca4b849a062f5b95c7f4599d55 Mon Sep 17 00:00:00 2001 From: Thomas Kam Date: Mon, 6 Nov 2023 23:21:52 -0500 Subject: [PATCH 25/46] pipeline update --- .github/workflows/dart.yml | 2 -- .github/workflows/prerelease.yml | 11 ++++------- .github/workflows/release.yml | 11 ++++------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index e839df99..744c0ba4 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -37,8 +37,6 @@ jobs: runs-on: macos-13 steps: - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: '15.0-beta' - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index b836df6a..a08733c0 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -17,6 +17,8 @@ jobs: name: Development Release # The type of runner that the job will run on runs-on: ubuntu-latest + permissions: + id-token: write # Required for authentication using OIDC # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -36,15 +38,10 @@ jobs: run: | sed -i "0,/\#\# \[.*/s//## [${{steps.changelog_reader.outputs.version}}-$GITHUB_RUN_ID]/" CHANGELOG.md cat CHANGELOG.md - - name: Setup credentials - run: | - cat < $PUB_CACHE/credentials.json - ${{ secrets.CREDENTIALS }} - EOF - name: Publish package - run: flutter pub publish --force + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 - name: Add entry to Github release uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.changelog_reader.outputs.version }}+${{ github.run_id }} - prerelease: true \ No newline at end of file + prerelease: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35967b42..9d4a3728 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,8 @@ jobs: release: # The type of runner that the job will run on runs-on: ubuntu-latest + permissions: + id-token: write # Required for authentication using OIDC # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -25,13 +27,8 @@ jobs: channel: "stable" - run: dart --version - run: flutter --version - - name: Setup credentials - run: | - cat < $PUB_CACHE/credentials.json - ${{ secrets.CREDENTIALS }} - EOF - name: Publish package - run: flutter pub publish --force + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 - name: Get Changelog Entry id: changelog_reader uses: mindsers/changelog-reader-action@v2.0.0 @@ -39,4 +36,4 @@ jobs: uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.changelog_reader.outputs.version }} - body: ${{ steps.changelog_reader.outputs.changes }} \ No newline at end of file + body: ${{ steps.changelog_reader.outputs.changes }} From d650f91256c317648b8ae8f5b0b66edab40a4e28 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Sun, 14 Jul 2024 20:57:07 +0200 Subject: [PATCH 26/46] building fixes --- example/android/app/build.gradle | 4 ++-- example/android/build.gradle | 2 +- example/lib/presentation/date_time_picker.dart | 2 +- example/lib/presentation/pages/calendars.dart | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 776dc817..7f0cf3be 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 32 + compileSdkVersion 34 ndkVersion '22.1.7171670' sourceSets { @@ -30,7 +30,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.builttoroam.devicecalendarexample" - minSdkVersion 19 + minSdkVersion flutter.minSdkVersion targetSdkVersion 31 versionCode 1 versionName "1.0" diff --git a/example/android/build.gradle b/example/android/build.gradle index d3f65307..8bbe685b 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.8.22' repositories { google() mavenCentral() diff --git a/example/lib/presentation/date_time_picker.dart b/example/lib/presentation/date_time_picker.dart index dc11e8d9..449b82e8 100644 --- a/example/lib/presentation/date_time_picker.dart +++ b/example/lib/presentation/date_time_picker.dart @@ -45,7 +45,7 @@ class DateTimePicker extends StatelessWidget { @override Widget build(BuildContext context) { - final valueStyle = Theme.of(context).textTheme.headline6; + final valueStyle = Theme.of(context).textTheme.titleLarge; return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 71c47ea5..389d9b02 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -46,7 +46,7 @@ class _CalendarsPageState extends State { padding: const EdgeInsets.all(10.0), child: Text( 'WARNING: some aspects of saving events are hardcoded in this example app. As such we recommend you do not modify existing events as this may result in loss of information', - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.titleLarge, ), ), Expanded( @@ -77,7 +77,7 @@ class _CalendarsPageState extends State { Text( "${_calendars[index].id}: ${_calendars[index].name!}", style: - Theme.of(context).textTheme.subtitle1, + Theme.of(context).textTheme.titleSmall, ), Text( "Account: ${_calendars[index].accountName!}"), From 634fd1b1673f36258bd07df903a9f551665caa83 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Sun, 14 Jul 2024 20:57:44 +0200 Subject: [PATCH 27/46] added event color for android --- android/build.gradle | 2 +- .../devicecalendar/CalendarDelegate.kt | 53 +++++++++++ .../devicecalendar/DeviceCalendarPlugin.kt | 9 ++ .../devicecalendar/common/Constants.kt | 4 +- .../devicecalendar/models/Event.kt | 2 + .../presentation/pages/calendar_event.dart | 95 ++++++++++++++----- .../presentation/pages/calendar_events.dart | 7 ++ lib/device_calendar.dart | 1 + lib/src/common/channel_constants.dart | 2 + lib/src/device_calendar.dart | 16 ++++ lib/src/models/event.dart | 20 +++- lib/src/models/event_color.dart | 9 ++ 12 files changed, 194 insertions(+), 26 deletions(-) create mode 100644 lib/src/models/event_color.dart diff --git a/android/build.gradle b/android/build.gradle index 1f5ff500..34fcf1ae 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 1cd3f98a..888c2bff 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -39,6 +39,8 @@ import com.builttoroam.devicecalendar.common.ErrorCodes.Companion as EC import com.builttoroam.devicecalendar.common.ErrorMessages.Companion as EM import org.dmfs.rfc5545.recur.Freq as RruleFreq import org.dmfs.rfc5545.recur.RecurrenceRule as Rrule +import android.provider.CalendarContract.Colors +import androidx.collection.SparseArrayCompat private const val RETRIEVE_CALENDARS_REQUEST_CODE = 0 private const val RETRIEVE_EVENTS_REQUEST_CODE = RETRIEVE_CALENDARS_REQUEST_CODE + 1 @@ -625,6 +627,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : values.put(Events.DTEND, end) values.put(Events.EVENT_END_TIMEZONE, endTimeZone) values.put(Events.DURATION, duration) + values.put(Events.EVENT_COLOR_KEY, event.eventColorKey) return values } @@ -938,6 +941,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : val endTimeZone = cursor.getString(Cst.EVENT_PROJECTION_END_TIMEZONE_INDEX) val availability = parseAvailability(cursor.getInt(Cst.EVENT_PROJECTION_AVAILABILITY_INDEX)) val eventStatus = parseEventStatus(cursor.getInt(Cst.EVENT_PROJECTION_STATUS_INDEX)) + val eventColor = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) val event = Event() event.eventTitle = title ?: "New Event" event.eventId = eventId.toString() @@ -953,6 +957,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : event.eventEndTimeZone = endTimeZone event.availability = availability event.eventStatus = eventStatus + event.eventColor = if (eventColor == 0) null else eventColor return event } @@ -1125,6 +1130,54 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : return reminders } + /** + * load available event colors for the given account name + * unable to find official documentation, so logic is based on https://android.googlesource.com/platform/packages/apps/Calendar.git/+/refs/heads/pie-release/src/com/android/calendar/EventInfoFragment.java + **/ + fun retrieveEventColors(accountName: String): List> { + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = Colors.CONTENT_URI + val colors = mutableListOf() + val displayColorKeyMap = SparseArrayCompat() + + val projection = arrayOf( + Colors.COLOR, + Colors.COLOR_KEY, + ) + + // load only event colors for the given account name + val selection = "${Colors.COLOR_TYPE} = ? AND ${Colors.ACCOUNT_NAME} = ?" + val selectionArgs = arrayOf(Colors.TYPE_EVENT.toString(), accountName) + + val cursor: Cursor? = contentResolver?.query(uri, projection, selection, selectionArgs, null) + cursor?.use { + while (it.moveToNext()) { + val color = it.getInt(it.getColumnIndexOrThrow(Colors.COLOR)) + val colorKey = it.getInt(it.getColumnIndexOrThrow(Colors.COLOR_KEY)) + displayColorKeyMap.put(color, colorKey); + colors.add(color) + } + cursor.close(); + // sort colors by colorValue, since they are loaded unordered + colors.sortWith(HsvColorComparator()) + } + return colors.map { Pair(it, displayColorKeyMap[it]!! ) }.toList() + } + + /** + * Compares colors based on their hue values in the HSV color space. + * https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-compose-integration-release/android-34/com/android/colorpicker/HsvColorComparator.java + */ + private class HsvColorComparator : Comparator { + override fun compare(color1: Int, color2: Int): Int { + val hsv1 = FloatArray(3) + val hsv2 = FloatArray(3) + Color.colorToHSV(color1, hsv1) + Color.colorToHSV(color2, hsv2) + return hsv1[0].compareTo(hsv2[0]) + } + } + @Synchronized private fun generateUniqueRequestCodeAndCacheParameters(parameters: CalendarMethodsParametersCacheModel): Int { // TODO we can ran out of Int's at some point so this probably should re-use some of the freed ones diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index c1f14533..2af342bd 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -25,10 +25,12 @@ private const val DELETE_EVENT_INSTANCE_METHOD = "deleteEventInstance" private const val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent" private const val CREATE_CALENDAR_METHOD = "createCalendar" private const val DELETE_CALENDAR_METHOD = "deleteCalendar" +private const val RETRIEVE_EVENT_COLORS_METHOD = "retrieveEventColors" // Method arguments private const val CALENDAR_ID_ARGUMENT = "calendarId" private const val CALENDAR_NAME_ARGUMENT = "calendarName" +private const val CALENDAR_ACCOUNT_NAME_ARGUMENT = "accountName" private const val START_DATE_ARGUMENT = "startDate" private const val END_DATE_ARGUMENT = "endDate" private const val EVENT_IDS_ARGUMENT = "eventIds" @@ -66,6 +68,7 @@ private const val LOCAL_ACCOUNT_NAME_ARGUMENT = "localAccountName" private const val EVENT_AVAILABILITY_ARGUMENT = "availability" private const val ATTENDANCE_STATUS_ARGUMENT = "attendanceStatus" private const val EVENT_STATUS_ARGUMENT = "eventStatus" +private const val EVENT_COLOR_KEY_ARGUMENT = "eventColorKey" class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { @@ -171,6 +174,11 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { val calendarId = call.argument(CALENDAR_ID_ARGUMENT) _calendarDelegate.deleteCalendar(calendarId!!, result) } + RETRIEVE_EVENT_COLORS_METHOD -> { + val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) + val colors = _calendarDelegate.retrieveEventColors(accountName!!) + result.success(colors.map { listOf(it.first, it.second) }) + } else -> { result.notImplemented() } @@ -192,6 +200,7 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { event.eventURL = call.argument(EVENT_URL_ARGUMENT) event.availability = parseAvailability(call.argument(EVENT_AVAILABILITY_ARGUMENT)) event.eventStatus = parseEventStatus(call.argument(EVENT_STATUS_ARGUMENT)) + event.eventColorKey = call.argument(EVENT_COLOR_KEY_ARGUMENT) if (call.hasArgument(RECURRENCE_RULE_ARGUMENT) && call.argument>( RECURRENCE_RULE_ARGUMENT diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt index 9d136ed5..fc49f227 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -50,6 +50,7 @@ class Constants { const val EVENT_PROJECTION_END_TIMEZONE_INDEX: Int = 12 const val EVENT_PROJECTION_AVAILABILITY_INDEX: Int = 13 const val EVENT_PROJECTION_STATUS_INDEX: Int = 14 + const val EVENT_PROJECTION_EVENT_COLOR_INDEX: Int = 15 val EVENT_PROJECTION: Array = arrayOf( CalendarContract.Instances.EVENT_ID, @@ -66,7 +67,8 @@ class Constants { CalendarContract.Events.EVENT_TIMEZONE, CalendarContract.Events.EVENT_END_TIMEZONE, CalendarContract.Events.AVAILABILITY, - CalendarContract.Events.STATUS + CalendarContract.Events.STATUS, + CalendarContract.Events.EVENT_COLOR ) const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt index 456e549f..dc988fbb 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt @@ -18,4 +18,6 @@ class Event { var reminders: MutableList = mutableListOf() var availability: Availability? = null var eventStatus: EventStatus? = null + var eventColor: Int? = null + var eventColorKey: Int? = null } \ No newline at end of file diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 72c4cb5b..47bd5a5c 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -18,14 +18,15 @@ class CalendarEventPage extends StatefulWidget { final Calendar _calendar; final Event? _event; final RecurringEventDialog? _recurringEventDialog; + final List? _eventColors; const CalendarEventPage(this._calendar, - [this._event, this._recurringEventDialog, Key? key]) + [this._event, this._recurringEventDialog, this._eventColors, Key? key]) : super(key: key); @override _CalendarEventPageState createState() { - return _CalendarEventPageState(_calendar, _event, _recurringEventDialog); + return _CalendarEventPageState(_calendar, _event, _recurringEventDialog, _eventColors); } } @@ -61,10 +62,11 @@ class _CalendarEventPageState extends State { EventStatus? _eventStatus; List? _attendees; List? _reminders; + List? _eventColors; String _timezone = 'Etc/UTC'; _CalendarEventPageState( - this._calendar, this._event, this._recurringEventDialog) { + this._calendar, this._event, this._recurringEventDialog, this._eventColors) { getCurentLocation(); } @@ -283,6 +285,30 @@ class _CalendarEventPageState extends State { }).toList(), ), ), + if (_eventColors?.isNotEmpty ?? false) + ListTile( + leading: const Text( + 'EventColor', + style: TextStyle(fontSize: 16), + ), + trailing: widget._event?.color == null ? const Text("not set") : Container( + width: 30, + height: 30, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(widget._event?.color ?? 0), + )), + onTap: () async { + final colors = _eventColors; + if (colors != null) { + final newColor = await selectColorDialog(colors); + if (newColor != null) { + setState(() { + _event?.updateEventColor(newColor); + }); + }} + }, + ), SwitchListTile( value: _event?.allDay ?? false, onChanged: (value) => @@ -674,11 +700,11 @@ class _CalendarEventPageState extends State { setState(() { if (value) { _rrule = _rrule?.copyWith( - byMonthDays: {1}, byWeekDays: {}); + byMonthDays: [1], byWeekDays: []); } else { _rrule = _rrule?.copyWith( - byMonthDays: {}, - byWeekDays: {ByWeekDayEntry(1, 1)}); + byMonthDays: [], + byWeekDays: [ByWeekDayEntry(1, 1)]); } }); }, @@ -694,7 +720,7 @@ class _CalendarEventPageState extends State { if (value != null) { setState(() { _rrule = _rrule - ?.copyWith(byMonths: {value.index + 1}); + ?.copyWith(byMonths: [value.index + 1]); _getValidDaysOfMonth(_rrule?.frequency); }); } @@ -722,7 +748,7 @@ class _CalendarEventPageState extends State { if (value != null) { setState(() { _rrule = - _rrule?.copyWith(byMonthDays: {value}); + _rrule?.copyWith(byMonthDays: [value]); }); } }, @@ -766,10 +792,10 @@ class _CalendarEventPageState extends State { _rrule?.byWeekDays.first.day ?? 1; setState(() { _rrule = _rrule?.copyWith( - byWeekDays: { + byWeekDays: [ ByWeekDayEntry( weekDay, value.index + 1) - }); + ]); }); } }, @@ -795,10 +821,10 @@ class _CalendarEventPageState extends State { 1; setState(() { _rrule = _rrule?.copyWith( - byWeekDays: { + byWeekDays: [ ByWeekDayEntry( value.index + 1, weekNo) - }); + ]); }); } }, @@ -825,7 +851,7 @@ class _CalendarEventPageState extends State { if (value != null) { setState(() { _rrule = _rrule?.copyWith( - byMonths: {value.index + 1}); + byMonths: [value.index + 1]); }); } }, @@ -1068,22 +1094,22 @@ class _CalendarEventPageState extends State { void _updateDaysOfWeek() { switch (_dayOfWeekGroup) { case DayOfWeekGroup.Weekday: - _rrule = _rrule?.copyWith(byWeekDays: { + _rrule = _rrule?.copyWith(byWeekDays: [ ByWeekDayEntry(1), ByWeekDayEntry(2), ByWeekDayEntry(3), ByWeekDayEntry(4), ByWeekDayEntry(5), - }); + ]); break; case DayOfWeekGroup.Weekend: - _rrule = _rrule?.copyWith(byWeekDays: { + _rrule = _rrule?.copyWith(byWeekDays: [ ByWeekDayEntry(6), ByWeekDayEntry(7), - }); + ]); break; case DayOfWeekGroup.AllDays: - _rrule = _rrule?.copyWith(byWeekDays: { + _rrule = _rrule?.copyWith(byWeekDays: [ ByWeekDayEntry(1), ByWeekDayEntry(2), ByWeekDayEntry(3), @@ -1091,7 +1117,7 @@ class _CalendarEventPageState extends State { ByWeekDayEntry(5), ByWeekDayEntry(6), ByWeekDayEntry(7), - }); + ]); break; case DayOfWeekGroup.None: default: @@ -1138,7 +1164,7 @@ class _CalendarEventPageState extends State { } } - int _weekNumFromWeekDayOccurence(Set weekdays) { + int _weekNumFromWeekDayOccurence(List weekdays) { final weekNum = weekdays.first.occurrence; if (weekNum != null) { return weekNum - 1; @@ -1168,7 +1194,7 @@ class _CalendarEventPageState extends State { } if (!hasByWeekDays && !hasByMonthDays) { _rrule = rrule - .copyWith(frequency: freq, byWeekDays: {ByWeekDayEntry(1, 1)}); + .copyWith(frequency: freq, byWeekDays: [ByWeekDayEntry(1, 1)]); } else { _rrule = rrule.copyWith(frequency: freq); } @@ -1177,8 +1203,8 @@ class _CalendarEventPageState extends State { if (!hasByWeekDays || !hasByMonths) { _rrule = rrule.copyWith( frequency: freq, - byWeekDays: {ByWeekDayEntry(1, 1)}, - byMonths: {1}); + byWeekDays: [ByWeekDayEntry(1, 1)], + byMonths: [1]); } else { _rrule = rrule.copyWith(frequency: freq); } @@ -1258,4 +1284,27 @@ class _CalendarEventPageState extends State { void showInSnackBar(BuildContext context, String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); } + + Future selectColorDialog(List colors) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select Event color'), + children: colors.map((color) => + SimpleDialogOption( + onPressed: () { Navigator.pop(context, color); }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(color.color)), + ), + ) + ).toList() + ); + } + ); + } } diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index a8d4b2b2..781b39b4 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -24,6 +24,7 @@ class _CalendarEventsPageState extends State { late DeviceCalendarPlugin _deviceCalendarPlugin; List _calendarEvents = []; + List? _eventColors; bool _isLoading = true; _CalendarEventsPageState(this._calendar) { @@ -33,6 +34,7 @@ class _CalendarEventsPageState extends State { @override void initState() { super.initState(); + _retrieveEventColors(); _retrieveCalendarEvents(); } @@ -123,6 +125,7 @@ class _CalendarEventsPageState extends State { _onLoading, _onDeletedFinished, ), + _eventColors ); })); if (refreshEvents != null && refreshEvents) { @@ -142,6 +145,10 @@ class _CalendarEventsPageState extends State { }); } + void _retrieveEventColors() async { + _eventColors = await _deviceCalendarPlugin.retrieveEventColors(_calendar); + } + Widget _getDeleteButton() { return IconButton( icon: const Icon(Icons.delete), diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index 3566d5df..b3a7c906 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -6,6 +6,7 @@ export 'src/models/calendar.dart'; export 'src/models/result.dart'; export 'src/models/reminder.dart'; export 'src/models/event.dart'; +export 'src/models/event_color.dart'; export 'src/models/retrieve_events_params.dart'; export 'package:rrule/rrule.dart'; export 'package:rrule/src/frequency.dart'; diff --git a/lib/src/common/channel_constants.dart b/lib/src/common/channel_constants.dart index 2eef3d2d..4c1890d5 100644 --- a/lib/src/common/channel_constants.dart +++ b/lib/src/common/channel_constants.dart @@ -11,6 +11,7 @@ class ChannelConstants { static const String methodNameCreateCalendar = 'createCalendar'; static const String methodNameDeleteCalendar = 'deleteCalendar'; static const String methodNameShowiOSEventModal = 'showiOSEventModal'; + static const String methodNameRetrieveEventColors = 'retrieveEventColors'; static const String parameterNameCalendarId = 'calendarId'; static const String parameterNameStartDate = 'startDate'; @@ -23,4 +24,5 @@ class ChannelConstants { static const String parameterNameCalendarName = 'calendarName'; static const String parameterNameCalendarColor = 'calendarColor'; static const String parameterNameLocalAccountName = 'localAccountName'; + static const String parameterAccountName = "accountName"; } diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index 4c1d12f1..b48d8971 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; +import 'package:device_calendar/src/models/event_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:timezone/data/latest.dart' as tz; @@ -342,6 +343,21 @@ class DeviceCalendarPlugin { ); } + Future?> retrieveEventColors(Calendar calendar) async { + if (!Platform.isAndroid) { + return null; + } + final accountName = calendar.accountName; + if (accountName == null) { + return []; + } + final dynamic colors = await _invokeChannelMethod(ChannelConstants.methodNameRetrieveEventColors, + arguments: () => { + ChannelConstants.parameterAccountName: accountName, + },); + return (colors.data as List).cast().map((color) => EventColor(color[0], color[1])).toList(); + } + Future> _invokeChannelMethod( String channelMethodName, { Function(Result)? assertParameters, diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 94ef6217..12e359d8 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; +import 'event_color.dart'; import '../../device_calendar.dart'; import '../common/error_messages.dart'; @@ -49,6 +50,15 @@ class Event { /// Indicates if this event is of confirmed, canceled, tentative or none status EventStatus? status; + /// Read-only. Color of the event + int? get color=> _color; + + /// The color of this event + int? _color; + + /// The color key of this event. This is needed to change the event color + int? _colorKey; + ///Note for development: /// ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. @@ -110,6 +120,7 @@ class Event { calendarId = json['calendarId']; title = json['eventTitle']; description = json['eventDescription']; + _color = json['eventColor']; startTimestamp = json['eventStartDate']; startLocationName = json['eventStartTimeZone']; @@ -237,6 +248,8 @@ class Event { data['eventURL'] = url?.data?.contentText; data['availability'] = availability.enumToString; data['eventStatus'] = status?.enumToString; + data['eventColor'] = color; + data['eventColorKey'] = _colorKey; if (attendees != null) { data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); @@ -310,4 +323,9 @@ class Event { return false; } } -} + + void updateEventColor(EventColor eventColor) { + _color = eventColor.color; + _colorKey = eventColor.colorKey; + } +} \ No newline at end of file diff --git a/lib/src/models/event_color.dart b/lib/src/models/event_color.dart new file mode 100644 index 00000000..0851bf23 --- /dev/null +++ b/lib/src/models/event_color.dart @@ -0,0 +1,9 @@ +class EventColor { + final int color; + final int colorKey; + + EventColor(this.color, this.colorKey); + + @override + String toString() => 'EventColor(color: $color, colorKey: $colorKey)'; +} \ No newline at end of file From 7dce8d1a88eedfccd4f927dab3486596170bc894 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Sun, 14 Jul 2024 21:08:54 +0200 Subject: [PATCH 28/46] added removing of event color --- .../presentation/pages/calendar_event.dart | 22 +++++++++++++------ lib/src/models/event.dart | 6 ++--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 47bd5a5c..82af5695 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -302,11 +302,10 @@ class _CalendarEventPageState extends State { final colors = _eventColors; if (colors != null) { final newColor = await selectColorDialog(colors); - if (newColor != null) { - setState(() { - _event?.updateEventColor(newColor); - }); - }} + setState(() { + _event?.updateEventColor(newColor); + }); + } }, ), SwitchListTile( @@ -1288,10 +1287,19 @@ class _CalendarEventPageState extends State { Future selectColorDialog(List colors) async { return await showDialog( context: context, + barrierDismissible: false, builder: (BuildContext context) { return SimpleDialog( title: const Text('Select Event color'), - children: colors.map((color) => + children: [ + SimpleDialogOption( + onPressed: () { Navigator.pop(context, null); }, + child: const Padding( + padding: EdgeInsets.all(16.0), + child: Text('Reset', textAlign: TextAlign.center,), + ), + ), + ...colors.map((color) => SimpleDialogOption( onPressed: () { Navigator.pop(context, color); }, child: Container( @@ -1302,7 +1310,7 @@ class _CalendarEventPageState extends State { color: Color(color.color)), ), ) - ).toList() + )] ); } ); diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 12e359d8..2c20e48f 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -324,8 +324,8 @@ class Event { } } - void updateEventColor(EventColor eventColor) { - _color = eventColor.color; - _colorKey = eventColor.colorKey; + void updateEventColor(EventColor? eventColor) { + _color = eventColor?.color; + _colorKey = eventColor?.colorKey; } } \ No newline at end of file From a669955e3792581aa94322d4d3bf71a872684069 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 22 Jul 2024 23:39:15 +0200 Subject: [PATCH 29/46] added updating of calendar color for android and ios --- .../devicecalendar/CalendarDelegate.kt | 23 ++++- .../devicecalendar/DeviceCalendarPlugin.kt | 24 ++++- .../presentation/pages/calendar_event.dart | 43 ++------- example/lib/presentation/pages/calendars.dart | 87 +++++++++++++++---- .../pages/color_picker_dialog.dart | 28 ++++++ ios/Classes/SwiftDeviceCalendarPlugin.swift | 30 +++++++ lib/device_calendar.dart | 1 + lib/src/common/channel_constants.dart | 3 + lib/src/device_calendar.dart | 76 +++++++++++++--- 9 files changed, 245 insertions(+), 70 deletions(-) create mode 100644 example/lib/presentation/pages/color_picker_dialog.dart diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 888c2bff..165c720a 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -1134,7 +1134,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : * load available event colors for the given account name * unable to find official documentation, so logic is based on https://android.googlesource.com/platform/packages/apps/Calendar.git/+/refs/heads/pie-release/src/com/android/calendar/EventInfoFragment.java **/ - fun retrieveEventColors(accountName: String): List> { + private fun retrieveColors(accountName: String, colorType: Int): List> { val contentResolver: ContentResolver? = _context?.contentResolver val uri: Uri = Colors.CONTENT_URI val colors = mutableListOf() @@ -1147,7 +1147,8 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : // load only event colors for the given account name val selection = "${Colors.COLOR_TYPE} = ? AND ${Colors.ACCOUNT_NAME} = ?" - val selectionArgs = arrayOf(Colors.TYPE_EVENT.toString(), accountName) + val selectionArgs = arrayOf(colorType.toString(), accountName) + val cursor: Cursor? = contentResolver?.query(uri, projection, selection, selectionArgs, null) cursor?.use { @@ -1164,6 +1165,24 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : return colors.map { Pair(it, displayColorKeyMap[it]!! ) }.toList() } + fun retrieveEventColors(accountName: String): List> { + return retrieveColors(accountName, Colors.TYPE_EVENT) + } + fun retrieveCalendarColors(accountName: String): List> { + return retrieveColors(accountName, Colors.TYPE_CALENDAR) + } + + fun updateCalendarColor(calendarId: Long, newColorKey: Int?, newColor: Int?): Boolean { + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarId) + val values = ContentValues().apply { + put(CalendarContract.Calendars.CALENDAR_COLOR_KEY, newColorKey) + put(CalendarContract.Calendars.CALENDAR_COLOR, newColor) + } + val rows = contentResolver?.update(uri, values, null, null) + return (rows ?: 0) > 0 + } + /** * Compares colors based on their hue values in the HSV color space. * https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-compose-integration-release/android-34/com/android/colorpicker/HsvColorComparator.java diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index 2af342bd..bde9ce5a 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -26,6 +26,8 @@ private const val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent" private const val CREATE_CALENDAR_METHOD = "createCalendar" private const val DELETE_CALENDAR_METHOD = "deleteCalendar" private const val RETRIEVE_EVENT_COLORS_METHOD = "retrieveEventColors" +private const val RETRIEVE_CALENDAR_COLORS_METHOD = "retrieveCalendarColors" +private const val UPDATE_CALENDAR_COLOR = "updateCalendarColor" // Method arguments private const val CALENDAR_ID_ARGUMENT = "calendarId" @@ -69,6 +71,7 @@ private const val EVENT_AVAILABILITY_ARGUMENT = "availability" private const val ATTENDANCE_STATUS_ARGUMENT = "attendanceStatus" private const val EVENT_STATUS_ARGUMENT = "eventStatus" private const val EVENT_COLOR_KEY_ARGUMENT = "eventColorKey" +private const val CALENDAR_COLOR_KEY_ARGUMENT = "calendarColorKey" class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { @@ -176,9 +179,28 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } RETRIEVE_EVENT_COLORS_METHOD -> { val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) - val colors = _calendarDelegate.retrieveEventColors(accountName!!) + val colors = _calendarDelegate.retrieveEventColors(accountName!!, ) result.success(colors.map { listOf(it.first, it.second) }) } + RETRIEVE_CALENDAR_COLORS_METHOD -> { + val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) + if (accountName == null) { + return []; + } + val colors = _calendarDelegate.retrieveCalendarColors(accountName) + result.success(colors.map { listOf(it.first, it.second) }) + } + UPDATE_CALENDAR_COLOR -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT)?.toLong() + if (calendarId == null) { + result.success(false) + return + } + val newColorKey = (call.argument(CALENDAR_COLOR_KEY_ARGUMENT))?.toInt() + val newColor = (call.argument(CALENDAR_COLOR_ARGUMENT))?.toInt() + val success = _calendarDelegate.updateCalendarColor(calendarId, newColorKey, newColor) + result.success(success) + } else -> { result.notImplemented() } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 82af5695..523024e7 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:device_calendar/device_calendar.dart'; +import 'package:device_calendar_example/presentation/pages/color_picker_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; @@ -299,13 +300,13 @@ class _CalendarEventPageState extends State { color: Color(widget._event?.color ?? 0), )), onTap: () async { - final colors = _eventColors; - if (colors != null) { - final newColor = await selectColorDialog(colors); + if (_eventColors != null) { + final colors = _eventColors?.map((eventColor) => Color(eventColor.color)).toList(); + final newColor = await ColorPickerDialog.selectColorDialog(colors ?? [], context); setState(() { - _event?.updateEventColor(newColor); + _event?.updateEventColor(_eventColors?.firstWhereOrNull((eventColor) => eventColor.color == newColor?.value)); }); - } + } }, ), SwitchListTile( @@ -1283,36 +1284,4 @@ class _CalendarEventPageState extends State { void showInSnackBar(BuildContext context, String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); } - - Future selectColorDialog(List colors) async { - return await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return SimpleDialog( - title: const Text('Select Event color'), - children: [ - SimpleDialogOption( - onPressed: () { Navigator.pop(context, null); }, - child: const Padding( - padding: EdgeInsets.all(16.0), - child: Text('Reset', textAlign: TextAlign.center,), - ), - ), - ...colors.map((color) => - SimpleDialogOption( - onPressed: () { Navigator.pop(context, color); }, - child: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color(color.color)), - ), - ) - )] - ); - } - ); - } } diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 389d9b02..3c8bf1f5 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -1,7 +1,11 @@ +import 'dart:io'; + import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; +import 'package:device_calendar_example/presentation/pages/color_picker_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:collection/collection.dart'; import 'calendar_events.dart'; @@ -17,6 +21,7 @@ class CalendarsPage extends StatefulWidget { class _CalendarsPageState extends State { late DeviceCalendarPlugin _deviceCalendarPlugin; List _calendars = []; + List get _writableCalendars => _calendars.where((c) => c.isReadOnly == false).toList(); @@ -46,7 +51,10 @@ class _CalendarsPageState extends State { padding: const EdgeInsets.all(10.0), child: Text( 'WARNING: some aspects of saving events are hardcoded in this example app. As such we recommend you do not modify existing events as this may result in loss of information', - style: Theme.of(context).textTheme.titleLarge, + style: Theme + .of(context) + .textTheme + .titleLarge, ), ), Expanded( @@ -55,15 +63,13 @@ class _CalendarsPageState extends State { itemCount: _calendars.length, itemBuilder: (BuildContext context, int index) { return GestureDetector( - key: Key(_calendars[index].isReadOnly == true - ? 'readOnlyCalendar${_readOnlyCalendars.indexWhere((c) => c.id == _calendars[index].id)}' - : 'writableCalendar${_writableCalendars.indexWhere((c) => c.id == _calendars[index].id)}'), + key: ValueKey(_calendars[index].color), onTap: () async { await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventsPage(_calendars[index], - key: const Key('calendarEventsPage')); - })); + return CalendarEventsPage(_calendars[index], + key: const Key('calendarEventsPage')); + })); }, child: Padding( padding: const EdgeInsets.all(10.0), @@ -75,21 +81,64 @@ class _CalendarsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "${_calendars[index].id}: ${_calendars[index].name!}", + "${_calendars[index] + .id}: ${_calendars[index].name!}", style: - Theme.of(context).textTheme.titleSmall, + Theme + .of(context) + .textTheme + .titleSmall, ), Text( - "Account: ${_calendars[index].accountName!}"), + "Account: ${_calendars[index] + .accountName!}"), Text( "type: ${_calendars[index].accountType}"), ])), - Container( - width: 15, - height: 15, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color(_calendars[index].color!)), + GestureDetector( + onTap: () async { + final calendar = _calendars[index]; + final googleCalendarColors = await _deviceCalendarPlugin + .retrieveCalendarColors(_calendars[index]); + final colors = googleCalendarColors.isNotEmpty + ? googleCalendarColors.map((calendarColor) => + Color(calendarColor.color)).toList() + : [ + Colors.red, + Colors.green, + Colors.blue, + Colors.yellow, + Colors.orange, + Colors.purple, + Colors.cyan, + Colors.pink, + Colors.brown, + Colors.grey, + ]; + final color = await ColorPickerDialog + .selectColorDialog(colors, context); + if (color != null) { + final success = await _deviceCalendarPlugin + .updateCalendarColor(calendar, + calendarColor: googleCalendarColors + .firstWhereOrNull((calendarColor) => + calendarColor.color == color.value), + color: color); + if (success) { + _retrieveCalendars(); + } + } + }, + child: Container( + key: ValueKey(_calendars[index].color), + margin: const EdgeInsets.symmetric( + horizontal: 5, vertical: 10), + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(_calendars[index].color!)), + ), ), const SizedBox(width: 10), if (_calendars[index].isDefault!) @@ -116,8 +165,8 @@ class _CalendarsPageState extends State { onPressed: () async { final createCalendar = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return const CalendarAddPage(); - })); + return const CalendarAddPage(); + })); if (createCalendar == true) { _retrieveCalendars(); @@ -158,4 +207,4 @@ class _CalendarsPageState extends State { _retrieveCalendars(); }); } -} +} \ No newline at end of file diff --git a/example/lib/presentation/pages/color_picker_dialog.dart b/example/lib/presentation/pages/color_picker_dialog.dart new file mode 100644 index 00000000..04d7fc7d --- /dev/null +++ b/example/lib/presentation/pages/color_picker_dialog.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class ColorPickerDialog { + static Future selectColorDialog(List colors, BuildContext context) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select color'), + children: [ + ...colors.map((color) => + SimpleDialogOption( + onPressed: () { Navigator.pop(context, color); }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color), + ), + ) + )] + ); + } + ); + } +} \ No newline at end of file diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index f37d1a5a..f1d519a7 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -114,6 +114,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let deleteEventMethod = "deleteEvent" let deleteEventInstanceMethod = "deleteEventInstance" let showEventModalMethod = "showiOSEventModal" + let updateCalendarColor = "updateCalendarColor" let calendarIdArgument = "calendarId" let startDateArgument = "startDate" let endDateArgument = "endDate" @@ -185,6 +186,8 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele case showEventModalMethod: self.flutterResult = result showEventModal(call, result) + case updateCalendarColor: + updateCalendarColor(call, result) default: result(FlutterMethodNotImplemented) } @@ -245,6 +248,33 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } + + + private func createCalendar(_ call: FlutterMethodCall, _ result: FlutterResult) { + let arguments = call.arguments as! Dictionary + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + let color = arguments[calendarColorArgument] as! Int + + guard let calendar = eventStore.calendar(withIdentifier: calendarIdentifier) else { + print("Calendar not found") + result(false) + return + } + + // Update the calendar color + calendar.cgColor = UIColorFromRGB(color ?? 0)?.cgColor + + // Save the changes + do { + try eventStore.saveCalendar(calendar, commit: true) + result(false) + } catch { + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } + } + private func retrieveCalendars(_ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let ekCalendars = self.eventStore.calendars(for: .event) diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index b3a7c906..ab9f78d6 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -7,6 +7,7 @@ export 'src/models/result.dart'; export 'src/models/reminder.dart'; export 'src/models/event.dart'; export 'src/models/event_color.dart'; +export 'src/models/calendar_color.dart'; export 'src/models/retrieve_events_params.dart'; export 'package:rrule/rrule.dart'; export 'package:rrule/src/frequency.dart'; diff --git a/lib/src/common/channel_constants.dart b/lib/src/common/channel_constants.dart index 4c1890d5..b56f8adf 100644 --- a/lib/src/common/channel_constants.dart +++ b/lib/src/common/channel_constants.dart @@ -12,6 +12,8 @@ class ChannelConstants { static const String methodNameDeleteCalendar = 'deleteCalendar'; static const String methodNameShowiOSEventModal = 'showiOSEventModal'; static const String methodNameRetrieveEventColors = 'retrieveEventColors'; + static const String methodNameRetrieveCalendarColors = 'retrieveCalendarColors'; + static const String methodNameUpdateCalendarColor = 'updateCalendarColor'; static const String parameterNameCalendarId = 'calendarId'; static const String parameterNameStartDate = 'startDate'; @@ -23,6 +25,7 @@ class ChannelConstants { static const String parameterNameFollowingInstances = 'followingInstances'; static const String parameterNameCalendarName = 'calendarName'; static const String parameterNameCalendarColor = 'calendarColor'; + static const String parameterNameCalendarColorKey = 'calendarColorKey'; static const String parameterNameLocalAccountName = 'localAccountName'; static const String parameterAccountName = "accountName"; } diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index b48d8971..6fc778e9 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -2,19 +2,14 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; -import 'package:device_calendar/src/models/event_color.dart'; +import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:timezone/data/latest.dart' as tz; -import 'package:timezone/timezone.dart'; import 'common/channel_constants.dart'; import 'common/error_codes.dart'; import 'common/error_messages.dart'; -import 'models/calendar.dart'; -import 'models/event.dart'; -import 'models/result.dart'; -import 'models/retrieve_events_params.dart'; /// Provides functionality for working with device calendar(s) class DeviceCalendarPlugin { @@ -343,7 +338,7 @@ class DeviceCalendarPlugin { ); } - Future?> retrieveEventColors(Calendar calendar) async { + Future?> retrieveEventColors(Calendar calendar) async { if (!Platform.isAndroid) { return null; } @@ -351,11 +346,70 @@ class DeviceCalendarPlugin { if (accountName == null) { return []; } - final dynamic colors = await _invokeChannelMethod(ChannelConstants.methodNameRetrieveEventColors, + final dynamic colors = await _invokeChannelMethod( + ChannelConstants.methodNameRetrieveEventColors, arguments: () => { - ChannelConstants.parameterAccountName: accountName, - },); - return (colors.data as List).cast().map((color) => EventColor(color[0], color[1])).toList(); + ChannelConstants.parameterAccountName: accountName, + }, + ); + return (colors.data as List) + .cast() + .map((color) => EventColor(color[0], color[1])) + .toList(); + } + + /// Retrieves available colors for Google Calendars. + /// + /// For non-Google calendars, an empty list is returned. Use the `color` parameter in [updateCalendarColor] for these. + /// + /// [calendar] The calendar to retrieve colors for. + /// + /// Returns a List with available colors for Google Calendars or an empty list for others. + Future> retrieveCalendarColors(Calendar calendar) async { + if (!Platform.isAndroid) { + return []; + } + final accountName = calendar.accountName; + if (accountName == null) { + return []; + } + final dynamic colors = await _invokeChannelMethod( + ChannelConstants.methodNameRetrieveCalendarColors, + arguments: () => { + ChannelConstants.parameterAccountName: accountName, + }, + ); + return (colors.data as List) + .cast() + .map((color) => CalendarColor(color[0], color[1])) + .toList(); + } + + /// Updates the color of a calendar using Google Calendar colors or platform-specific colors. + /// [calendar] The calendar to update. Must have a non-null `id`. + /// [calendarColor] Required for Google Calendars where [retrieveCalendarColors] is not empty. + /// [color] Required for locale or iOS Calendars where [retrieveCalendarColors] is empty. + /// + /// Returns `true` if the update was successful, otherwise `false`. + Future updateCalendarColor(Calendar calendar, + {CalendarColor? calendarColor, Color? color}) async { + final calendarId = calendar.id; + if (calendarId == null || color == null && calendarColor == null) { + return false; + } + final result = await _invokeChannelMethod( + ChannelConstants.methodNameUpdateCalendarColor, + arguments: () => { + ChannelConstants.parameterNameCalendarId: Platform.isAndroid ? int.tryParse(calendarId) : calendarId, + ChannelConstants.parameterNameCalendarColorKey: calendarColor?.colorKey, + ChannelConstants.parameterNameCalendarColor: color?.value, + }, + ); + final success = (result.data as bool?) ?? false; + if (success) { + calendar.color = color?.value ?? calendarColor?.color; + } + return success; } Future> _invokeChannelMethod( From f44704179a52358cee7fd315e602a0e477d76e19 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Tue, 23 Jul 2024 00:10:02 +0200 Subject: [PATCH 30/46] fixes for iOS updateCalendarcolor --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 25 ++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index f1d519a7..323c8f5a 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -248,31 +248,36 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } - - - private func createCalendar(_ call: FlutterMethodCall, _ result: FlutterResult) { - let arguments = call.arguments as! Dictionary + private func updateCalendarColor(_ call: FlutterMethodCall, _ result: FlutterResult) { let arguments = call.arguments as! Dictionary let calendarId = arguments[calendarIdArgument] as! String let color = arguments[calendarColorArgument] as! Int - - guard let calendar = eventStore.calendar(withIdentifier: calendarIdentifier) else { + + guard let calendar = eventStore.calendar(withIdentifier: calendarId) else { print("Calendar not found") result(false) return } - + // Update the calendar color - calendar.cgColor = UIColorFromRGB(color ?? 0)?.cgColor - + calendar.cgColor = UIColorFromRGB(color).cgColor + // Save the changes do { try eventStore.saveCalendar(calendar, commit: true) - result(false) + result(true) // Assuming the operation was successful, return true } catch { result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) } } + + func UIColorFromRGB(_ rgbValue: Int) -> UIColor { + return UIColor( + red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, + green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, + blue: CGFloat(rgbValue & 0x0000FF) / 255.0, + alpha: CGFloat(1.0) + ) } private func retrieveCalendars(_ result: @escaping FlutterResult) { From c333f314b77353b923e45628d42272dd4db6c7df Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Tue, 23 Jul 2024 14:38:53 +0200 Subject: [PATCH 31/46] small fix --- .../com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index bde9ce5a..a5d7df80 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -179,13 +179,18 @@ class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } RETRIEVE_EVENT_COLORS_METHOD -> { val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) + if (accountName == null) { + result.success(intArrayOf()) + return; + } val colors = _calendarDelegate.retrieveEventColors(accountName!!, ) result.success(colors.map { listOf(it.first, it.second) }) } RETRIEVE_CALENDAR_COLORS_METHOD -> { val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) if (accountName == null) { - return []; + result.success(intArrayOf()) + return; } val colors = _calendarDelegate.retrieveCalendarColors(accountName) result.success(colors.map { listOf(it.first, it.second) }) From cd0c652380fcc5767ca5c5acdfe371479ed3b464 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Tue, 23 Jul 2024 21:56:10 +0200 Subject: [PATCH 32/46] added calendar color file --- lib/src/models/calendar_color.dart | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 lib/src/models/calendar_color.dart diff --git a/lib/src/models/calendar_color.dart b/lib/src/models/calendar_color.dart new file mode 100644 index 00000000..e63e0be1 --- /dev/null +++ b/lib/src/models/calendar_color.dart @@ -0,0 +1,9 @@ +class CalendarColor { + final int color; + final int colorKey; + + CalendarColor(this.color, this.colorKey); + + @override + String toString() => 'CalendarColor(color: $color, colorKey: $colorKey)'; +} \ No newline at end of file From 75ee949f416aaaa9fafdfd976b66d069a8828311 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 12 Aug 2024 12:09:10 +0200 Subject: [PATCH 33/46] made colorKey accessible in event --- lib/src/models/event.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 2c20e48f..1c5fe91f 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -53,10 +53,13 @@ class Event { /// Read-only. Color of the event int? get color=> _color; - /// The color of this event + /// Read-only. Color of the event + int? get colorKey=> _colorKey; + + /// Only updatable for Android calendars where [DeviceCalendarPlugin.retrieveEventColors] returns an empty list. int? _color; - /// The color key of this event. This is needed to change the event color + /// Only updatable for colors of [DeviceCalendarPlugin.retrieveEventColors]. int? _colorKey; ///Note for development: From 72b58f27bf0af6975166381efa467ef8a50ad412 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 12 Aug 2024 14:20:55 +0200 Subject: [PATCH 34/46] set eventcolor, loaded eventColorKey for event --- .../kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt | 3 +++ .../kotlin/com/builttoroam/devicecalendar/common/Constants.kt | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 165c720a..dd73716f 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -628,6 +628,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : values.put(Events.EVENT_END_TIMEZONE, endTimeZone) values.put(Events.DURATION, duration) values.put(Events.EVENT_COLOR_KEY, event.eventColorKey) + values.put(Events.EVENT_COLOR, event.eventColor) return values } @@ -942,6 +943,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : val availability = parseAvailability(cursor.getInt(Cst.EVENT_PROJECTION_AVAILABILITY_INDEX)) val eventStatus = parseEventStatus(cursor.getInt(Cst.EVENT_PROJECTION_STATUS_INDEX)) val eventColor = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) + val eventColorKey = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX) val event = Event() event.eventTitle = title ?: "New Event" event.eventId = eventId.toString() @@ -958,6 +960,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : event.availability = availability event.eventStatus = eventStatus event.eventColor = if (eventColor == 0) null else eventColor + event.eventColorKey = if (eventColorKey == 0) null else eventColorKey return event } diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt index fc49f227..f02eebd2 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -51,6 +51,7 @@ class Constants { const val EVENT_PROJECTION_AVAILABILITY_INDEX: Int = 13 const val EVENT_PROJECTION_STATUS_INDEX: Int = 14 const val EVENT_PROJECTION_EVENT_COLOR_INDEX: Int = 15 + const val EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX: Int = 16 val EVENT_PROJECTION: Array = arrayOf( CalendarContract.Instances.EVENT_ID, @@ -68,7 +69,8 @@ class Constants { CalendarContract.Events.EVENT_END_TIMEZONE, CalendarContract.Events.AVAILABILITY, CalendarContract.Events.STATUS, - CalendarContract.Events.EVENT_COLOR + CalendarContract.Events.EVENT_COLOR, + CalendarContract.Events.EVENT_COLOR_KEY ) const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 From e6e88e867444f568071900671355be10dd67e732 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 12 Aug 2024 18:12:35 +0200 Subject: [PATCH 35/46] mvoed color picker file --- .../lib/presentation/color_picker_dialog.dart | 28 +++++++++++++++++++ example/lib/presentation/pages/calendars.dart | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 example/lib/presentation/color_picker_dialog.dart diff --git a/example/lib/presentation/color_picker_dialog.dart b/example/lib/presentation/color_picker_dialog.dart new file mode 100644 index 00000000..04d7fc7d --- /dev/null +++ b/example/lib/presentation/color_picker_dialog.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class ColorPickerDialog { + static Future selectColorDialog(List colors, BuildContext context) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select color'), + children: [ + ...colors.map((color) => + SimpleDialogOption( + onPressed: () { Navigator.pop(context, color); }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color), + ), + ) + )] + ); + } + ); + } +} \ No newline at end of file diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 3c8bf1f5..5dc921bc 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; -import 'package:device_calendar_example/presentation/pages/color_picker_dialog.dart'; +import 'package:device_calendar_example/presentation/color_picker_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:collection/collection.dart'; From 35c49e62dbcc1a89d46966029c3e62c47cd5cbe4 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Mon, 12 Aug 2024 18:12:50 +0200 Subject: [PATCH 36/46] removed old color picker --- .../pages/color_picker_dialog.dart | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 example/lib/presentation/pages/color_picker_dialog.dart diff --git a/example/lib/presentation/pages/color_picker_dialog.dart b/example/lib/presentation/pages/color_picker_dialog.dart deleted file mode 100644 index 04d7fc7d..00000000 --- a/example/lib/presentation/pages/color_picker_dialog.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -class ColorPickerDialog { - static Future selectColorDialog(List colors, BuildContext context) async { - return await showDialog( - context: context, - builder: (BuildContext context) { - return SimpleDialog( - title: const Text('Select color'), - children: [ - ...colors.map((color) => - SimpleDialogOption( - onPressed: () { Navigator.pop(context, color); }, - child: Container( - width: 48, - height: 48, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: color), - ), - ) - )] - ); - } - ); - } -} \ No newline at end of file From 992240b2228f437b82688c257df3724a840f5564 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 4 Sep 2024 22:22:14 +0200 Subject: [PATCH 37/46] exposed color and colorKey --- lib/src/models/event.dart | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 1c5fe91f..8b170910 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -51,16 +51,13 @@ class Event { EventStatus? status; /// Read-only. Color of the event - int? get color=> _color; - /// Read-only. Color of the event - int? get colorKey=> _colorKey; /// Only updatable for Android calendars where [DeviceCalendarPlugin.retrieveEventColors] returns an empty list. - int? _color; + int? color; /// Only updatable for colors of [DeviceCalendarPlugin.retrieveEventColors]. - int? _colorKey; + int? colorKey; ///Note for development: /// @@ -123,7 +120,7 @@ class Event { calendarId = json['calendarId']; title = json['eventTitle']; description = json['eventDescription']; - _color = json['eventColor']; + color = json['eventColor']; startTimestamp = json['eventStartDate']; startLocationName = json['eventStartTimeZone']; @@ -252,7 +249,7 @@ class Event { data['availability'] = availability.enumToString; data['eventStatus'] = status?.enumToString; data['eventColor'] = color; - data['eventColorKey'] = _colorKey; + data['eventColorKey'] = colorKey; if (attendees != null) { data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); @@ -328,7 +325,7 @@ class Event { } void updateEventColor(EventColor? eventColor) { - _color = eventColor?.color; - _colorKey = eventColor?.colorKey; + color = eventColor?.color; + colorKey = eventColor?.colorKey; } } \ No newline at end of file From 44c248c9f083fa91cf2341549107fbce7570a0d7 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 13:25:11 +0200 Subject: [PATCH 38/46] fixed color picker import --- example/lib/presentation/pages/calendar_event.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index 523024e7..a447d33c 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -2,12 +2,12 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:device_calendar/device_calendar.dart'; -import 'package:device_calendar_example/presentation/pages/color_picker_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:intl/intl.dart'; +import '../color_picker_dialog.dart'; import '../date_time_picker.dart'; import '../recurring_event_dialog.dart'; import 'event_attendee.dart'; From ba0910b43a1271fe17ce118d6e8f92ed599047ea Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 17:05:56 +0200 Subject: [PATCH 39/46] fixes for setting event color --- example/lib/presentation/pages/calendar_event.dart | 2 +- example/lib/presentation/pages/calendar_events.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index a447d33c..a4bc270b 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -304,7 +304,7 @@ class _CalendarEventPageState extends State { final colors = _eventColors?.map((eventColor) => Color(eventColor.color)).toList(); final newColor = await ColorPickerDialog.selectColorDialog(colors ?? [], context); setState(() { - _event?.updateEventColor(_eventColors?.firstWhereOrNull((eventColor) => eventColor.color == newColor?.value)); + _event?.updateEventColor(_eventColors?.firstWhereOrNull((eventColor) => Color(eventColor.color).value == newColor?.value)); }); } }, diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index 781b39b4..6b2e8384 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -79,7 +79,7 @@ class _CalendarEventsPageState extends State { onPressed: () async { final refreshEvents = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventPage(_calendar); + return CalendarEventPage(_calendar, null, null, _eventColors); })); if (refreshEvents == true) { await _retrieveCalendarEvents(); From c702f17b663c963e6682d5d99246fe72fc82dad2 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 17:18:44 +0200 Subject: [PATCH 40/46] clean up --- example/lib/presentation/pages/calendars.dart | 4 +++- lib/src/models/event.dart | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 5dc921bc..bc434173 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -63,7 +63,9 @@ class _CalendarsPageState extends State { itemCount: _calendars.length, itemBuilder: (BuildContext context, int index) { return GestureDetector( - key: ValueKey(_calendars[index].color), + key: Key(_calendars[index].isReadOnly == true + ? 'readOnlyCalendar${_readOnlyCalendars.indexWhere((c) => c.id == _calendars[index].id)} color:${_calendars[index].color}' + : 'writableCalendar${_writableCalendars.indexWhere((c) => c.id == _calendars[index].id)} color:${_calendars[index].color}'), onTap: () async { await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 8b170910..d106eac1 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -50,13 +50,10 @@ class Event { /// Indicates if this event is of confirmed, canceled, tentative or none status EventStatus? status; - /// Read-only. Color of the event - - - /// Only updatable for Android calendars where [DeviceCalendarPlugin.retrieveEventColors] returns an empty list. + /// Read-only. Android exclusive. Updatable only using [Event.updateEventColor] with color from [DeviceCalendarPlugin.retrieveEventColors] int? color; - /// Only updatable for colors of [DeviceCalendarPlugin.retrieveEventColors]. + /// Read-only. Android exclusive. Updatable only using [Event.updateEventColor] with color from [DeviceCalendarPlugin.retrieveEventColors] int? colorKey; ///Note for development: From 2ee14a1035cc2e03a3478aa7f4e067fa531b54b5 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 17:26:30 +0200 Subject: [PATCH 41/46] added serilization test for eventColor --- lib/src/models/event.dart | 1 + test/device_calendar_test.dart | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index d106eac1..eda68ffa 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -118,6 +118,7 @@ class Event { title = json['eventTitle']; description = json['eventDescription']; color = json['eventColor']; + colorKey = json['eventColorKey']; startTimestamp = json['eventStartDate']; startLocationName = json['eventStartTimeZone']; diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index 132aad61..a2f1f1ca 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -216,7 +216,9 @@ void main() { recurrenceRule: recurrence, reminders: [reminder], availability: Availability.Busy, - status: EventStatus.Confirmed); + status: EventStatus.Confirmed, + ); + event.updateEventColor(EventColor(0xffff00ff, 1)); final stringEvent = event.toJson(); expect(stringEvent, isNotNull); @@ -241,5 +243,7 @@ void main() { expect(newEvent.reminders?.length, equals(1)); expect(newEvent.availability, equals(event.availability)); expect(newEvent.status, equals(event.status)); + expect(newEvent.color, equals(event.color)); + expect(newEvent.colorKey, equals(event.colorKey)); }); } From b41223cf6737c179bc2ea39bc47a01dbbe419e54 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 17:56:16 +0200 Subject: [PATCH 42/46] tiny formatting adjustment --- test/device_calendar_test.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index a2f1f1ca..0d91e738 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -216,8 +216,7 @@ void main() { recurrenceRule: recurrence, reminders: [reminder], availability: Availability.Busy, - status: EventStatus.Confirmed, - ); + status: EventStatus.Confirmed); event.updateEventColor(EventColor(0xffff00ff, 1)); final stringEvent = event.toJson(); From 787696271ec29fc41a300bc0f5a88da00682be79 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 11 Sep 2024 21:02:45 +0200 Subject: [PATCH 43/46] set example kotlin version to 1.8..22 --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 34fcf1ae..73ccfc61 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group 'com.builttoroam.devicecalendar' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.8.22' repositories { google() mavenCentral() From 755ddfa99832cf9bc722df3f472ec2438490c54a Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Fri, 13 Sep 2024 21:13:51 +0200 Subject: [PATCH 44/46] flutter_native_timezone made release builds fail, migrated to flutter_timezone --- example/lib/presentation/event_item.dart | 4 ++-- example/lib/presentation/pages/calendar_event.dart | 4 ++-- example/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index f91bb7de..b18d299d 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:intl/intl.dart'; import 'recurring_event_dialog.dart'; @@ -313,7 +313,7 @@ class _EventItemState extends State { void setCurentLocation() async { String? timezone; try { - timezone = await FlutterNativeTimezone.getLocalTimezone(); + timezone = await FlutterTimezone.getLocalTimezone(); } catch (e) { debugPrint('Could not get the local timezone'); } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index a4bc270b..83ad1a23 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -4,7 +4,7 @@ import 'package:collection/collection.dart'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:intl/intl.dart'; import '../color_picker_dialog.dart'; @@ -73,7 +73,7 @@ class _CalendarEventPageState extends State { void getCurentLocation() async { try { - _timezone = await FlutterNativeTimezone.getLocalTimezone(); + _timezone = await FlutterTimezone.getLocalTimezone(); } catch (e) { debugPrint('Could not get the local timezone'); } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b32d2fb0..9ffb81f4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter intl: ^0.17.0 uuid: ^3.0.6 - flutter_native_timezone: ^2.0.0 + flutter_timezone: ^3.0.1 device_calendar: path: ../ From 7791f596754213df3fa871a0c0316e139c78c305 Mon Sep 17 00:00:00 2001 From: Christopher Wolf <> Date: Wed, 25 Sep 2024 20:51:04 +0200 Subject: [PATCH 45/46] increased rrule version from 0.2.10 to 0.2.15 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7c99a9c5..101681fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ dependencies: sdk: flutter collection: ^1.16.0 timezone: ^0.9.0 - rrule: ^0.2.10 + rrule: ^0.2.15 dev_dependencies: flutter_test: From 2d8eddf0fdd96760a77530627d512f1caf440ebf Mon Sep 17 00:00:00 2001 From: Jonathan de Gaston Date: Fri, 7 Mar 2025 10:16:10 -0700 Subject: [PATCH 46/46] Fix isAllDay null error on iOS When an Event object is created in flutter with null allDay value the value will default to false on iOS like it does on Android. --- ios/Classes/SwiftDeviceCalendarPlugin.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 323c8f5a..8c302f49 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -841,7 +841,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let arguments = call.arguments as! Dictionary let calendarId = arguments[calendarIdArgument] as! String let eventId = arguments[eventIdArgument] as? String - let isAllDay = arguments[eventAllDayArgument] as! Bool + let isAllDay = (arguments[eventAllDayArgument] as? Bool) ?? false let startDateMillisecondsSinceEpoch = arguments[eventStartDateArgument] as! NSNumber let endDateDateMillisecondsSinceEpoch = arguments[eventEndDateArgument] as! NSNumber let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch.doubleValue / 1000.0)