From 6b8af4f99b5d99eb49eeb9fd0d0f3955c74a336b Mon Sep 17 00:00:00 2001 From: Nao Kreuzeder Date: Tue, 28 Nov 2023 10:33:57 +0100 Subject: [PATCH 01/11] added event color --- .../devicecalendar/CalendarDelegate.kt | 2 + .../devicecalendar/common/Constants.kt | 7 +- .../devicecalendar/models/Event.kt | 3 + lib/src/models/event.dart | 79 +++++++++---------- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 1cd3f98a..2ce608dc 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -938,6 +938,8 @@ 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.getLong(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) val event = Event() event.eventTitle = title ?: "New Event" event.eventId = eventId.toString() 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..3e5396e3 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,9 @@ 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 +69,9 @@ 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..749a858a 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,7 @@ class Event { var reminders: MutableList = mutableListOf() var availability: Availability? = null var eventStatus: EventStatus? = null +// ##### + var eventColor: Long? = null + } \ No newline at end of file diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index 94ef6217..00ebfa52 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -49,6 +49,10 @@ class Event { /// Indicates if this event is of confirmed, canceled, tentative or none status EventStatus? status; + //##### + /// Read-only. Color of the event + int? color; + ///Note for development: /// ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. @@ -59,20 +63,23 @@ class Event { ///`android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt` ///`android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt` ///`android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt` - Event(this.calendarId, - {this.eventId, - this.title, - this.start, - this.end, - this.description, - this.attendees, - this.recurrenceRule, - this.reminders, - this.availability = Availability.Busy, - this.location, - this.url, - this.allDay = false, - this.status}); + Event( + this.calendarId, { + this.eventId, + this.title, + this.start, + this.end, + this.description, + this.attendees, + this.recurrenceRule, + this.reminders, + this.availability = Availability.Busy, + this.location, + this.url, + this.allDay = false, + this.status, + this.color, + }); ///Get Event from JSON. /// @@ -110,22 +117,19 @@ class Event { calendarId = json['calendarId']; title = json['eventTitle']; description = json['eventDescription']; + color = json['eventColor']; //##### startTimestamp = json['eventStartDate']; startLocationName = json['eventStartTimeZone']; var startTimeZone = timeZoneDatabase.locations[startLocationName]; startTimeZone ??= local; - start = startTimestamp != null - ? TZDateTime.fromMillisecondsSinceEpoch(startTimeZone, startTimestamp) - : TZDateTime.now(local); + start = startTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(startTimeZone, startTimestamp) : TZDateTime.now(local); endTimestamp = json['eventEndDate']; endLocationName = json['eventEndTimeZone']; var endLocation = timeZoneDatabase.locations[endLocationName]; endLocation ??= startTimeZone; - end = endTimestamp != null - ? TZDateTime.fromMillisecondsSinceEpoch(endLocation, endTimestamp) - : TZDateTime.now(local); + end = endTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(endLocation, endTimestamp) : TZDateTime.now(local); allDay = json['eventAllDay'] ?? false; if (Platform.isAndroid && (allDay ?? false)) { // On Android, the datetime in an allDay event is adjusted to local @@ -161,9 +165,7 @@ class Event { // Getting and setting an organiser for iOS var organiser = Attendee.fromJson(json['organizer']); - var attendee = attendees?.firstWhereOrNull((at) => - at?.name == organiser.name && - at?.emailAddress == organiser.emailAddress); + var attendee = attendees?.firstWhereOrNull((at) => at?.name == organiser.name && at?.emailAddress == organiser.emailAddress); if (attendee != null) { attendee.isOrganiser = true; } @@ -175,33 +177,27 @@ class Event { //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(); + 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(); + 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(); + 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(); + 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(); + 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(); + json['recurrenceRule']['bysetpos'] = json['recurrenceRule']['bysetpos'].cast(); } // debugPrint("EVENT_MODEL: $title; RRULE = ${json['recurrenceRule']}"); recurrenceRule = RecurrenceRule.fromJson(json['recurrenceRule']); @@ -214,8 +210,7 @@ class Event { }).toList(); } if (legacyJSON) { - throw const FormatException( - 'legacy JSON detected. Please update your current JSONs as they may not be supported later on.'); + throw const FormatException('legacy JSON detected. Please update your current JSONs as they may not be supported later on.'); } } @@ -226,25 +221,23 @@ class Event { data['eventId'] = eventId; data['eventTitle'] = title; data['eventDescription'] = description; - data['eventStartDate'] = start?.millisecondsSinceEpoch ?? - TZDateTime.now(local).millisecondsSinceEpoch; + data['eventStartDate'] = start?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; data['eventStartTimeZone'] = start?.location.name; - data['eventEndDate'] = end?.millisecondsSinceEpoch ?? - TZDateTime.now(local).millisecondsSinceEpoch; + data['eventEndDate'] = end?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; data['eventEndTimeZone'] = end?.location.name; data['eventAllDay'] = allDay; data['eventLocation'] = location; data['eventURL'] = url?.data?.contentText; data['availability'] = availability.enumToString; data['eventStatus'] = status?.enumToString; + data['eventColor'] = color; //##### if (attendees != null) { data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); } if (attendees != null) { - data['organizer'] = - attendees?.firstWhereOrNull((a) => a!.isOrganiser)?.toJson(); + data['organizer'] = attendees?.firstWhereOrNull((a) => a!.isOrganiser)?.toJson(); } if (recurrenceRule != null) { From 5a9e2210792478403d2a1fecb4101bcb17769b79 Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Tue, 28 Nov 2023 11:23:50 +0100 Subject: [PATCH 02/11] Added event color field. --- CHANGELOG.md | 200 --- LICENSE | 27 - README.md | 121 -- analysis_options.yaml | 30 - android/.gitignore | 8 - android/build.gradle | 58 - android/gradle.properties | 3 - android/gradle/wrapper/gradle-wrapper.jar | Bin 54708 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - android/gradlew | 172 --- android/gradlew.bat | 84 -- android/proguard-rules.pro | 1 - android/settings.gradle | 1 - android/src/main/AndroidManifest.xml | 3 - .../devicecalendar/AvailabilitySerializer.kt | 18 - .../devicecalendar/CalendarDelegate.kt | 1267 ----------------- .../devicecalendar/DeviceCalendarPlugin.kt | 301 ---- .../devicecalendar/EventStatusSerializer.kt | 15 - .../devicecalendar/common/Constants.kt | 119 -- .../devicecalendar/common/ErrorCodes.kt | 11 - .../devicecalendar/common/ErrorMessages.kt | 16 - .../devicecalendar/models/Attendee.kt | 10 - .../devicecalendar/models/Availability.kt | 7 - .../devicecalendar/models/Calendar.kt | 13 - .../CalendarMethodsParametersCacheModel.kt | 16 - .../devicecalendar/models/Event.kt | 24 - .../devicecalendar/models/EventStatus.kt | 7 - .../devicecalendar/models/RecurrenceRule.kt | 17 - .../devicecalendar/models/Reminder.kt | 3 - device_calendar.iml | 19 - device_calendar_android.iml | 30 - example/.gitignore | 9 - example/.metadata | 8 - example/README.md | 195 --- example/analysis_options.yaml | 30 - example/android/.gitignore | 10 - example/android/app/build.gradle | 60 - example/android/app/proguard-rules.pro | 1 - .../android/app/src/main/AndroidManifest.xml | 44 - .../devicecalendarexample/MainActivity.kt | 6 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../app/src/main/res/values/styles.xml | 8 - example/android/build.gradle | 29 - example/android/gradle.properties | 3 - .../android/gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - example/android/gradlew | 160 --- example/android/gradlew.bat | 90 -- example/android/settings.gradle | 15 - example/device_calendar_example.iml | 17 - example/device_calendar_example_android.iml | 27 - example/integration_test/app_test.dart | 89 -- .../integration_test/integration_test.dart | 8 - .../integration_test_android.dart | 29 - example/integration_test/ios.sh | 24 - example/ios/.gitignore | 45 - example/ios/Flutter/AppFrameworkInfo.plist | 30 - example/ios/Flutter/Debug.xcconfig | 2 - example/ios/Flutter/Release.xcconfig | 2 - example/ios/Podfile | 45 - example/ios/Podfile.lock | 34 - example/ios/Runner.xcodeproj/project.pbxproj | 507 ------- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 87 -- .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - example/ios/Runner/AppDelegate.swift | 13 - .../AppIcon.appiconset/Contents.json | 122 -- .../Icon-App-1024x1024@1x.png | Bin 11112 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 - .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - .../Runner/Base.lproj/LaunchScreen.storyboard | 37 - example/ios/Runner/Base.lproj/Main.storyboard | 26 - example/ios/Runner/Info.plist | 55 - example/ios/Runner/Runner-Bridging-Header.h | 1 - example/lib/common/app_routes.dart | 3 - example/lib/main.dart | 29 - .../lib/presentation/date_time_picker.dart | 81 -- example/lib/presentation/event_item.dart | 342 ----- example/lib/presentation/input_dropdown.dart | 42 - .../lib/presentation/pages/calendar_add.dart | 164 --- .../presentation/pages/calendar_event.dart | 1261 ---------------- .../presentation/pages/calendar_events.dart | 190 --- example/lib/presentation/pages/calendars.dart | 161 --- .../presentation/pages/event_attendee.dart | 174 --- .../presentation/pages/event_reminders.dart | 102 -- .../presentation/recurring_event_dialog.dart | 101 -- example/pubspec.yaml | 27 - ios/.gitignore | 36 - ios/Assets/.gitkeep | 0 ios/Classes/DeviceCalendarPlugin.h | 4 - ios/Classes/DeviceCalendarPlugin.m | 8 - ios/Classes/SwiftDeviceCalendarPlugin.swift | 1136 --------------- ios/device_calendar.podspec | 22 - lib/device_calendar.dart | 17 - lib/src/common/calendar_enums.dart | 315 ---- lib/src/common/channel_constants.dart | 26 - lib/src/common/error_codes.dart | 6 - lib/src/common/error_messages.dart | 27 - lib/src/device_calendar.dart | 443 ------ lib/src/models/attendee.dart | 81 -- lib/src/models/calendar.dart | 56 - lib/src/models/event.dart | 306 ---- .../android/attendance_status.dart | 15 - .../android/attendee_details.dart | 23 - .../ios/attendance_status.dart | 18 - .../ios/attendee_details.dart | 21 - lib/src/models/reminder.dart | 18 - lib/src/models/result.dart | 33 - lib/src/models/retrieve_events_params.dart | 7 - pubspec.yaml | 29 - test/device_calendar_test.dart | 245 ---- 135 files changed, 10122 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 analysis_options.yaml delete mode 100644 android/.gitignore delete mode 100644 android/build.gradle delete mode 100644 android/gradle.properties delete mode 100644 android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 android/gradlew delete mode 100644 android/gradlew.bat delete mode 100644 android/proguard-rules.pro delete mode 100644 android/settings.gradle delete mode 100644 android/src/main/AndroidManifest.xml delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt delete mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt delete mode 100644 device_calendar.iml delete mode 100644 device_calendar_android.iml delete mode 100644 example/.gitignore delete mode 100644 example/.metadata delete mode 100644 example/README.md delete mode 100644 example/analysis_options.yaml delete mode 100644 example/android/.gitignore delete mode 100644 example/android/app/build.gradle delete mode 100644 example/android/app/proguard-rules.pro delete mode 100644 example/android/app/src/main/AndroidManifest.xml delete mode 100644 example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt delete mode 100644 example/android/app/src/main/res/drawable/launch_background.xml delete mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 example/android/app/src/main/res/values/styles.xml delete mode 100644 example/android/build.gradle delete mode 100644 example/android/gradle.properties delete mode 100644 example/android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 example/android/gradle/wrapper/gradle-wrapper.properties delete mode 100755 example/android/gradlew delete mode 100644 example/android/gradlew.bat delete mode 100644 example/android/settings.gradle delete mode 100644 example/device_calendar_example.iml delete mode 100644 example/device_calendar_example_android.iml delete mode 100644 example/integration_test/app_test.dart delete mode 100644 example/integration_test/integration_test.dart delete mode 100644 example/integration_test/integration_test_android.dart delete mode 100755 example/integration_test/ios.sh delete mode 100755 example/ios/.gitignore delete mode 100755 example/ios/Flutter/AppFrameworkInfo.plist delete mode 100755 example/ios/Flutter/Debug.xcconfig delete mode 100755 example/ios/Flutter/Release.xcconfig delete mode 100644 example/ios/Podfile delete mode 100755 example/ios/Podfile.lock delete mode 100644 example/ios/Runner.xcodeproj/project.pbxproj delete mode 100755 example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100755 example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100755 example/ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100755 example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100755 example/ios/Runner/AppDelegate.swift delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100755 example/ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100755 example/ios/Runner/Base.lproj/Main.storyboard delete mode 100755 example/ios/Runner/Info.plist delete mode 100755 example/ios/Runner/Runner-Bridging-Header.h delete mode 100644 example/lib/common/app_routes.dart delete mode 100644 example/lib/main.dart delete mode 100644 example/lib/presentation/date_time_picker.dart delete mode 100644 example/lib/presentation/event_item.dart delete mode 100644 example/lib/presentation/input_dropdown.dart delete mode 100644 example/lib/presentation/pages/calendar_add.dart delete mode 100644 example/lib/presentation/pages/calendar_event.dart delete mode 100644 example/lib/presentation/pages/calendar_events.dart delete mode 100644 example/lib/presentation/pages/calendars.dart delete mode 100644 example/lib/presentation/pages/event_attendee.dart delete mode 100644 example/lib/presentation/pages/event_reminders.dart delete mode 100644 example/lib/presentation/recurring_event_dialog.dart delete mode 100644 example/pubspec.yaml delete mode 100644 ios/.gitignore delete mode 100644 ios/Assets/.gitkeep delete mode 100644 ios/Classes/DeviceCalendarPlugin.h delete mode 100644 ios/Classes/DeviceCalendarPlugin.m delete mode 100644 ios/Classes/SwiftDeviceCalendarPlugin.swift delete mode 100644 ios/device_calendar.podspec delete mode 100644 lib/device_calendar.dart delete mode 100644 lib/src/common/calendar_enums.dart delete mode 100644 lib/src/common/channel_constants.dart delete mode 100644 lib/src/common/error_codes.dart delete mode 100644 lib/src/common/error_messages.dart delete mode 100644 lib/src/device_calendar.dart delete mode 100644 lib/src/models/attendee.dart delete mode 100644 lib/src/models/calendar.dart delete mode 100644 lib/src/models/event.dart delete mode 100644 lib/src/models/platform_specifics/android/attendance_status.dart delete mode 100644 lib/src/models/platform_specifics/android/attendee_details.dart delete mode 100644 lib/src/models/platform_specifics/ios/attendance_status.dart delete mode 100644 lib/src/models/platform_specifics/ios/attendee_details.dart delete mode 100644 lib/src/models/reminder.dart delete mode 100644 lib/src/models/result.dart delete mode 100644 lib/src/models/retrieve_events_params.dart delete mode 100644 pubspec.yaml delete mode 100644 test/device_calendar_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 2fa175fa..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,200 +0,0 @@ -# Changelog - - - -## [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 - - *Note:* `timezone 0.9.0` [removed named database files](https://pub.dev/packages/timezone/changelog#090). If you are only using `device_calendar`, you can ignore this note. -- Added support for all-day multi-day events on iOS -- Fixed iOS issue of adding attendees to events -- Fixed Android issue of the `ownerAccount` being null - -## [4.2.0](https://github.com/builttoroam/device_calendar/releases/tag/4.2.0) - -- Fix: apks can be build correctly now -- Support for viewing and editing attendee status - - iOS needs a specific native view and permissions to edit attendees due to iOS restrictions. See README and example app. - -## [4.1.0](https://github.com/builttoroam/device_calendar/releases/tag/4.1.0) - -- Fix: title, descriptions etc are now retrieved properly. -- Fix: Event JSONs created and are now readable. Previous (mislabeled) JSONs are also readable with warnings. -- Fix: removed depreceated plugins from Example. -- Integration tests are now working. Android instructions are ready. -- Gradle plug-ins are updated. -- Compiles with jvm 1.8, should be compilable for Flutter 2.9+ -- Android: proper support for all day events, and multi-day all day events. - -## [4.0.1](https://github.com/builttoroam/device_calendar/releases/tag/4.0.1) - -- Fix: event time are now properly retrieved - -## [4.0.0](https://github.com/builttoroam/device_calendar/releases/tag/4.0.0) - -- Timezone plugin and logic implemented. All issues related to timezone shoulde be fixed. -- Events.availability defaults to busy when not specified [354](https://github.com/builttoroam/device_calendar/pull/354) -- Events parameter now includes location and url. [319](https://github.com/builttoroam/device_calendar/pull/319) -- Android: Fixed bug where platform exception appeared, when Events.availability was null on Event [241](https://github.com/builttoroam/device_calendar/issues/241) -- Fixed various issues in example [270](https://github.com/builttoroam/device_calendar/issues/270), [268](https://github.com/builttoroam/device_calendar/issues/268) -- Android: deleteEvent code aligned with flutter [258](https://github.com/builttoroam/device_calendar/issues/258) -- Android: Updated to V2 embeddding [326](https://github.com/builttoroam/device_calendar/issues/326) -- iOS: Updated swift versions, possibly improved compability with Obj-C [flutter/flutter#16049 (comment)](https://github.com/flutter/flutter/issues/16049#issuecomment-611192738) - -## [3.9.0](https://github.com/builttoroam/device_calendar/releases/tag/3.9.0) - -- Migrated to null safety -- Updated multiple underlying dependencies -- Rebuilt iOS podfile -- Upgraded to new Android plugins APIs for flutter - -## 3.1.0 25th March 2020 - Bug fixes and new features - -- Boolean variable `isDefault` added for issue [145](https://github.com/builttoroam/device_calendar/issues/145) (**NOTE**: This is not supported Android API 16 or lower, `isDefault` will always be false) -- Events with 'null' title now defaults to 'New Event', issue [126](https://github.com/builttoroam/device_calendar/issues/126) -- Updated property summaries for issues [121](https://github.com/builttoroam/device_calendar/issues/121) and [122](https://github.com/builttoroam/device_calendar/issues/122) -- Updated example documentation for issue [119](https://github.com/builttoroam/device_calendar/issues/119) -- Read-only calendars cannot be edited or deleted for the example app -- Added `DayOfWeekGroup` enum and an extension `getDays` to get corresponding dates of the enum values -- Added to retrieve colour for calendars. Thanks to [nadavfima](https://github.com/nadavfima) for the contribution and PR to add colour support for both Android and iOS -- Added compatibility with a new Flutter plugin for Android. Thanks to the PR submitted by [RohitKumarMishra](https://github.com/RohitKumarMishra) -- [Android] Fixed all day timezone issue [164](https://github.com/builttoroam/device_calendar/issues/164) -- Added support for deleting individual or multiple instances of a recurring event for issue [108](https://github.com/builttoroam/device_calendar/issues/108) -- Ability to add local calendars with a desired colour for issue [115](https://github.com/builttoroam/device_calendar/issues/115) -- Returns account name and type for each calendars for issue [179](https://github.com/builttoroam/device_calendar/issues/179) - -## 3.0.0+3 3rd February 2020 - -- Fixed all day conditional check for issue [162](https://github.com/builttoroam/device_calendar/issues/162) - -## 3.0.0+2 30th January 2020 - -- Updated `event.allDay` property in `createOrUpdateEvent` method to be null-aware - -## 3.0.0+1 28th January 2020 - -- Updated `event.url` property in `createOrUpdateEvent` method to be null-aware for issue [152](https://github.com/builttoroam/device_calendar/issues/152) - -## 3.0.0 21st January 2020 - -- **BREAKING CHANGE** Properties for the attendee model in `attendee.dart` file have been changed: - - Boolean property `isRequired` has been replaced to `AttendeeRole` enum - - New arugment added for `AttendeeRole` property -- **BREAKING CHANGE** Package updates: - - [Android] Updated Gradle plugin to 3.5.2 and Gradle wrapper to 5.4.1 - - [iOS] Updated Swift to 5 -- `name` and `isOrganiser` (read-only) properties have been added -- Attendee UI update for the example app -- Ability to add, modify or remove an attendee - -## 2.0.0 17th January 2020 - -- **BREAKING CHANGE** The recurrence models in `recurrence_rule.dart` file have been chaged -- **BREAKING CHANGE** All articles used in property names or arugments have been removed (i.e. enum `DayOfTheWeek` to `DayOfWeek`) -- Recurrence fix for monthly and yearly frequencies -- UI update for the example app -- Add support for all day events - -## 1.0.0+3 9th January 2020 - -- Flutter upgrade to 1.12.13 -- Added an URL input for calendar events for issue [132](https://github.com/builttoroam/device_calendar/issues/132) - -## 1.0.0+2 30th August 2019 - -- Fix home page URL - -## 1.0.0+1 30th August 2019 - -- Add integration tests to example app. Note that this is more for internal use at the moment as it currently requires an Android device with a calendar that can be written to and assumes that the tests are executed from a Mac. - -## 1.0.0 28th August 2019 - -- **BREAKING CHANGE** `retrieveCalendars` and `retrieveEvents` now return lists that cannot be modified (`UnmodifiableListView`) to address part of issue [113](https://github.com/builttoroam/device_calendar/issues/113) -- Support for more advanced recurrence rules -- Update README to include information about using ProGuard for issue [99](https://github.com/builttoroam/device_calendar/issues/99) -- Made event title optional to fix issue [72](https://github.com/builttoroam/device_calendar/issues/72) -- Return information about the organiser of the event as per issue [73](https://github.com/builttoroam/device_calendar/issues/73) -- Return attendance status of attendees and if they're required for an event. These are details are different across iOS and Android and so are returned within platform-specific objects -- Ability to modify attendees for an event -- Ability to create reminders for events expressed in minutes before the event starts - -## 0.2.2 19th August 2019 - -- Add support for specifying the location of an event. Thanks to [oli06](https://github.com/oli06) and [zemanux](https://github.com/zemanux) for submitting PRs to add the functionality to iOS and Android respectively - -## 0.2.1+1 5th August 2019 - -- Fixing date in changelog for version 0.2.1 - -## 0.2.1 5th August 2019 - -- [Android] Fixes issue [101](https://github.com/builttoroam/device_calendar/issues/101) where plugin results in a crash with headless execution - -## 0.2.0 30th July 2019 - -- **BREAKING CHANGE** [Android] Updated to use Gradle plugin to 3.4.2, Gradle wrapper to 5.1.1, Kotlin version to 1.3.41 and bumped Android dependencies -- Add initial support for recurring events. Note that currently editing or deleting a recurring event will affect all instances of it. Future releases will look at supporting more advanced recurrence rules -- Remove old example app to avoid confusion - -## 0.1.3 5th July 2019 - -- [iOS] Fixes issue [94](https://github.com/builttoroam/device_calendar/issues/94) that occurred on 32-bit iOS devices around date of events. Thanks to the PR submitted by [duzenko](https://github.com/duzenko) - -## 0.1.2+2 28th May 2019 - -- Non-functional release. Minor refactoring in Android code to address issues found in Codefactor and fix build status badge in README - -## 0.1.2+1 17th May 2019 - -- Non-functional release. Fixed formatting in changelog and code comments -- Added more info about potential issues in consuming the plugin within an Objective-C project - -## 0.1.2 - 16th May 2019 - -- [Android] An updated fix to address issue [79](https://github.com/builttoroam/device_calendar/issues/79), thanks to the PR submitted by [Gerry High](https://github.com/gerryhigh) - -## 0.1.1 - 1st March 2019 - -- Fixed issue [79](https://github.com/builttoroam/device_calendar/issues/79) where on Android, the plugin was indicating that it was handling permissions that it shouldn't have - -## 0.1.0 - 26th February 2019 - -- **BREAKING CHANGE** Migrated to the plugin to use AndroidX instead of the deprecated Android support libraries. Please ensure you have migrated your application following the guide [here](https://developer.android.com/jetpack/androidx/migrate) -- **BREAKING CHANGE** Updated Kotlin to version 1.3.21 -- **BREAKING CHANGE** Updated Gradle plugin to 3.3.1 and distribution to 4.10.2 - -## 0.0.8 - 26th February 2019 - -- This was a breaking change that should've been incremented as minor version update instead of a patch version update. See changelog for 0.1.0 for the details of this update - -## 0.0.7 - 16th November 2018 - -- Fixes issue [##67](https://github.com/builttoroam/device_calendar/issues/67) and [##68](https://github.com/builttoroam/device_calendar/issues/68). Thanks to PR submitted by huzhiren. - -## 0.0.6 - 18th June 2018 - -- [iOS] Fix an issue when adding/updating an event with a null description - -## 0.0.5 - 14th June 2018 - -- [Android] Fixed an issue with retrieving events by id only - -## 0.0.4 - 12th June 2018 - -- Reordering changelog -- Creating new example for the Pub Dart Example tab -- Moving existing example to the example_app GitHub folder - -## 0.0.2 - 0.0.3 - 7th June 2018 - -- Fixing incorrect Travis build links - -## 0.0.1 - 7th June 2018 - -- Ability to retrieve device calendars -- CRUD operations on calendar events diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 81f1b5da..00000000 --- a/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2018 Built to Roam. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Built to Roam nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 2af1e99d..00000000 --- a/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# Device Calendar Plugin - -[![pub package](https://img.shields.io/pub/v/device_calendar.svg)](https://pub.dartlang.org/packages/device_calendar) ![Pub Version (including pre-releases)](https://img.shields.io/pub/v/device_calendar?include_prereleases&label=Prerelease) [![build](https://github.com/builttoroam/device_calendar/actions/workflows/dart.yml/badge.svg?branch=develop)](https://github.com/builttoroam/device_calendar/actions/workflows/dart.yml) - -A cross platform plugin for modifying calendars on the user's device. - -## Breaking changes at v4 - -* **If you're upgrading from previous versions, your code will need to be modified (slightly), otherwise it will not run after update. See [Timezone support](https://github.com/builttoroam/device_calendar#timezone-support-with-tzdatetime) for more details.** -* **There are some changes to event JSON formats at v4. Pay extra care if you handle event JSONs. Directly calling to and from device calendars should be unaffected.** - -## Features - -* Request permissions to modify calendars on the user's device -* Check if permissions to modify the calendars on the user's device have been granted -* Add or retrieve calendars on the user's device -* Retrieve events associated with a calendar -* Add, update or delete events from a calendar -* Set up, edit or delete recurring events - * **NOTE**: Editing a recurring event will currently edit all instances of it - * **NOTE**: Deleting multiple instances in **Android** takes time to update, you'll see the changes after a few seconds -* Add, modify or remove attendees and receive if an attendee is an organiser for an event -* Setup reminders for an event -* Specify a time zone for event start and end date - * **NOTE**: Due to a limitation of iOS API, single time zone property is used for iOS (`event.startTimeZone`) - * **NOTE**: For the time zone list, please refer to the `TZ database name` column on [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) - -## Timezone support with TZDateTime - -Due to feedback we received, starting from `4.0.0` we will be using the `timezone` package to better handle all timezone data. - -This is already included in this package. However, you need to add this line whenever the package is needed. - -```dart -import 'package:timezone/timezone.dart'; -``` - -If you don't need any timezone specific features in your app, you may use `flutter_native_timezone` to get your devices' current timezone, then convert your previous `DateTime` with it. - -```dart -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; - -initializeTimeZones(); - -// As an example, our default timezone is UTC. -Location _currentLocation = getLocation('Etc/UTC'); - -Future setCurentLocation() async { - String timezone = 'Etc/UTC'; - try { - timezone = await FlutterNativeTimezone.getLocalTimezone(); - } catch (e) { - print('Could not get the local timezone'); - } - _currentLocation = getLocation(timezone); - setLocalLocation(_currentLocation); -} - -... - -event.start = TZDateTime.from(oldDateTime, _currentLocation); -``` - -For other use cases, feedback or future developments on the feature, feel free to open a discussion on GitHub. - -## Null-safety migration - -From `v3.9.0`, device_calendar is null-safe. However, not all workflows have been checked and bugs from older versions still persist. - -You are strongly advised to test your workflow with the new package before shipping. -Better yet, please leave a note for what works and what doesn't, or contribute some bug fixes! - -## Android Integration - -The following will need to be added to the `AndroidManifest.xml` file for your application to indicate permissions to modify calendars are needed - -```xml - - -``` - -### 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()`. - -You may add the following setting to the ProGuard rules file `proguard-rules.pro` (thanks to [Britannio Jarrett](https://github.com/britannio)). Read more about the issue [here](https://github.com/builttoroam/device_calendar/issues/99) - -``` --keep class com.builttoroam.devicecalendar.** { *; } -``` - -See [here](https://github.com/builttoroam/device_calendar/issues/99#issuecomment-612449677) for an example setup. - -For more information, refer to the guide at [Android Developer](https://developer.android.com/studio/build/shrink-code#keep-code) - -### AndroidX migration - -Since `v.1.0`, this version has migrated to use AndroidX instead of the deprecated Android support libraries. When using `0.10.0` and onwards for this plugin, please ensure your application has been migrated following the guide [here](https://developer.android.com/jetpack/androidx/migrate) - -## iOS Integration - -For iOS 10+ support, you'll need to modify the `Info.plist` to add the following key/value pair - -```xml -NSCalendarsUsageDescription -Access most functions for calendar viewing and editing. - -NSContactsUsageDescription -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/analysis_options.yaml b/analysis_options.yaml deleted file mode 100644 index 68a79339..00000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# 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/android/.gitignore b/android/.gitignore deleted file mode 100644 index c6cbe562..00000000 --- a/android/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index 1f5ff500..00000000 --- a/android/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -group 'com.builttoroam.devicecalendar' -version '1.0-SNAPSHOT' - -buildscript { - ext.kotlin_version = '1.6.0' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion 33 - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - defaultConfig { - minSdkVersion 19 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'proguard-rules.pro' - } - lintOptions { - disable 'InvalidPackage' - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - namespace 'com.builttoroam.devicecalendar' -} - -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.12.2' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index 4d3226ab..00000000 --- a/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7a3265ee94c0ab25cf079ac8ccdf87f41d455d42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54708 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

_x$+l{{cH8$W#CT diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3c9d0852..00000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/gradlew b/android/gradlew deleted file mode 100644 index cccdd3d5..00000000 --- a/android/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat deleted file mode 100644 index f9553162..00000000 --- a/android/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro deleted file mode 100644 index d7668e11..00000000 --- a/android/proguard-rules.pro +++ /dev/null @@ -1 +0,0 @@ --keep class com.builttoroam.devicecalendar.** { *; } diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index ef870028..00000000 --- a/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'device_calendar' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml deleted file mode 100644 index 1479c8d6..00000000 --- a/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt deleted file mode 100644 index 5a803a6b..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.builttoroam.devicecalendar - -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) { - return JsonPrimitive(src.name) - } - return JsonObject() - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt deleted file mode 100644 index 2ce608dc..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ /dev/null @@ -1,1267 +0,0 @@ -package com.builttoroam.devicecalendar - -import android.Manifest -import android.annotation.SuppressLint -import android.content.ContentResolver -import android.content.ContentUris -import android.content.ContentValues -import android.content.Context -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.ErrorMessages -import com.builttoroam.devicecalendar.models.* -import com.builttoroam.devicecalendar.models.Calendar -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -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.RecurrenceRule.WeekdayNum -import java.util.* -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()) - - init { - val gsonBuilder = GsonBuilder() - 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 - - if (!_cachedParametersMap.containsKey(requestCode)) { - // this plugin doesn't handle this request code - return false - } - - 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 - - try { - if (!permissionGranted) { - finishWithError( - EC.NOT_AUTHORIZED, - EM.NOT_AUTHORIZED_MESSAGE, - cachedValues.pendingChannelResult - ) - return false - } - - when (cachedValues.calendarDelegateMethodCode) { - RETRIEVE_CALENDARS_REQUEST_CODE -> { - retrieveCalendars(cachedValues.pendingChannelResult) - } - RETRIEVE_EVENTS_REQUEST_CODE -> { - 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 - ) - } - DELETE_EVENT_REQUEST_CODE -> { - deleteEvent( - cachedValues.calendarId, - cachedValues.eventId, - cachedValues.pendingChannelResult - ) - } - REQUEST_PERMISSIONS_REQUEST_CODE -> { - finishWithSuccess(permissionGranted, cachedValues.pendingChannelResult) - } - DELETE_CALENDAR_REQUEST_CODE -> { - deleteCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) - } - } - - return true - } finally { - _cachedParametersMap.remove(cachedValues.calendarDelegateMethodCode) - } - } - - fun requestPermissions(pendingChannelResult: MethodChannel.Result) { - if (arePermissionsGranted()) { - finishWithSuccess(true, pendingChannelResult) - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - REQUEST_PERMISSIONS_REQUEST_CODE - ) - requestPermissions(parameters) - } - } - - fun hasPermissions(pendingChannelResult: MethodChannel.Result) { - finishWithSuccess(arePermissionsGranted(), pendingChannelResult) - } - - @SuppressLint("MissingPermission") - fun retrieveCalendars(pendingChannelResult: MethodChannel.Result) { - if (arePermissionsGranted()) { - val contentResolver: ContentResolver? = _context?.contentResolver - val uri: Uri = CalendarContract.Calendars.CONTENT_URI - val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query(uri, Cst.CALENDAR_PROJECTION, null, null, null) - } else { - contentResolver?.query(uri, Cst.CALENDAR_PROJECTION_OLDER_API, null, null, null) - } - val calendars: MutableList = mutableListOf() - try { - while (cursor?.moveToNext() == true) { - val calendar = parseCalendarRow(cursor) ?: continue - calendars.add(calendar) - } - - finishWithSuccess(_gson?.toJson(calendars), pendingChannelResult) - } catch (e: Exception) { - finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) - } finally { - cursor?.close() - } - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - RETRIEVE_CALENDARS_REQUEST_CODE - ) - requestPermissions(parameters) - } - } - - 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( - EC.INVALID_ARGUMENT, - EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, - pendingChannelResult - ) - } - return null - } - - val contentResolver: ContentResolver? = _context?.contentResolver - val uri: Uri = CalendarContract.Calendars.CONTENT_URI - - val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query( - ContentUris.withAppendedId(uri, calendarIdNumber), - Cst.CALENDAR_PROJECTION, - null, - null, - null - ) - } else { - contentResolver?.query( - ContentUris.withAppendedId(uri, calendarIdNumber), - Cst.CALENDAR_PROJECTION_OLDER_API, - null, - null, - null - ) - } - - try { - if (cursor?.moveToFirst() == true) { - val calendar = parseCalendarRow(cursor) - if (isInternalCall) { - return calendar - } else { - finishWithSuccess(_gson?.toJson(calendar), pendingChannelResult) - } - } else { - if (!isInternalCall) { - finishWithError( - EC.NOT_FOUND, - "The calendar with the ID $calendarId could not be found", - pendingChannelResult - ) - } - } - } catch (e: Exception) { - finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) - } finally { - cursor?.close() - } - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - RETRIEVE_CALENDAR_REQUEST_CODE, - calendarId - ) - requestPermissions(parameters) - } - - return null - } - - fun deleteCalendar( - calendarId: String, - pendingChannelResult: MethodChannel.Result, - isInternalCall: Boolean = false - ): Calendar? { - if (isInternalCall || arePermissionsGranted()) { - val calendarIdNumber = calendarId.toLongOrNull() - if (calendarIdNumber == null) { - if (!isInternalCall) { - 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 deleteSucceeded = contentResolver?.delete(calenderUriWithId, null, null) ?: 0 - finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) - } else { - if (!isInternalCall) { - 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 - ) - requestPermissions(parameters) - } - - return null - } - - 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() - 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.OWNER_ACCOUNT, localAccountName) - 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 - val calendarId = java.lang.Long.parseLong(result?.lastPathSegment!!) - - finishWithSuccess(calendarId.toString(), pendingChannelResult) - } - - fun retrieveEvents( - calendarId: String, - startDate: Long?, - endDate: Long?, - eventIds: List, - pendingChannelResult: MethodChannel.Result - ) { - if (startDate == null && endDate == null && eventIds.isEmpty()) { - 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( - EC.NOT_FOUND, - "Couldn't retrieve the Calendar with ID $calendarId", - pendingChannelResult - ) - return - } - - val contentResolver: ContentResolver? = _context?.contentResolver - val eventsUriBuilder = CalendarContract.Instances.CONTENT_URI.buildUpon() - ContentUris.appendId(eventsUriBuilder, startDate ?: Date(0).time) - ContentUris.appendId(eventsUriBuilder, endDate ?: Date(Long.MAX_VALUE).time) - - val eventsUri = eventsUriBuilder.build() - val eventsCalendarQuery = "(${Events.CALENDAR_ID} = $calendarId)" - val eventsNotDeletedQuery = "(${Events.DELETED} != 1)" - val eventsIdsQuery = - "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" - - var eventsSelectionQuery = "$eventsCalendarQuery AND $eventsNotDeletedQuery" - if (eventIds.isNotEmpty()) { - eventsSelectionQuery += " AND ($eventsIdsQuery)" - } - val eventsSortOrder = Events.DTSTART + " DESC" - - val eventsCursor = contentResolver?.query( - eventsUri, - Cst.EVENT_PROJECTION, - eventsSelectionQuery, - null, - eventsSortOrder - ) - - val events: MutableList = mutableListOf() - - val exceptionHandler = CoroutineExceptionHandler { _, exception -> - uiThreadHandler.post { - finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) - } - } - - GlobalScope.launch(Dispatchers.IO + exceptionHandler) { - while (eventsCursor?.moveToNext() == true) { - val event = parseEvent(calendarId, eventsCursor) ?: continue - events.add(event) - } - for (event in events) { - val attendees = retrieveAttendees(calendar, event.eventId!!, contentResolver) - event.organizer = - attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } - event.attendees = attendees - event.reminders = retrieveReminders(event.eventId!!, contentResolver) - } - }.invokeOnCompletion { cause -> - eventsCursor?.close() - if (cause == null) { - uiThreadHandler.post { - finishWithSuccess(_gson?.toJson(events), pendingChannelResult) - } - } - } - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - RETRIEVE_EVENTS_REQUEST_CODE, - calendarId, - startDate, - endDate - ) - requestPermissions(parameters) - } - - return - } - - fun createOrUpdateEvent( - calendarId: String, - event: Event?, - pendingChannelResult: MethodChannel.Result - ) { - if (arePermissionsGranted()) { - if (event == null) { - finishWithError( - EC.GENERIC_ERROR, - EM.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, - pendingChannelResult - ) - return - } - - val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) - if (calendar == null) { - finishWithError( - EC.NOT_FOUND, - "Couldn't retrieve the Calendar with ID $calendarId", - pendingChannelResult - ) - return - } - - val contentResolver: ContentResolver? = _context?.contentResolver - val values = buildEventContentValues(event, calendarId) - - val exceptionHandler = CoroutineExceptionHandler { _, exception -> - uiThreadHandler.post { - finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) - } - } - - val job: Job - var eventId: Long? = event.eventId?.toLongOrNull() - if (eventId == null) { - val uri = contentResolver?.insert(Events.CONTENT_URI, values) - // get the event ID that is the last element in the Uri - eventId = java.lang.Long.parseLong(uri?.lastPathSegment!!) - job = GlobalScope.launch(Dispatchers.IO + exceptionHandler) { - insertAttendees(event.attendees, eventId, contentResolver) - insertReminders(event.reminders, eventId, contentResolver) - } - } 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 - for (attendeeToDelete in attendeesToDelete) { - deleteAttendee(eventId, attendeeToDelete, contentResolver) - } - - val attendeesToInsert = - event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } - insertAttendees(attendeesToInsert, eventId, contentResolver) - deleteExistingReminders(contentResolver, eventId) - insertReminders(event.reminders, eventId, contentResolver!!) - - val existingSelfAttendee = existingAttendees.firstOrNull { - it.emailAddress == calendar.ownerAccount - } - val newSelfAttendee = event.attendees.firstOrNull { - it.emailAddress == calendar.ownerAccount - } - if (existingSelfAttendee != null && newSelfAttendee != null && - newSelfAttendee.attendanceStatus != null && - existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus - ) { - updateAttendeeStatus(eventId, newSelfAttendee, contentResolver) - } - } - } - job.invokeOnCompletion { cause -> - if (cause == null) { - uiThreadHandler.post { - finishWithSuccess(eventId.toString(), pendingChannelResult) - } - } - } - } else { - 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 - ) - ) - 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) - } - if (reminderUri != null) { - contentResolver?.delete(reminderUri, null, null) - } - } - cursor?.close() - } - - @SuppressLint("MissingPermission") - private fun insertReminders( - reminders: List, - eventId: Long?, - contentResolver: ContentResolver - ) { - if (reminders.isEmpty()) { - return - } - val remindersContentValues = reminders.map { - ContentValues().apply { - put(CalendarContract.Reminders.EVENT_ID, eventId) - put(CalendarContract.Reminders.MINUTES, it.minutes) - put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) - } - }.toTypedArray() - contentResolver.bulkInsert(CalendarContract.Reminders.CONTENT_URI, remindersContentValues) - } - - private fun buildEventContentValues(event: Event, calendarId: String): ContentValues { - val values = ContentValues() - - 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.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.AVAILABILITY, getAvailability(event.availability)) - 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 - } - - private fun getTimeZone(timeZoneString: String?): TimeZone { - val deviceTimeZone: TimeZone = java.util.Calendar.getInstance().timeZone - var timeZone = TimeZone.getTimeZone(timeZoneString ?: deviceTimeZone.id) - - // Invalid time zone names defaults to GMT so update that to be device's time zone - if (timeZone.id == "GMT" && timeZoneString != "GMT") { - timeZone = TimeZone.getTimeZone(deviceTimeZone.id) - } - - return timeZone - } - - private fun getAvailability(availability: Availability?): Int? = when (availability) { - Availability.BUSY -> Events.AVAILABILITY_BUSY - Availability.FREE -> Events.AVAILABILITY_FREE - Availability.TENTATIVE -> Events.AVAILABILITY_TENTATIVE - else -> null - } - - private fun getEventStatus(eventStatus: EventStatus?): Int? = when (eventStatus) { - EventStatus.CONFIRMED -> Events.STATUS_CONFIRMED - EventStatus.TENTATIVE -> Events.STATUS_TENTATIVE - EventStatus.CANCELED -> Events.STATUS_CANCELED - else -> null - } - - @SuppressLint("MissingPermission") - private fun insertAttendees( - attendees: List, - eventId: Long?, - contentResolver: ContentResolver? - ) { - if (attendees.isEmpty()) { - return - } - - val attendeesValues = attendees.map { - ContentValues().apply { - put(CalendarContract.Attendees.ATTENDEE_NAME, it.name) - put(CalendarContract.Attendees.ATTENDEE_EMAIL, it.emailAddress) - put( - CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, - CalendarContract.Attendees.RELATIONSHIP_ATTENDEE - ) - put(CalendarContract.Attendees.ATTENDEE_TYPE, it.role) - put( - CalendarContract.Attendees.ATTENDEE_STATUS, - it.attendanceStatus - ) - put(CalendarContract.Attendees.EVENT_ID, eventId) - } - }.toTypedArray() - - contentResolver?.bulkInsert(CalendarContract.Attendees.CONTENT_URI, attendeesValues) - } - - @SuppressLint("MissingPermission") - 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 + " = ?)" - 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 - ) - } - - 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( - EC.NOT_FOUND, - "The calendar with the ID $calendarId could not be found", - pendingChannelResult - ) - return - } - - if (existingCal.isReadOnly) { - finishWithError( - EC.NOT_ALLOWED, - "Calendar with ID $calendarId is read-only", - pendingChannelResult - ) - return - } - - val eventIdNumber = eventId.toLongOrNull() - if (eventIdNumber == null) { - finishWithError( - EC.INVALID_ARGUMENT, - EM.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, - pendingChannelResult - ) - return - } - - val contentResolver: ContentResolver? = _context?.contentResolver - if (startDate == null && endDate == null && followingInstances == null) { // Delete all instances - val eventsUriWithId = ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) - val deleteSucceeded = contentResolver?.delete(eventsUriWithId, null, null) ?: 0 - finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) - } else { - if (!followingInstances!!) { // Only this instance - val exceptionUriWithId = - ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) - val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query( - contentResolver, - Cst.EVENT_INSTANCE_DELETION, - startDate!!, - endDate!! - ) - - while (instanceCursor.moveToNext()) { - val foundEventID = - instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) - - if (eventIdNumber == foundEventID) { - values.put( - Events.ORIGINAL_INSTANCE_TIME, - instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_BEGIN_INDEX) - ) - values.put(Events.STATUS, Events.STATUS_CANCELED) - } - } - - val deleteSucceeded = contentResolver?.insert(exceptionUriWithId, values) - instanceCursor.close() - finishWithSuccess(deleteSucceeded != null, pendingChannelResult) - } else { // This and following instances - val eventsUriWithId = - ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) - val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query( - contentResolver, - Cst.EVENT_INSTANCE_DELETION, - startDate!!, - endDate!! - ) - - while (instanceCursor.moveToNext()) { - val foundEventID = - instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) - - if (eventIdNumber == foundEventID) { - 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, - Cst.EVENT_INSTANCE_DELETION, - startDate, - lastDate - ) - while (cursor.moveToNext()) { - 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, - Cst.EVENT_INSTANCE_DELETION, - startDate - DateUtils.YEAR_IN_MILLIS, - startDate - 1 - ) - var lastRecurrenceDate: Long? = null - - while (cursor.moveToNext()) { - if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { - lastRecurrenceDate = - cursor.getLong(Cst.EVENT_INSTANCE_DELETION_END_INDEX) - } - } - - if (lastRecurrenceDate != null) { - newRule.until = DateTime(lastRecurrenceDate) - } else { - newRule.until = DateTime(startDate - 1) - } - cursor.close() - } - - values.put(Events.RRULE, newRule.toString()) - contentResolver?.update(eventsUriWithId, values, null, null) - finishWithSuccess(true, pendingChannelResult) - } - } - instanceCursor.close() - } - } - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - DELETE_EVENT_REQUEST_CODE, - calendarId - ) - parameters.eventId = eventId - requestPermissions(parameters) - } - } - - private fun arePermissionsGranted(): Boolean { - if (atLeastAPI(23) && _binding != null) { - val writeCalendarPermissionGranted = _binding!!.activity.checkSelfPermission(Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED - val readCalendarPermissionGranted = _binding!!.activity.checkSelfPermission(Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED - return writeCalendarPermissionGranted && readCalendarPermissionGranted - } - - return true - } - - private fun requestPermissions(parameters: CalendarMethodsParametersCacheModel) { - val requestCode: Int = generateUniqueRequestCodeAndCacheParameters(parameters) - requestPermissions(requestCode) - } - - private fun requestPermissions(requestCode: Int) { - if (atLeastAPI(23)) { - _binding!!.activity.requestPermissions( - arrayOf( - Manifest.permission.WRITE_CALENDAR, - Manifest.permission.READ_CALENDAR - ), requestCode - ) - } - } - - private fun parseCalendarRow(cursor: Cursor?): Calendar? { - if (cursor == null) { - return null - } - - 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(), - displayName, - calendarColor, - accountName, - accountType, - ownerAccount - ) - - calendar.isReadOnly = isCalendarReadOnly(accessLevel) - if (atLeastAPI(17)) { - val isPrimary = cursor.getString(Cst.CALENDAR_PROJECTION_IS_PRIMARY_INDEX) - calendar.isDefault = isPrimary == "1" - } else { - calendar.isDefault = false - } - return calendar - } - - private fun parseEvent(calendarId: String, cursor: Cursor?): Event? { - if (cursor == null) { - return null - } - 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 eventColor = cursor.getLong(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) - val event = Event() - event.eventTitle = title ?: "New Event" - event.eventId = eventId.toString() - event.calendarId = calendarId - event.eventDescription = description - event.eventStartDate = begin - event.eventEndDate = end - event.eventAllDay = allDay - event.eventLocation = location - event.eventURL = url - event.recurrenceRule = parseRecurrenceRuleString(recurringRule) - event.eventStartTimeZone = startTimeZone - event.eventEndTimeZone = endTimeZone - event.availability = availability - event.eventStatus = eventStatus - - return event - } - - private fun parseRecurrenceRuleString(recurrenceRuleString: String?): RecurrenceRule? { - if (recurrenceRuleString == null) { - return null - } - val rfcRecurrenceRule = Rrule(recurrenceRuleString) - val frequency = when (rfcRecurrenceRule.freq) { - 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) - - recurrenceRule.count = rfcRecurrenceRule.count - recurrenceRule.interval = rfcRecurrenceRule.interval - - val until = rfcRecurrenceRule.until - if (until != null) { - recurrenceRule.until = formatDateTime(dateTime = until) - } - - 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) - } - - recurrenceRule.bysetpos = rfcRecurrenceRule.getByPart(Rrule.Part.BYSETPOS) - - return recurrenceRule - } - - private fun formatDateTime(dateTime: DateTime): String { - assert(dateTime.year in 0..9999) - - fun twoDigits(n: Int): String { - return if (n < 10) "0$n" else "$n" - } - - 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" - } - - 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? { - if (cursor == null) { - return null - } - - val emailAddress = cursor.getString(Cst.ATTENDEE_EMAIL_INDEX) - - return Attendee( - emailAddress, - 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 - ) - } - - private fun parseReminderRow(cursor: Cursor?): Reminder? { - if (cursor == null) { - return null - } - - return Reminder(cursor.getInt(Cst.REMINDER_MINUTES_INDEX)) - } - - private fun isCalendarReadOnly(accessLevel: Int): Boolean { - return when (accessLevel) { - Events.CAL_ACCESS_CONTRIBUTOR, - Events.CAL_ACCESS_ROOT, - Events.CAL_ACCESS_OWNER, - Events.CAL_ACCESS_EDITOR - -> false - else -> true - } - } - - @SuppressLint("MissingPermission") - 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, - Cst.ATTENDEE_PROJECTION, - attendeesQuery, - null, - null - ) - attendeesCursor.use { cursor -> - if (cursor?.moveToFirst() == true) { - do { - val attendee = parseAttendeeRow(calendar, attendeesCursor) ?: continue - attendees.add(attendee) - } while (cursor.moveToNext()) - } - } - - return attendees - } - - @SuppressLint("MissingPermission") - 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, - Cst.REMINDER_PROJECTION, - remindersQuery, - null, - null - ) - remindersCursor.use { cursor -> - if (cursor?.moveToFirst() == true) { - do { - val reminder = parseReminderRow(remindersCursor) ?: continue - reminders.add(reminder) - } while (cursor.moveToNext()) - } - } - - return reminders - } - - @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 - val uniqueRequestCode: Int = (_cachedParametersMap.keys.maxOrNull() ?: 0) + 1 - parameters.ownCacheKey = uniqueRequestCode - _cachedParametersMap[uniqueRequestCode] = parameters - - return uniqueRequestCode - } - - private fun finishWithSuccess(result: T, pendingChannelResult: MethodChannel.Result) { - pendingChannelResult.success(result) - clearCachedParameters(pendingChannelResult) - } - - 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() - for (cachedParameter in cachedParameters) { - if (_cachedParametersMap.containsKey(cachedParameter.ownCacheKey)) { - _cachedParametersMap.remove(cachedParameter.ownCacheKey) - } - } - } - - private fun atLeastAPI(api: Int): Boolean { - return api <= Build.VERSION.SDK_INT - } - - 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.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.wkst != null) { - rr.weekStart = Weekday.valueOf(recurrenceRule.wkst!!) - } - - if (recurrenceRule.byday != null) { - rr.byDayPart = recurrenceRule.byday?.mapNotNull { - WeekdayNum.valueOf(it) - }?.toMutableList() - } - - if (recurrenceRule.bymonthday != null) { - rr.setByPart(Rrule.Part.BYMONTHDAY, recurrenceRule.bymonthday!!) - } - - if (recurrenceRule.byyearday != null) { - rr.setByPart(Rrule.Part.BYYEARDAY, recurrenceRule.byyearday!!) - } - - 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) - } - rr.setByPart(Rrule.Part.BYMONTH, newMonth) - } - - if (recurrenceRule.bysetpos != null) { - rr.setByPart(Rrule.Part.BYSETPOS, recurrenceRule.bysetpos!!) - } - return rr.toString() - } - - 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) { - Events.AVAILABILITY_BUSY -> Availability.BUSY - Events.AVAILABILITY_FREE -> Availability.FREE - Events.AVAILABILITY_TENTATIVE -> Availability.TENTATIVE - else -> null - } - - private fun parseEventStatus(status: Int): EventStatus? = when(status) { - Events.STATUS_CONFIRMED -> EventStatus.CONFIRMED - Events.STATUS_CANCELED -> EventStatus.CANCELED - Events.STATUS_TENTATIVE -> EventStatus.TENTATIVE - else -> null - } -} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt deleted file mode 100644 index c1f14533..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ /dev/null @@ -1,301 +0,0 @@ -package com.builttoroam.devicecalendar - -import android.app.Activity -import android.content.Context -import androidx.annotation.NonNull -import com.builttoroam.devicecalendar.common.Constants -import com.builttoroam.devicecalendar.models.* -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -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 org.dmfs.rfc5545.recur.Freq - -const val CHANNEL_NAME = "plugins.builttoroam.com/device_calendar" - -// 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 - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel: MethodChannel - private var context: Context? = null - private var activity: Activity? = null - - private lateinit var _calendarDelegate: CalendarDelegate - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) - channel.setMethodCallHandler(this) - _calendarDelegate = CalendarDelegate(null, context!!) - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activity = binding.activity - _calendarDelegate = CalendarDelegate(binding, context!!) - binding.addRequestPermissionsResultListener(_calendarDelegate) - } - - override fun onDetachedFromActivityForConfigChanges() { - activity = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activity = binding.activity - _calendarDelegate = CalendarDelegate(binding, context!!) - binding.addRequestPermissionsResultListener(_calendarDelegate) - } - - override fun onDetachedFromActivity() { - activity = null - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - REQUEST_PERMISSIONS_METHOD -> { - _calendarDelegate.requestPermissions(result) - } - HAS_PERMISSIONS_METHOD -> { - _calendarDelegate.hasPermissions(result) - } - RETRIEVE_CALENDARS_METHOD -> { - _calendarDelegate.retrieveCalendars(result) - } - RETRIEVE_EVENTS_METHOD -> { - val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - 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 -> { - val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - val eventId = call.argument(EVENT_ID_ARGUMENT) - - _calendarDelegate.deleteEvent(calendarId!!, eventId!!, result) - } - DELETE_EVENT_INSTANCE_METHOD -> { - val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - val eventId = call.argument(EVENT_ID_ARGUMENT) - val startDate = call.argument(EVENT_START_DATE_ARGUMENT) - val endDate = call.argument(EVENT_END_DATE_ARGUMENT) - val followingInstances = call.argument(FOLLOWING_INSTANCES) - - _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 - ) - } - DELETE_CALENDAR_METHOD -> { - val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - _calendarDelegate.deleteCalendar(calendarId!!, result) - } - else -> { - result.notImplemented() - } - } - } - - private fun parseEventArgs(call: MethodCall, calendarId: String?): Event { - val event = Event() - event.eventTitle = call.argument(EVENT_TITLE_ARGUMENT) - event.calendarId = calendarId - event.eventId = call.argument(EVENT_ID_ARGUMENT) - event.eventDescription = call.argument(EVENT_DESCRIPTION_ARGUMENT) - event.eventAllDay = call.argument(EVENT_ALL_DAY_ARGUMENT) ?: false - event.eventStartDate = call.argument(EVENT_START_DATE_ARGUMENT)!! - event.eventEndDate = call.argument(EVENT_END_DATE_ARGUMENT)!! - event.eventStartTimeZone = call.argument(EVENT_START_TIMEZONE_ARGUMENT) - event.eventEndTimeZone = call.argument(EVENT_END_TIMEZONE_ARGUMENT) - event.eventLocation = call.argument(EVENT_LOCATION_ARGUMENT) - event.eventURL = call.argument(EVENT_URL_ARGUMENT) - 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 - ) { - val recurrenceRule = parseRecurrenceRuleArgs(call) - event.recurrenceRule = recurrenceRule - } - - 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( - attendeeArgs[EMAIL_ADDRESS_ARGUMENT] as String, - attendeeArgs[NAME_ARGUMENT] as String?, - attendeeArgs[ROLE_ARGUMENT] as Int, - attendeeArgs[ATTENDANCE_STATUS_ARGUMENT] as Int?, - null, 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 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(UNTIL_ARGUMENT)) { - recurrenceRule.until = recurrenceRuleArgs[UNTIL_ARGUMENT] as String? - } - - if (recurrenceRuleArgs.containsKey(BY_WEEK_DAYS_ARGUMENT)) { - recurrenceRule.byday = - recurrenceRuleArgs[BY_WEEK_DAYS_ARGUMENT].toListOf()?.toMutableList() - } - - if (recurrenceRuleArgs.containsKey(BY_MONTH_DAYS_ARGUMENT)) { - recurrenceRule.bymonthday = - recurrenceRuleArgs[BY_MONTH_DAYS_ARGUMENT] as MutableList? - } - - if (recurrenceRuleArgs.containsKey(BY_YEAR_DAYS_ARGUMENT)) { - recurrenceRule.byyearday = - recurrenceRuleArgs[BY_YEAR_DAYS_ARGUMENT] as MutableList? - } - - 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 - } - - private inline fun Any?.toListOf(): List? { - return (this as List<*>?)?.filterIsInstance()?.toList() - } - - private fun parseAvailability(value: String?): Availability? = - if (value == null || value == Constants.AVAILABILITY_UNAVAILABLE) { - null - } else { - Availability.valueOf(value) - } - - private fun parseEventStatus(value: String?): EventStatus? = - if (value == null || value == Constants.EVENT_STATUS_NONE) { - null - } else { - EventStatus.valueOf(value) - } -} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt deleted file mode 100644 index 4883b7a7..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.builttoroam.devicecalendar - -import com.builttoroam.devicecalendar.models.EventStatus -import com.google.gson.* -import java.lang.reflect.Type - -class EventStatusSerializer: JsonSerializer { - override fun serialize(src: EventStatus?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { - return JsonPrimitive(src.name) - } - 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 deleted file mode 100644 index 3e5396e3..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.builttoroam.devicecalendar.common - -import android.provider.CalendarContract - -class Constants { - companion object { - const val CALENDAR_PROJECTION_ID_INDEX: Int = 0 - const val CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX: Int = 1 - const val CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX: Int = 2 - const val CALENDAR_PROJECTION_DISPLAY_NAME_INDEX: Int = 3 - const val CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX: Int = 4 - const val CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX: Int = 5 - const val CALENDAR_PROJECTION_COLOR_INDEX: Int = 6 - const val CALENDAR_PROJECTION_IS_PRIMARY_INDEX: Int = 7 - - // 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 - - ) - - // 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 - ) - - const val EVENT_PROJECTION_ID_INDEX: Int = 0 - const val EVENT_PROJECTION_TITLE_INDEX: Int = 1 - const val EVENT_PROJECTION_DESCRIPTION_INDEX: Int = 2 - const val EVENT_PROJECTION_BEGIN_INDEX: Int = 3 - const val EVENT_PROJECTION_END_INDEX: Int = 4 - const val EVENT_PROJECTION_RECURRING_RULE_INDEX: Int = 7 - const val EVENT_PROJECTION_ALL_DAY_INDEX: Int = 8 - const val EVENT_PROJECTION_EVENT_LOCATION_INDEX: Int = 9 - const val EVENT_PROJECTION_CUSTOM_APP_URI_INDEX: Int = 10 - const val EVENT_PROJECTION_START_TIMEZONE_INDEX: Int = 11 - const val EVENT_PROJECTION_END_TIMEZONE_INDEX: Int = 12 - 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, - CalendarContract.Events.TITLE, - CalendarContract.Events.DESCRIPTION, - CalendarContract.Instances.BEGIN, - CalendarContract.Instances.END, - CalendarContract.Instances.DURATION, - CalendarContract.Events.RDATE, - CalendarContract.Events.RRULE, - CalendarContract.Events.ALL_DAY, - CalendarContract.Events.EVENT_LOCATION, - CalendarContract.Events.CUSTOM_APP_URI, - CalendarContract.Events.EVENT_TIMEZONE, - CalendarContract.Events.EVENT_END_TIMEZONE, - CalendarContract.Events.AVAILABILITY, - CalendarContract.Events.STATUS, - //##### - CalendarContract.Events.EVENT_COLOR, - ) - - const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 - const val EVENT_INSTANCE_DELETION_RRULE_INDEX: Int = 1 - const val EVENT_INSTANCE_DELETION_LAST_DATE_INDEX: Int = 2 - const val EVENT_INSTANCE_DELETION_BEGIN_INDEX: Int = 3 - 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 - ) - - const val ATTENDEE_ID_INDEX: Int = 0 - const val ATTENDEE_EVENT_ID_INDEX: Int = 1 - const val ATTENDEE_NAME_INDEX: Int = 2 - const val ATTENDEE_EMAIL_INDEX: Int = 3 - const val ATTENDEE_TYPE_INDEX: Int = 4 - const val ATTENDEE_RELATIONSHIP_INDEX: Int = 5 - 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 - ) - - const val REMINDER_MINUTES_INDEX = 1 - val REMINDER_PROJECTION: Array = arrayOf( - CalendarContract.Reminders.EVENT_ID, - CalendarContract.Reminders.MINUTES - ) - - const val AVAILABILITY_UNAVAILABLE = "UNAVAILABLE" - - const val EVENT_STATUS_NONE = "NONE" - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt deleted file mode 100644 index 3509ad11..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.builttoroam.devicecalendar.common - -class ErrorCodes { - companion object { - const val INVALID_ARGUMENT: String = "400" - const val NOT_FOUND: String = "404" - const val NOT_ALLOWED: String = "405" - const val NOT_AUTHORIZED: String = "401" - const val GENERIC_ERROR: String = "500" - } -} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt deleted file mode 100644 index e8486baa..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt +++ /dev/null @@ -1,16 +0,0 @@ -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)" - } -} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt deleted file mode 100644 index 825ca964..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt +++ /dev/null @@ -1,10 +0,0 @@ -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 diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt deleted file mode 100644 index 0ac7faa1..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.builttoroam.devicecalendar.models - -enum class Availability { - BUSY, - FREE, - TENTATIVE -} \ 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 deleted file mode 100644 index 6e10b7fe..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ /dev/null @@ -1,13 +0,0 @@ -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? -) { - var isReadOnly: Boolean = false - var isDefault: Boolean = false -} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt deleted file mode 100644 index 22bb4c4b..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt +++ /dev/null @@ -1,16 +0,0 @@ -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 -) { - 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 deleted file mode 100644 index 749a858a..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.builttoroam.devicecalendar.models - -class Event { - var eventTitle: String? = null - var eventId: String? = null - var calendarId: String? = null - var eventDescription: String? = null - var eventStartDate: Long? = null - var eventEndDate: Long? = null - var eventStartTimeZone: String? = null - var eventEndTimeZone: String? = null - var eventAllDay: Boolean = false - var eventLocation: String? = null - var eventURL: String? = null - var attendees: MutableList = mutableListOf() - var recurrenceRule: RecurrenceRule? = null - var organizer: Attendee? = null - var reminders: MutableList = mutableListOf() - var availability: Availability? = null - var eventStatus: EventStatus? = null -// ##### - var eventColor: Long? = null - -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt deleted file mode 100644 index c8422795..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.builttoroam.devicecalendar.models - -enum class EventStatus { - CONFIRMED, - CANCELED, - TENTATIVE -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt deleted file mode 100644 index 1da83111..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.builttoroam.devicecalendar.models - -import org.dmfs.rfc5545.recur.Freq - -class RecurrenceRule(val freq: Freq) { - var count: Int? = null - var interval: 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/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt deleted file mode 100644 index c9695796..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.builttoroam.devicecalendar.models - -class Reminder(val minutes: Int) \ No newline at end of file diff --git a/device_calendar.iml b/device_calendar.iml deleted file mode 100644 index 73e7ebd0..00000000 --- a/device_calendar.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/device_calendar_android.iml b/device_calendar_android.iml deleted file mode 100644 index ac5d744d..00000000 --- a/device_calendar_android.iml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/.gitignore b/example/.gitignore deleted file mode 100644 index dee655cc..00000000 --- a/example/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.DS_Store -.dart_tool/ - -.packages -.pub/ - -build/ - -.flutter-plugins diff --git a/example/.metadata b/example/.metadata deleted file mode 100644 index 8cab361b..00000000 --- a/example/.metadata +++ /dev/null @@ -1,8 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 44b7e7d3f42f050a79712daab253af06e9daf530 - channel: beta diff --git a/example/README.md b/example/README.md deleted file mode 100644 index a24543bc..00000000 --- a/example/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# Examples - -Most of the APIs are covered in [calendar_event.dart](https://github.com/builttoroam/device_calendar/blob/master/example/lib/presentation/pages/calendar_event.dart) or [calendar_events.dart](https://github.com/builttoroam/device_calendar/blob/master/example/lib/presentation/pages/calendar_events.dart) files in the example app. -You'll be able to get a reference of how the APIs are used. - -For a full API reference, the documentation can be found at [pub.dev](https://pub.dev/documentation/device_calendar/latest/device_calendar/device_calendar-library.html). - -## DayOfWeekGroup Enum - -`DayOfWeekGroup` enum allows to explicitly choose and return a list of `DayOfWeek` enum values by using an extension `getDays`: - -* `DayOfWeekGroup.Weekday.getDays` will return: - - ```dart - [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday]; - ``` - -* `DayOfWeekGroup.Weekend.getDays` will return: - - ```dart - [DayOfWeek.Saturday, DayOfWeek.Sunday]; - ``` - -* `DayOfWeekGroup.Alldays.getDays` will return: - - ```dart - [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday]; - ``` - -## Attendee Examples - -Examples below present on how to initialise an `Attendee` model in Dart: - -* A required attendee: - - ```dart - Attendee( - name: 'Test User 1', - emailAddress: 'test1@example.com', - role: AttendeeRole.Required); - ``` - -* An optional attendee: - - ```dart - Attendee( - name: 'Test User 2', - emailAddress: 'test2@example.com', - role: AttendeeRole.Optional); - ``` - -## Reminder Examples - -Examples below present on how to initialise a `Reminder` model in Dart: - -* 30 minutes - - ```dart - Reminder(minutes: 30); - ``` - -* 1 day - - ```dart - Reminder(minutes: 1440); - ``` - -## Recurrence Rule Examples - -Examples below present sample parameters of recurrence rules received by each platform and required properties for the `RecurrenceRule` model in Dart.\ -**Please note**: Receiving monthly and yearly recurrence parameters are slightly different for the two platforms. - -You can find more standard examples at [iCalendar.org](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html). - -### **Daily Rule** - -Daily every 5 days and end after 3 occurrences - -* Recurrence parameter example (Android and iOS):\ -`FREQ=DAILY;INTERVAL=5;COUNT=3` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Daily, - interval: 5, - totalOccurrences: 3); - ``` - -### **Weekly Rule** - -Weekly on Monday, Tuesday and Saturday every 2 weeks and end on 31 Jan 2020 - -* Recurrence parameter example (Android and iOS):\ -`FREQ=WEEKLY;BYDAY=MO,TU,SA;INTERVAL=2;UNTIL=20200130T130000Z` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Weekly, - interval: 2, - endDate: DateTime(2020, 1, 31), - daysOfWeek: [ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Saturday ]); - ``` - -### **Monthly/Yearly SetPosition (Week Number) Rule** - -Monthly on third Thursday - -* Recurrence parameter example (Android):\ -`FREQ=MONTHLY;INTERVAL=1;BYDAY=3TH` -* Recurrence parameter example (iOS):\ -`FREQ=MONTHLY;INTERVAL=1;BYDAY=TH;BYSETPOS=3` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Monthly, - interval: 1, - daysOfWeek: [ DayOfWeek.Thursday ], - weekOfMonth: WeekNumber.Third); - ``` - -Monthly on last Thursday - -* Recurrence parameter example (Android and iOS):\ -`FREQ=MONTHLY;INTERVAL=1;BYDAY=-1TH` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Monthly, - interval: 1, - daysOfWeek: [ DayOfWeek.Thursday ], - weekOfMonth: WeekNumber.Last); - ``` - -Yearly on third Thursday of January - -* Recurrence parameter example (Android and iOS):\ -`FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYDAY=3TH` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Yearly, - interval: 1, - monthOfYear: MonthOfYear.January, - weekOfMonth: WeekNumber.Third); - ``` - -Yearly on last Thursday of January - -* Recurrence parameter example (Android and iOS):\ -`FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYDAY=-1TH` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Yearly, - interval: 1, - monthOfYear: MonthOfYear.January, - weekOfMonth: WeekNumber.Last); - ``` - -### **Monthly/Yearly By Day of a Month Rule** - -Monthly on 8th - -* Recurrence parameter example (Android and iOS):\ -`FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=8` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Monthly, - interval: 1, - dayOfMonth: 8); - ``` - -Yearly on 8th of February - -* Recurrence parameter example (Android):\ -`FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=8;BYMONTH=2` -* Recurrence parameter example (iOS):\ -`FREQ=YEARLY;INTERVAL=1` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Yearly, - interval: 1, - monthOfYear: MonthOfYear.February, - dayOfMonth: 8); - ``` diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml deleted file mode 100644 index 3e1200f9..00000000 --- a/example/analysis_options.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# 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 diff --git a/example/android/.gitignore b/example/android/.gitignore deleted file mode 100644 index 65b7315a..00000000 --- a/example/android/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -*.iml -*.class -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -GeneratedPluginRegistrant.java diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle deleted file mode 100644 index 776dc817..00000000 --- a/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 32 - ndkVersion '22.1.7171670' - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.builttoroam.devicecalendarexample" - minSdkVersion 19 - targetSdkVersion 31 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -} diff --git a/example/android/app/proguard-rules.pro b/example/android/app/proguard-rules.pro deleted file mode 100644 index 77023c9c..00000000 --- a/example/android/app/proguard-rules.pro +++ /dev/null @@ -1 +0,0 @@ --keep class com.builttoroam.devicecalendar.** { *; } \ No newline at end of file diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 630265e9..00000000 --- a/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt b/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt deleted file mode 100644 index 2d71b1d4..00000000 --- a/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.builttoroam.devicecalendarexample - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() { -} diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f8..00000000 --- a/example/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 00fa4417..00000000 --- a/example/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/example/android/build.gradle b/example/android/build.gradle deleted file mode 100644 index d3f65307..00000000 --- a/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - ext.kotlin_version = '1.6.0' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/example/android/gradle.properties b/example/android/gradle.properties deleted file mode 100644 index 946d709d..00000000 --- a/example/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -android.enableJetifier=true -android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536M \ No newline at end of file diff --git a/example/android/gradle/wrapper/gradle-wrapper.jar b/example/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 13372aef5e24af05341d49695ee84e5f9b594659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53636 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ⅅ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 H-+%o-(+5aQ diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 6f5b3ec5..00000000 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Tue Jun 16 16:20:15 AEST 2020 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip diff --git a/example/android/gradlew b/example/android/gradlew deleted file mode 100755 index 9d82f789..00000000 --- a/example/android/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/example/android/gradlew.bat b/example/android/gradlew.bat deleted file mode 100644 index 8a0b282a..00000000 --- a/example/android/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/example/android/settings.gradle b/example/android/settings.gradle deleted file mode 100644 index 5a2f14fb..00000000 --- a/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/example/device_calendar_example.iml b/example/device_calendar_example.iml deleted file mode 100644 index c92516a5..00000000 --- a/example/device_calendar_example.iml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/example/device_calendar_example_android.iml b/example/device_calendar_example_android.iml deleted file mode 100644 index b050030a..00000000 --- a/example/device_calendar_example_android.iml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/example/integration_test/app_test.dart b/example/integration_test/app_test.dart deleted file mode 100644 index 6e4a1908..00000000 --- a/example/integration_test/app_test.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:uuid/uuid.dart'; - -import 'package:device_calendar_example/main.dart' as app; - -/// NOTE: These integration tests are currently made to be run on a physical device where there is at least a calendar that can be written to. -/// Calendar permissions are needed. See example/test_driver/integration_test.dart for how to run this on Android -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('Calendar plugin example', () { - final eventTitle = const Uuid().v1(); - final saveEventButtonFinder = find.byKey(const Key('saveEventButton')); - final eventTitleFinder = find.text(eventTitle); - final firstWritableCalendarFinder = - find.byKey(const Key('writableCalendar0')); - final addEventButtonFinder = find.byKey(const Key('addEventButton')); - final titleFieldFinder = find.byKey(const Key('titleField')); - final deleteButtonFinder = find.byKey(const Key('deleteEventButton')); -//TODO: remove redundant restarts. Currently needed because the first screen is always "test starting..." - testWidgets('starts on calendars page', (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('calendarsPage')), findsOneWidget); - }); - testWidgets('select first writable calendar', (WidgetTester tester) async { - app.main(); - - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - expect(firstWritableCalendarFinder, findsOneWidget); - }); - testWidgets('go to add event page', (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - await tester.tap(firstWritableCalendarFinder); - - await tester.pumpAndSettle(); - expect(addEventButtonFinder, findsOneWidget); - print('found add event button'); - await tester.tap(addEventButtonFinder); - await tester.pumpAndSettle(); - expect(saveEventButtonFinder, findsOneWidget); - }); - testWidgets('try to save event without entering mandatory fields', - (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - await tester.tap(firstWritableCalendarFinder); - await tester.pumpAndSettle(); - await tester.tap(addEventButtonFinder); - - await tester.pumpAndSettle(); - await tester.tap(saveEventButtonFinder); - await tester.pumpAndSettle(); - expect(find.text('Please fix the errors in red before submitting.'), - findsOneWidget); - }); - testWidgets('save event with title $eventTitle', - (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - await tester.tap(firstWritableCalendarFinder); - await tester.pumpAndSettle(); - await tester.tap(addEventButtonFinder); - - await tester.pumpAndSettle(); - await tester.tap(titleFieldFinder); - - await tester.enterText(titleFieldFinder, eventTitle); - await tester.tap(saveEventButtonFinder); - await tester.pumpAndSettle(); - expect(eventTitleFinder, findsOneWidget); - }); - testWidgets('delete event with title $eventTitle', - (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - await tester.tap(firstWritableCalendarFinder); - await tester.pumpAndSettle(); - await tester.tap(eventTitleFinder); - - await tester.scrollUntilVisible(deleteButtonFinder, -5); - await tester.tap(deleteButtonFinder); - await tester.pumpAndSettle(); - expect(eventTitleFinder, findsNothing); - }); - }); -} diff --git a/example/integration_test/integration_test.dart b/example/integration_test/integration_test.dart deleted file mode 100644 index ca6e9ef3..00000000 --- a/example/integration_test/integration_test.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:integration_test/integration_test_driver.dart'; - -/// Instruction for iOS: -/// See `ios.sh` -/// Instruction for android: -/// See `integration_test_android.dart` - -Future main() => integrationDriver(); diff --git a/example/integration_test/integration_test_android.dart b/example/integration_test/integration_test_android.dart deleted file mode 100644 index 2c840da5..00000000 --- a/example/integration_test/integration_test_android.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:io'; - -import 'package:integration_test/integration_test_driver.dart'; - -// make sure 'adb devices' works on your local machine, then from the root of the plugin, run the following: -/* -1. -cd example -2. -flutter drive --driver=integration_test/integration_test.dart --target=integration_test/app_test.dart - */ - -Future main() async { - await Process.run('adb', [ - 'shell', - 'pm', - 'grant', - 'com.builttoroam.devicecalendarexample', - 'android.permission.READ_CALENDAR' - ]); - await Process.run('adb', [ - 'shell', - 'pm', - 'grant', - 'com.builttoroam.devicecalendarexample', - 'android.permission.WRITE_CALENDAR' - ]); - await integrationDriver(); -} diff --git a/example/integration_test/ios.sh b/example/integration_test/ios.sh deleted file mode 100755 index 8f8eede4..00000000 --- a/example/integration_test/ios.sh +++ /dev/null @@ -1,24 +0,0 @@ -# Use: integration_test/ios.sh -# -# Executes the device_calendar integration test for iOS -# This script creates and starts a new iOS simulator, grants calendar permission -# to the app then runs the integration tests and finally deletes the simulator. -# -# Prerequisites: Xcode, Xcode Command Line Tools, Xcode iOS Simulator -# -# To run an integration test, make sure the script has execute permission -# example: `chmod +x example/integration_test/ios.sh` then: -# 1. cd example -# 2. integration_test/ios.sh -# 3. You should see `All tests passed` -# -# Success - "All tests passed." is printed to the console -# -deviceId=$(xcrun simctl create builtToRoamCalendarTest "iPhone 13" 2> /dev/null | tail -1) -echo "Created device: $deviceId, booting..." -xcrun simctl boot $deviceId -xcrun simctl privacy $deviceId grant calendar com.builttoroam.deviceCalendarExample00 -echo "Running tests..." -flutter drive --driver=integration_test/integration_test.dart --target=integration_test/app_test.dart -d $deviceId -echo "Removing device: $deviceId" -xcrun simctl delete $deviceId diff --git a/example/ios/.gitignore b/example/ios/.gitignore deleted file mode 100755 index 1c202be0..00000000 --- a/example/ios/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/app.flx -/Flutter/app.zip -/Flutter/flutter_assets/ -/Flutter/App.framework -/Flutter/Flutter.framework -/Flutter/Generated.xcconfig -/ServiceDefinitions.json - -**/.symlinks/ -Pods/ diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100755 index 9b41e7d8..00000000 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 11.0 - - diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig deleted file mode 100755 index e8efba11..00000000 --- a/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig deleted file mode 100755 index 399e9340..00000000 --- a/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile deleted file mode 100644 index 997d1cb3..00000000 --- a/example/ios/Podfile +++ /dev/null @@ -1,45 +0,0 @@ -# Uncomment this line to define a global platform for your project -platform :ios, '11.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - flutter_additional_ios_build_settings(target) - config.build_settings['SWIFT_VERSION'] = '5.0' - # Or whatever Swift version your app is using that works with your plugins - end - end -end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock deleted file mode 100755 index cb8f159b..00000000 --- a/example/ios/Podfile.lock +++ /dev/null @@ -1,34 +0,0 @@ -PODS: - - device_calendar (0.0.1): - - Flutter - - Flutter (1.0.0) - - flutter_native_timezone (0.0.1): - - Flutter - - integration_test (0.0.1): - - Flutter - -DEPENDENCIES: - - device_calendar (from `.symlinks/plugins/device_calendar/ios`) - - Flutter (from `Flutter`) - - flutter_native_timezone (from `.symlinks/plugins/flutter_native_timezone/ios`) - - integration_test (from `.symlinks/plugins/integration_test/ios`) - -EXTERNAL SOURCES: - device_calendar: - :path: ".symlinks/plugins/device_calendar/ios" - Flutter: - :path: Flutter - flutter_native_timezone: - :path: ".symlinks/plugins/flutter_native_timezone/ios" - integration_test: - :path: ".symlinks/plugins/integration_test/ios" - -SPEC CHECKSUMS: - device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 - integration_test: 13825b8a9334a850581300559b8839134b124670 - -PODFILE CHECKSUM: 10625bdc9b9ef8574174815aabd5b048e6e29bff - -COCOAPODS: 1.11.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 160e1d14..00000000 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,507 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 763C5C3662C48FEF7F2B0120 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E098C60D243A71853922C094 /* Pods_Runner.framework */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2AD784C0989B63BAA46EFDFD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E098C60D243A71853922C094 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F504DA6DD95FB326B0725EEE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 763C5C3662C48FEF7F2B0120 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 0C6DE7144DB7716571BF5210 /* Pods */ = { - isa = PBXGroup; - children = ( - F504DA6DD95FB326B0725EEE /* Pods-Runner.debug.xcconfig */, - 2AD784C0989B63BAA46EFDFD /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 6400E78A7B626A4B08303DA0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - E098C60D243A71853922C094 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 0C6DE7144DB7716571BF5210 /* Pods */, - 6400E78A7B626A4B08303DA0 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - A6EB42BBC2C9005F5277AB4F /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 122A90E1AB6EBE6FCB241A06 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1300; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = PG8Q9ZR89L; - LastSwiftMigration = 1130; - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 122A90E1AB6EBE6FCB241A06 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/device_calendar/device_calendar.framework", - "${BUILT_PRODUCTS_DIR}/flutter_native_timezone/flutter_native_timezone.framework", - "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_calendar.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_timezone.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - A6EB42BBC2C9005F5277AB4F /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = PG8Q9ZR89L; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = PG8Q9ZR89L; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index 919434a6..00000000 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100755 index 14d255fd..00000000 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index 21a3cc14..00000000 --- a/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index 18d98100..00000000 --- a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift deleted file mode 100755 index 70693e4a..00000000 --- a/example/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100755 index d36b1fab..00000000 --- a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100755 index 3d43d11e66f4de3da27ed045ca4fe38ad8b48094..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11112 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 P-1+_A+?hGkL;m(J7kc&K diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100755 index 28c6bf03016f6c994b70f38d1b7346e5831b531f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100755 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100755 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100755 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100755 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100755 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100755 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100755 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100755 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100755 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100755 index 89c2725b..00000000 --- a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100755 index f2e259c7..00000000 --- a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100755 index f3c28516..00000000 --- a/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist deleted file mode 100755 index ad25cf2b..00000000 --- a/example/ios/Runner/Info.plist +++ /dev/null @@ -1,55 +0,0 @@ - - - - - NSContactsUsageDescription - Contacts are used for event attendee editing. - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - device_calendar_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - NSCalendarsUsageDescription - We need access to your calendar to help you track events - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h deleted file mode 100755 index 7335fdf9..00000000 --- a/example/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/example/lib/common/app_routes.dart b/example/lib/common/app_routes.dart deleted file mode 100644 index 991a9d70..00000000 --- a/example/lib/common/app_routes.dart +++ /dev/null @@ -1,3 +0,0 @@ -class AppRoutes { - static const calendars = '/'; -} diff --git a/example/lib/main.dart b/example/lib/main.dart deleted file mode 100644 index 3b5d61ee..00000000 --- a/example/lib/main.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'common/app_routes.dart'; -import 'presentation/pages/calendars.dart'; - -void main() => runApp(const MyApp()); - -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData(), - themeMode: ThemeMode.system, - darkTheme: ThemeData.dark(), - routes: { - AppRoutes.calendars: (context) { - return const CalendarsPage(key: Key('calendarsPage')); - } - }, - ); - } -} diff --git a/example/lib/presentation/date_time_picker.dart b/example/lib/presentation/date_time_picker.dart deleted file mode 100644 index dc11e8d9..00000000 --- a/example/lib/presentation/date_time_picker.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -import 'input_dropdown.dart'; - -class DateTimePicker extends StatelessWidget { - const DateTimePicker( - {Key? key, - this.labelText, - this.selectedDate, - this.selectedTime, - this.selectDate, - this.selectTime, - this.enableTime = true}) - : super(key: key); - - final String? labelText; - final DateTime? selectedDate; - final TimeOfDay? selectedTime; - final ValueChanged? selectDate; - final ValueChanged? selectTime; - final bool enableTime; - - Future _selectDate(BuildContext context) async { - final picked = await showDatePicker( - context: context, - initialDate: selectedDate != null - ? DateTime.parse(selectedDate.toString()) - : DateTime.now(), - firstDate: DateTime(2015, 8), - lastDate: DateTime(2101)); - if (picked != null && picked != selectedDate && selectDate != null) { - selectDate!(picked); - } - } - - Future _selectTime(BuildContext context) async { - if (selectedTime == null) return; - final picked = - await showTimePicker(context: context, initialTime: selectedTime!); - if (picked != null && picked != selectedTime) selectTime!(picked); - } - - @override - Widget build(BuildContext context) { - final valueStyle = Theme.of(context).textTheme.headline6; - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - flex: 4, - child: InputDropdown( - labelText: labelText, - valueText: selectedDate == null - ? '' - : DateFormat.yMMMd().format(selectedDate as DateTime), - valueStyle: valueStyle, - onPressed: () { - _selectDate(context); - }, - ), - ), - if (enableTime) ...[ - const SizedBox(width: 12.0), - Expanded( - flex: 3, - child: InputDropdown( - valueText: selectedTime?.format(context) ?? '', - valueStyle: valueStyle, - onPressed: () { - _selectTime(context); - }, - ), - ), - ] - ], - ); - } -} diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart deleted file mode 100644 index f91bb7de..00000000 --- a/example/lib/presentation/event_item.dart +++ /dev/null @@ -1,342 +0,0 @@ -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 'recurring_event_dialog.dart'; - -class EventItem extends StatefulWidget { - final Event? _calendarEvent; - final DeviceCalendarPlugin _deviceCalendarPlugin; - final bool _isReadOnly; - - final Function(Event) _onTapped; - final VoidCallback _onLoadingStarted; - final Function(bool) _onDeleteFinished; - - const EventItem( - this._calendarEvent, - this._deviceCalendarPlugin, - this._onLoadingStarted, - this._onDeleteFinished, - this._onTapped, - this._isReadOnly, - {Key? key}) - : super(key: key); - - @override - State createState() { - return _EventItemState(); - } -} - -class _EventItemState extends State { - final double _eventFieldNameWidth = 75.0; - Location? _currentLocation; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => setCurentLocation()); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - if (widget._calendarEvent != null) { - widget._onTapped(widget._calendarEvent as Event); - } - }, - child: Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Padding( - padding: EdgeInsets.symmetric(vertical: 10.0), - child: FlutterLogo(), - ), - ListTile( - title: Text(widget._calendarEvent?.title ?? ''), - subtitle: Text(widget._calendarEvent?.description ?? '')), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - if (_currentLocation != null) - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Starts'), - ), - Text( - widget._calendarEvent == null - ? '' - : _formatDateTime( - dateTime: widget._calendarEvent!.start!, - ), - ) - ], - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 5.0), - ), - if (_currentLocation != null) - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Ends'), - ), - Text( - widget._calendarEvent?.end == null - ? '' - : _formatDateTime( - dateTime: widget._calendarEvent!.end!, - ), - ), - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('All day?'), - ), - Text(widget._calendarEvent?.allDay != null && - widget._calendarEvent?.allDay == true - ? 'Yes' - : 'No') - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Location'), - ), - Expanded( - child: Text( - widget._calendarEvent?.location ?? '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('URL'), - ), - Expanded( - child: Text( - widget._calendarEvent?.url?.data?.contentText ?? '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Attendees'), - ), - Expanded( - child: Text( - widget._calendarEvent?.attendees - ?.where((a) => a?.name?.isNotEmpty ?? false) - .map((a) => a?.name) - .join(', ') ?? - '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Availability'), - ), - Expanded( - child: Text( - widget._calendarEvent?.availability.enumToString ?? - '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Status'), - ), - Expanded( - child: Text( - widget._calendarEvent?.status?.enumToString ?? '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - ], - ), - ), - ButtonBar( - children: [ - if (!widget._isReadOnly) ...[ - IconButton( - onPressed: () { - if (widget._calendarEvent != null) { - widget._onTapped(widget._calendarEvent as Event); - } - }, - icon: const Icon(Icons.edit), - ), - IconButton( - onPressed: () async { - await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - if (widget._calendarEvent?.recurrenceRule == null) { - return AlertDialog( - title: const Text( - 'Are you sure you want to delete this event?'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - widget._onLoadingStarted(); - final deleteResult = await widget - ._deviceCalendarPlugin - .deleteEvent( - widget._calendarEvent?.calendarId, - widget._calendarEvent?.eventId); - widget._onDeleteFinished( - deleteResult.isSuccess && - deleteResult.data != null); - }, - child: const Text('Delete'), - ), - ], - ); - } else { - if (widget._calendarEvent == null) { - return const SizedBox(); - } - return RecurringEventDialog( - widget._deviceCalendarPlugin, - widget._calendarEvent!, - widget._onLoadingStarted, - widget._onDeleteFinished); - } - }, - ); - }, - icon: const Icon(Icons.delete), - ), - ] else ...[ - IconButton( - onPressed: () { - if (widget._calendarEvent != null) { - widget._onTapped(widget._calendarEvent!); - } - }, - icon: const Icon(Icons.remove_red_eye), - ), - ] - ], - ) - ], - ), - ), - ); - } - - void setCurentLocation() async { - String? timezone; - try { - timezone = await FlutterNativeTimezone.getLocalTimezone(); - } catch (e) { - debugPrint('Could not get the local timezone'); - } - timezone ??= 'Etc/UTC'; - _currentLocation = timeZoneDatabase.locations[timezone]; - setState(() {}); - } - - /// Formats [dateTime] into a human-readable string. - /// If [_calendarEvent] is an Android allDay event, then the output will - /// omit the time. - String _formatDateTime({DateTime? dateTime}) { - if (dateTime == null) { - return 'Error'; - } - var output = ''; - if (Platform.isAndroid && widget._calendarEvent?.allDay == true) { - // just the dates, no times - output = DateFormat.yMd().format(dateTime); - } else { - output = DateFormat('yyyy-MM-dd HH:mm:ss') - .format(TZDateTime.from(dateTime, _currentLocation!)); - } - return output; - } -} diff --git a/example/lib/presentation/input_dropdown.dart b/example/lib/presentation/input_dropdown.dart deleted file mode 100644 index a6c19820..00000000 --- a/example/lib/presentation/input_dropdown.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class InputDropdown extends StatelessWidget { - const InputDropdown( - {Key? key, - this.child, - this.labelText, - this.valueText, - this.valueStyle, - this.onPressed}) - : super(key: key); - - final String? labelText; - final String? valueText; - final TextStyle? valueStyle; - final VoidCallback? onPressed; - final Widget? child; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onPressed, - child: InputDecorator( - decoration: InputDecoration( - labelText: labelText, - ), - baseStyle: valueStyle, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - if (valueText != null) Text(valueText!, style: valueStyle), - Icon(Icons.arrow_drop_down, - color: Theme.of(context).brightness == Brightness.light - ? Colors.grey.shade700 - : Colors.white70), - ], - ), - ), - ); - } -} diff --git a/example/lib/presentation/pages/calendar_add.dart b/example/lib/presentation/pages/calendar_add.dart deleted file mode 100644 index 7d6d8820..00000000 --- a/example/lib/presentation/pages/calendar_add.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'dart:io'; - -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(); - } -} - -class _CalendarAddPageState extends State { - final GlobalKey _formKey = GlobalKey(); - final GlobalKey _scaffoldKey = GlobalKey(); - late DeviceCalendarPlugin _deviceCalendarPlugin; - - AutovalidateMode _autovalidate = AutovalidateMode.disabled; - String _calendarName = ''; - ColorChoice? _colorChoice; - String _localAccountName = ''; - - _CalendarAddPageState() { - _deviceCalendarPlugin = DeviceCalendarPlugin(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: const Text('Create Calendar'), - ), - body: Form( - autovalidateMode: _autovalidate, - key: _formKey, - child: Container( - padding: const EdgeInsets.all(10), - child: Column( - children: [ - TextFormField( - decoration: const InputDecoration( - labelText: 'Calendar Name', - hintText: 'My New Calendar', - ), - validator: _validateCalendarName, - onSaved: (String? value) => _calendarName = value ?? '', - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Calendar Color'), - DropdownButton( - onChanged: (selectedColor) { - setState(() => _colorChoice = selectedColor); - }, - value: _colorChoice, - items: ColorChoice.values - .map((color) => DropdownMenuItem( - value: color, - child: Text(color.toString().split('.').last), - )) - .toList(), - ), - ], - ), - if (Platform.isAndroid) - TextFormField( - decoration: const InputDecoration( - labelText: 'Local Account Name', - hintText: 'Device Calendar', - ), - onSaved: (String? value) => _localAccountName = value ?? '', - ), - ], - ), - ), - ), - floatingActionButton: FloatingActionButton( - 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.'); - } else { - form?.save(); - var result = await _deviceCalendarPlugin.createCalendar( - _calendarName, - calendarColor: _colorChoice?.value, - localAccountName: _localAccountName, - ); - - if (result.isSuccess) { - Navigator.pop(context, true); - } else { - showInSnackBar(result.errors - .map((err) => '[${err.errorCode}] ${err.errorMessage}') - .join(' | ')); - } - } - }, - child: const Icon(Icons.check), - ), - ); - } - - String? _validateCalendarName(String? value) { - if (value == null) return null; - if (value.isEmpty) { - return 'Calendar name is required.'; - } - - return null; - } - - void showInSnackBar(String value) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); - } -} - -enum ColorChoice { - Red, - Orange, - Yellow, - Green, - Blue, - Purple, - Brown, - Black, - White -} - -extension ColorChoiceExtension on ColorChoice { - static Color _value(ColorChoice val) { - switch (val) { - case ColorChoice.Red: - return Colors.red; - case ColorChoice.Orange: - return Colors.orange; - case ColorChoice.Yellow: - return Colors.yellow; - case ColorChoice.Green: - return Colors.green; - case ColorChoice.Blue: - return Colors.blue; - case ColorChoice.Purple: - return Colors.purple; - case ColorChoice.Brown: - return Colors.brown; - case ColorChoice.Black: - return Colors.black; - case ColorChoice.White: - return Colors.white; - default: - return Colors.red; - } - } - - Color get value => _value(this); -} diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart deleted file mode 100644 index 72c4cb5b..00000000 --- a/example/lib/presentation/pages/calendar_event.dart +++ /dev/null @@ -1,1261 +0,0 @@ -import 'dart:io'; - -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 '../date_time_picker.dart'; -import '../recurring_event_dialog.dart'; -import 'event_attendee.dart'; -import 'event_reminders.dart'; - -enum RecurrenceRuleEndType { Indefinite, MaxOccurrences, SpecifiedEndDate } - -class CalendarEventPage extends StatefulWidget { - final Calendar _calendar; - final Event? _event; - final RecurringEventDialog? _recurringEventDialog; - - const CalendarEventPage(this._calendar, - [this._event, this._recurringEventDialog, Key? key]) - : super(key: key); - - @override - _CalendarEventPageState createState() { - return _CalendarEventPageState(_calendar, _event, _recurringEventDialog); - } -} - -class _CalendarEventPageState extends State { - final GlobalKey _formKey = GlobalKey(); - final GlobalKey _scaffoldKey = GlobalKey(); - final Calendar _calendar; - - Event? _event; - 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; - - TZDateTime? _endDate; - TimeOfDay? _endTime; - - AutovalidateMode _autovalidate = AutovalidateMode.disabled; - DayOfWeekGroup _dayOfWeekGroup = DayOfWeekGroup.None; - - RecurrenceRuleEndType _recurrenceRuleEndType = - RecurrenceRuleEndType.Indefinite; - RecurrenceRule? _rrule; - - final List _validDaysOfMonth = []; - - Availability _availability = Availability.Busy; - EventStatus? _eventStatus; - List? _attendees; - List? _reminders; - String _timezone = 'Etc/UTC'; - - _CalendarEventPageState( - this._calendar, this._event, this._recurringEventDialog) { - getCurentLocation(); - } - - void getCurentLocation() async { - try { - _timezone = await FlutterNativeTimezone.getLocalTimezone(); - } catch (e) { - debugPrint('Could not get the local timezone'); - } - - _deviceCalendarPlugin = DeviceCalendarPlugin(); - - final event = _event; - if (event == null) { - debugPrint( - 'calendar_event _timezone ------------------------- $_timezone'); - final currentLocation = timeZoneDatabase.locations[_timezone]; - if (currentLocation != null) { - 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']; - 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, availability: _availability); - - debugPrint('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); - - _eventStatus = EventStatus.None; - } else { - 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); - } - - final attendees = event.attendees; - if (attendees != null && attendees.isNotEmpty) { - _attendees = []; - _attendees?.addAll(attendees as Iterable); - } - - final reminders = event.reminders; - if (reminders != null && reminders.isNotEmpty) { - _reminders = []; - _reminders?.addAll(reminders); - } - - final rrule = event.recurrenceRule; - if (rrule != null) { - // debugPrint('OLD_RRULE: ${rrule.toString()}'); - _rrule = rrule; - if (rrule.count != null) { - _recurrenceRuleEndType = RecurrenceRuleEndType.MaxOccurrences; - } - if (rrule.until != null) { - _recurrenceRuleEndType = RecurrenceRuleEndType.SpecifiedEndDate; - } - } - - _availability = event.availability; - _eventStatus = event.status; - } - - // Getting days of the current month (or a selected month for the yearly recurrence) as a default - _getValidDaysOfMonth(_rrule?.frequency); - setState(() {}); - } - - void printAttendeeDetails(Attendee attendee) { - debugPrint( - 'attendee name: ${attendee.name}, email address: ${attendee.emailAddress}, type: ${attendee.role?.enumToString}'); - debugPrint( - 'ios specifics - status: ${attendee.iosAttendeeDetails?.attendanceStatus}, type: ${attendee.iosAttendeeDetails?.attendanceStatus?.enumToString}'); - debugPrint( - 'android specifics - status ${attendee.androidAttendeeDetails?.attendanceStatus}, type: ${attendee.androidAttendeeDetails?.attendanceStatus?.enumToString}'); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: Text(_event?.eventId?.isEmpty ?? true - ? 'Create event' - : _calendar.isReadOnly == true - ? 'View event ${_event?.title}' - : 'Edit event ${_event?.title}'), - ), - 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?.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; - }, - ), - ), - 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; - } - }, - ), - ), - ListTile( - leading: const Text( - 'Availability', - style: TextStyle(fontSize: 16), - ), - trailing: DropdownButton( - value: _availability, - onChanged: (Availability? newValue) { - setState(() { - if (newValue != null) { - _availability = newValue; - _event?.availability = newValue; - } - }); - }, - items: Availability.values - .map>( - (Availability value) { - return DropdownMenuItem( - value: value, - child: Text(value.enumToString), - ); - }).toList(), - ), - ), - if (Platform.isAndroid) - ListTile( - leading: const 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: const Text('All Day'), - ), - if (_startDate != null) - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'From', - enableTime: _event?.allDay == false, - selectedDate: _startDate, - selectedTime: _startTime, - selectDate: (DateTime date) { - setState(() { - var currentLocation = - timeZoneDatabase.locations[_timezone]; - if (currentLocation != null) { - _startDate = - TZDateTime.from(date, currentLocation); - _event?.start = _combineDateWithTime( - _startDate, _startTime); - } - }); - }, - selectTime: (TimeOfDay time) { - setState( - () { - _startTime = time; - _event?.start = _combineDateWithTime( - _startDate, _startTime); - }, - ); - }, - ), - ), - 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); - _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: const 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) => - const EventAttendeePage())); - if (result != null) { - _attendees ??= []; - setState(() { - _attendees?.add(result); - }); - } - } - : null, - leading: const Icon(Icons.people), - title: Text(_calendar.isReadOnly == false - ? 'Add Attendees' - : 'Attendees'), - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: _attendees?.length ?? 0, - itemBuilder: (context, index) { - return Container( - color: (_attendees?[index].isOrganiser ?? false) - ? MediaQuery.of(context).platformBrightness == - Brightness.dark - ? Colors.black26 - : 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), - 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 ?? - 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: const 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: const 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: const 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 ?? true) - Text(_calendar.isReadOnly == false - ? 'Add reminders' - : 'Reminders'), - for (var reminder in _reminders ?? []) - Text('${reminder.minutes} minutes before; ') - ], - ), - ), - ), - ), - CheckboxListTile( - value: _rrule != null, - title: const Text('Is recurring'), - onChanged: (isChecked) { - if (isChecked != null) { - setState(() { - if (isChecked) { - _rrule = - RecurrenceRule(frequency: Frequency.daily); - } else { - _rrule = null; - } - }); - } - }, - ), - if (_rrule != null) ...[ - ListTile( - leading: const Text('Select a Recurrence Type'), - trailing: DropdownButton( - onChanged: (selectedFrequency) { - setState(() { - _onFrequencyChange( - selectedFrequency ?? Frequency.daily); - _getValidDaysOfMonth(selectedFrequency); - }); - }, - 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(), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), - child: Row( - children: [ - const Text('Repeat Every '), - Flexible( - child: TextFormField( - initialValue: '${_rrule?.interval ?? 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) { - _rrule = _rrule?.copyWith( - interval: int.tryParse(value)); - } - }, - ), - ), - _recurrenceFrequencyToIntervalText( - _rrule?.frequency), - ], - ), - ), - 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(() { - 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: 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: const Text('Delete'), - ), - ], - ], - ), - ), - ), - ), - floatingActionButton: Visibility( - visible: _calendar.isReadOnly == false, - child: FloatingActionButton( - key: const Key('saveEventButton'), - onPressed: () async { - final form = _formKey.currentState; - if (form?.validate() == false) { - _autovalidate = - AutovalidateMode.always; // Start validating on every change. - showInSnackBar( - context, 'Please fix the errors in red before submitting.'); - return; - } else { - form?.save(); - _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: const Icon(Icons.check), - ), - ), - ); - } - - 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(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 const Text('Indefinitely'); - case RecurrenceRuleEndType.MaxOccurrences: - return const Text('After a set number of times'); - case RecurrenceRuleEndType.SpecifiedEndDate: - return const Text('Continues until a specified date'); - default: - return const Text(''); - } - } - - // Get total days of a month - void _getValidDaysOfMonth(Frequency? frequency) { - _validDaysOfMonth.clear(); - var totalDays = 0; - - // Year frequency: Get total days of the selected month - if (frequency == Frequency.yearly) { - totalDays = DateTime(DateTime.now().year, - (_rrule?.hasByMonths ?? false) ? _rrule!.byMonths.first : 1, 0) - .day; - } else { - // Otherwise, get total days of the current month - var now = DateTime.now(); - totalDays = DateTime(now.year, now.month + 1, 0).day; - } - - for (var i = 1; i <= totalDays; i++) { - _validDaysOfMonth.add(i); - } - } - - void _updateDaysOfWeek() { - 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: - _rrule = _rrule?.copyWith(byWeekDays: { - ByWeekDayEntry(1), - ByWeekDayEntry(2), - ByWeekDayEntry(3), - ByWeekDayEntry(4), - ByWeekDayEntry(5), - ByWeekDayEntry(6), - ByWeekDayEntry(7), - }); - break; - case DayOfWeekGroup.None: - default: - _rrule?.byWeekDays.clear(); - break; - } - // () => setState(() => {}); - } - - void _updateDaysOfWeekGroup({DayOfWeek? selectedDay}) { - 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; - } - } - } - - int _weekNumFromWeekDayOccurence(Set weekdays) { - final weekNum = weekdays.first.occurrence; - if (weekNum != null) { - return weekNum - 1; - } else { - return 0; - } - } - - 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); - } - } - } - } - - /// 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; - } - } - } - - String? _validateTotalOccurrences(String? value) { - if (value == null) return null; - if (value.isNotEmpty && int.tryParse(value) == null) { - return 'Total occurrences needs to be a valid number'; - } - return null; - } - - String? _validateInterval(String? value) { - if (value == null) return null; - if (value.isNotEmpty && int.tryParse(value) == null) { - return 'Interval needs to be a valid number'; - } - return null; - } - - String? _validateTitle(String? value) { - if (value == null) return null; - if (value.isEmpty) { - return 'Name is required.'; - } - return null; - } - - TZDateTime? _combineDateWithTime(TZDateTime? date, TimeOfDay? time) { - if (date == null) return null; - var currentLocation = timeZoneDatabase.locations[_timezone]; - - final dateWithoutTime = TZDateTime.from( - DateTime.parse(DateFormat('y-MM-dd 00:00:00').format(date)), - currentLocation!); - - if (time == null) return dateWithoutTime; - if (Platform.isAndroid && _event?.allDay == true) return dateWithoutTime; - - return dateWithoutTime - .add(Duration(hours: time.hour, minutes: time.minute)); - } - - 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 deleted file mode 100644 index a8d4b2b2..00000000 --- a/example/lib/presentation/pages/calendar_events.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'dart:async'; - -import 'package:device_calendar/device_calendar.dart'; -import 'package:flutter/material.dart'; - -import '../event_item.dart'; -import '../recurring_event_dialog.dart'; -import 'calendar_event.dart'; - -class CalendarEventsPage extends StatefulWidget { - final Calendar _calendar; - - const CalendarEventsPage(this._calendar, {Key? key}) : super(key: key); - - @override - _CalendarEventsPageState createState() { - return _CalendarEventsPageState(_calendar); - } -} - -class _CalendarEventsPageState extends State { - final Calendar _calendar; - final GlobalKey _scaffoldstate = GlobalKey(); - - late DeviceCalendarPlugin _deviceCalendarPlugin; - List _calendarEvents = []; - bool _isLoading = true; - - _CalendarEventsPageState(this._calendar) { - _deviceCalendarPlugin = DeviceCalendarPlugin(); - } - - @override - void initState() { - super.initState(); - _retrieveCalendarEvents(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - key: _scaffoldstate, - appBar: AppBar( - title: Text('${_calendar.name} events'), - actions: [_getDeleteButton()], - ), - body: (_calendarEvents.isNotEmpty || _isLoading) - ? Stack( - children: [ - ListView.builder( - itemCount: _calendarEvents.length, - itemBuilder: (BuildContext context, int index) { - return EventItem( - _calendarEvents[index], - _deviceCalendarPlugin, - _onLoading, - _onDeletedFinished, - _onTapped, - _calendar.isReadOnly != null && - _calendar.isReadOnly as bool); - }, - ), - if (_isLoading) - const Center( - child: CircularProgressIndicator(), - ) - ], - ) - : const Center(child: Text('No events found')), - floatingActionButton: _getAddEventButton(context)); - } - - Widget? _getAddEventButton(BuildContext context) { - if (_calendar.isReadOnly == false || _calendar.isReadOnly == null) { - return FloatingActionButton( - key: const Key('addEventButton'), - onPressed: () async { - final refreshEvents = await Navigator.push(context, - MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventPage(_calendar); - })); - if (refreshEvents == true) { - await _retrieveCalendarEvents(); - } - }, - child: const Icon(Icons.add), - ); - } else { - return null; - } - } - - void _onLoading() { - setState(() { - _isLoading = true; - }); - } - - Future _onDeletedFinished(bool deleteSucceeded) async { - if (deleteSucceeded) { - await _retrieveCalendarEvents(); - } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Oops, we ran into an issue deleting the event'), - backgroundColor: Colors.red, - duration: Duration(seconds: 5), - )); - setState(() { - _isLoading = false; - }); - } - } - - Future _onTapped(Event event) async { - final refreshEvents = await Navigator.push(context, - MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventPage( - _calendar, - event, - RecurringEventDialog( - _deviceCalendarPlugin, - event, - _onLoading, - _onDeletedFinished, - ), - ); - })); - if (refreshEvents != null && refreshEvents) { - await _retrieveCalendarEvents(); - } - } - - Future _retrieveCalendarEvents() async { - final startDate = DateTime.now().add(const Duration(days: -30)); - final endDate = DateTime.now().add(const Duration(days: 365 * 10)); - var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( - _calendar.id, - RetrieveEventsParams(startDate: startDate, endDate: endDate)); - setState(() { - _calendarEvents = calendarEventsResult.data ?? []; - _isLoading = false; - }); - } - - Widget _getDeleteButton() { - return IconButton( - icon: const Icon(Icons.delete), - onPressed: () async { - await _showDeleteDialog(); - }); - } - - Future _showDeleteDialog() async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Warning'), - content: SingleChildScrollView( - child: ListBody( - children: const [ - Text('This will delete this calendar'), - Text('Are you sure?'), - ], - ), - ), - actions: [ - TextButton( - onPressed: () async { - var returnValue = - await _deviceCalendarPlugin.deleteCalendar(_calendar.id!); - debugPrint( - 'returnValue: ${returnValue.data}, ${returnValue.errors}'); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, - child: const Text('Delete!'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - ], - ); - }, - ); - } -} diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart deleted file mode 100644 index 71c47ea5..00000000 --- a/example/lib/presentation/pages/calendars.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:device_calendar/device_calendar.dart'; -import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'calendar_events.dart'; - -class CalendarsPage extends StatefulWidget { - const CalendarsPage({Key? key}) : super(key: key); - - @override - _CalendarsPageState createState() { - return _CalendarsPageState(); - } -} - -class _CalendarsPageState extends State { - late DeviceCalendarPlugin _deviceCalendarPlugin; - List _calendars = []; - List get _writableCalendars => - _calendars.where((c) => c.isReadOnly == false).toList(); - - List get _readOnlyCalendars => - _calendars.where((c) => c.isReadOnly == true).toList(); - - _CalendarsPageState() { - _deviceCalendarPlugin = DeviceCalendarPlugin(); - } - - @override - void initState() { - super.initState(); - _retrieveCalendars(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Calendars'), - actions: [_getRefreshButton()], - ), - body: Column( - children: [ - Padding( - 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, - ), - ), - Expanded( - flex: 1, - child: ListView.builder( - 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)}'), - onTap: () async { - await Navigator.push(context, - MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventsPage(_calendars[index], - key: const Key('calendarEventsPage')); - })); - }, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - children: [ - Expanded( - 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, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color(_calendars[index].color!)), - ), - 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: const Text('Default'), - ), - Icon(_calendars[index].isReadOnly == true - ? Icons.lock - : Icons.lock_open) - ], - ), - ), - ); - }, - ), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () async { - final createCalendar = await Navigator.push(context, - MaterialPageRoute(builder: (BuildContext context) { - return const CalendarAddPage(); - })); - - if (createCalendar == true) { - _retrieveCalendars(); - } - }, - child: const Icon(Icons.add), - ), - ); - } - - void _retrieveCalendars() async { - try { - var permissionsGranted = await _deviceCalendarPlugin.hasPermissions(); - if (permissionsGranted.isSuccess && - (permissionsGranted.data == null || - permissionsGranted.data == false)) { - permissionsGranted = await _deviceCalendarPlugin.requestPermissions(); - if (!permissionsGranted.isSuccess || - permissionsGranted.data == null || - permissionsGranted.data == false) { - return; - } - } - - final calendarsResult = await _deviceCalendarPlugin.retrieveCalendars(); - setState(() { - _calendars = calendarsResult.data as List; - }); - } on PlatformException catch (e, s) { - debugPrint('RETRIEVE_CALENDARS: $e, $s'); - } - } - - Widget _getRefreshButton() { - return IconButton( - 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 deleted file mode 100644 index 2fff734e..00000000 --- a/example/lib/presentation/pages/event_attendee.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'dart:io'; - -import 'package:device_calendar/device_calendar.dart'; -import 'package:device_calendar_example/common/app_routes.dart'; -import 'package:flutter/material.dart'; - -late DeviceCalendarPlugin _deviceCalendarPlugin; - -class EventAttendeePage extends StatefulWidget { - final Attendee? attendee; - final String? eventId; - const EventAttendeePage({Key? key, this.attendee, this.eventId}) - : super(key: key); - - @override - _EventAttendeePageState createState() => - _EventAttendeePageState(attendee, eventId ?? ''); -} - -class _EventAttendeePageState extends State { - Attendee? _attendee; - final _formKey = GlobalKey(); - final _nameController = TextEditingController(); - final _emailAddressController = TextEditingController(); - var _role = AttendeeRole.None; - var _status = AndroidAttendanceStatus.None; - String _eventId = ''; - - _EventAttendeePageState(Attendee? attendee, eventId) { - if (attendee != null) { - _attendee = attendee; - _nameController.text = _attendee!.name!; - _emailAddressController.text = _attendee!.emailAddress!; - _role = _attendee!.role!; - _status = _attendee!.androidAttendeeDetails?.attendanceStatus ?? - AndroidAttendanceStatus.None; - } - _eventId = eventId; - } - - @override - void dispose() { - _nameController.dispose(); - _emailAddressController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(_attendee != null - ? 'Edit attendee ${_attendee!.name}' - : 'Add an Attendee'), - ), - body: Column( - children: [ - Form( - key: _formKey, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - controller: _nameController, - validator: (value) { - if (_attendee?.isCurrentUser == false && - (value == null || value.isEmpty)) { - return 'Please enter a name'; - } - return null; - }, - decoration: const InputDecoration(labelText: 'Name'), - ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - controller: _emailAddressController, - validator: (value) { - if (value == null || - value.isEmpty || - !value.contains('@')) { - return 'Please enter a valid email address'; - } - return null; - }, - decoration: - const InputDecoration(labelText: 'Email Address'), - ), - ), - ListTile( - leading: const Text('Role'), - trailing: DropdownButton( - onChanged: (value) { - setState(() { - _role = value as AttendeeRole; - }); - }, - value: _role, - items: AttendeeRole.values - .map((role) => DropdownMenuItem( - value: role, - child: Text(role.enumToString), - )) - .toList(), - ), - ), - Visibility( - visible: Platform.isIOS, - child: ListTile( - onTap: () async { - _deviceCalendarPlugin = DeviceCalendarPlugin(); - - await _deviceCalendarPlugin - .showiOSEventModal(_eventId); - Navigator.popUntil( - context, ModalRoute.withName(AppRoutes.calendars)); - //TODO: finish calling and getting attendee details from iOS - }, - leading: const Icon(Icons.edit), - title: const Text('View / edit iOS attendance details'), - ), - ), - Visibility( - visible: Platform.isAndroid, - child: ListTile( - leading: const Text('Android attendee status'), - trailing: DropdownButton( - onChanged: (value) { - setState(() { - _status = value as AndroidAttendanceStatus; - }); - }, - value: _status, - items: AndroidAttendanceStatus.values - .map((status) => DropdownMenuItem( - value: status, - child: Text(status.enumToString), - )) - .toList(), - ), - ), - ) - ], - ), - ), - ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - setState(() { - _attendee = Attendee( - name: _nameController.text, - emailAddress: _emailAddressController.text, - role: _role, - isOrganiser: _attendee?.isOrganiser ?? false, - isCurrentUser: _attendee?.isCurrentUser ?? false, - iosAttendeeDetails: _attendee?.iosAttendeeDetails, - androidAttendeeDetails: AndroidAttendeeDetails.fromJson( - {'attendanceStatus': _status.index})); - - _emailAddressController.clear(); - }); - - Navigator.pop(context, _attendee); - } - }, - child: Text(_attendee != null ? 'Update' : 'Add'), - ) - ], - ), - ); - } -} diff --git a/example/lib/presentation/pages/event_reminders.dart b/example/lib/presentation/pages/event_reminders.dart deleted file mode 100644 index 4b0a11f3..00000000 --- a/example/lib/presentation/pages/event_reminders.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:device_calendar/device_calendar.dart'; - -class EventRemindersPage extends StatefulWidget { - final List _reminders; - const EventRemindersPage(this._reminders, {Key? key}) : super(key: key); - - @override - _EventRemindersPageState createState() => - _EventRemindersPageState(_reminders); -} - -class _EventRemindersPageState extends State { - List _reminders = []; - final _formKey = GlobalKey(); - final _minutesController = TextEditingController(); - - _EventRemindersPageState(List reminders) { - _reminders = [...reminders]; - } - - @override - void dispose() { - _minutesController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Reminders'), - ), - body: Column( - children: [ - Form( - key: _formKey, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - children: [ - Expanded( - child: TextFormField( - controller: _minutesController, - validator: (value) { - if (value == null || - value.isEmpty || - int.tryParse(value) == null) { - return 'Please enter a reminder time in minutes'; - } - return null; - }, - decoration: const InputDecoration( - labelText: 'Minutes before start'), - ), - ), - ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - setState(() { - _reminders.add(Reminder( - minutes: int.parse(_minutesController.text))); - _minutesController.clear(); - }); - } - }, - child: const Text('Add'), - ), - ], - ), - ), - ), - Expanded( - child: ListView.builder( - itemCount: _reminders.length, - itemBuilder: (context, index) { - return ListTile( - title: Text('${_reminders[index].minutes} minutes'), - trailing: ElevatedButton( - onPressed: () { - setState(() { - _reminders.removeWhere( - (a) => a.minutes == _reminders[index].minutes); - }); - }, - child: const Text('Delete'), - ), - ); - }, - ), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(context, _reminders); - }, - child: const Text('Done'), - ) - ], - ), - ); - } -} diff --git a/example/lib/presentation/recurring_event_dialog.dart b/example/lib/presentation/recurring_event_dialog.dart deleted file mode 100644 index c8b8ff35..00000000 --- a/example/lib/presentation/recurring_event_dialog.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:device_calendar/device_calendar.dart'; - -class RecurringEventDialog extends StatefulWidget { - final DeviceCalendarPlugin _deviceCalendarPlugin; - final Event _calendarEvent; - - final VoidCallback _onLoadingStarted; - final Function(bool) _onDeleteFinished; - - const RecurringEventDialog(this._deviceCalendarPlugin, this._calendarEvent, - this._onLoadingStarted, this._onDeleteFinished, - {Key? key}) - : super(key: key); - - @override - _RecurringEventDialogState createState() => - _RecurringEventDialogState(_deviceCalendarPlugin, _calendarEvent, - onLoadingStarted: _onLoadingStarted, - onDeleteFinished: _onDeleteFinished); -} - -class _RecurringEventDialogState extends State { - late DeviceCalendarPlugin _deviceCalendarPlugin; - late Event _calendarEvent; - VoidCallback? _onLoadingStarted; - Function(bool)? _onDeleteFinished; - - _RecurringEventDialogState( - DeviceCalendarPlugin deviceCalendarPlugin, Event calendarEvent, - {VoidCallback? onLoadingStarted, Function(bool)? onDeleteFinished}) { - _deviceCalendarPlugin = deviceCalendarPlugin; - _calendarEvent = calendarEvent; - _onLoadingStarted = onLoadingStarted; - _onDeleteFinished = onDeleteFinished; - } - - @override - Widget build(BuildContext context) { - return SimpleDialog( - title: const Text('Are you sure you want to delete this event?'), - children: [ - SimpleDialogOption( - onPressed: () async { - Navigator.of(context).pop(true); - if (_onLoadingStarted != null) _onLoadingStarted!(); - final deleteResult = - await _deviceCalendarPlugin.deleteEventInstance( - _calendarEvent.calendarId, - _calendarEvent.eventId, - _calendarEvent.start?.millisecondsSinceEpoch, - _calendarEvent.end?.millisecondsSinceEpoch, - false); - if (_onDeleteFinished != null) { - _onDeleteFinished!( - deleteResult.isSuccess && deleteResult.data != null); - } - }, - child: const Text('This instance only'), - ), - SimpleDialogOption( - onPressed: () async { - Navigator.of(context).pop(true); - if (_onLoadingStarted != null) _onLoadingStarted!(); - final deleteResult = - await _deviceCalendarPlugin.deleteEventInstance( - _calendarEvent.calendarId, - _calendarEvent.eventId, - _calendarEvent.start?.millisecondsSinceEpoch, - _calendarEvent.end?.millisecondsSinceEpoch, - true); - if (_onDeleteFinished != null) { - _onDeleteFinished!( - deleteResult.isSuccess && deleteResult.data != null); - } - }, - child: const Text('This and following instances'), - ), - SimpleDialogOption( - onPressed: () async { - Navigator.of(context).pop(true); - if (_onLoadingStarted != null) _onLoadingStarted!(); - final deleteResult = await _deviceCalendarPlugin.deleteEvent( - _calendarEvent.calendarId, _calendarEvent.eventId); - if (_onDeleteFinished != null) { - _onDeleteFinished!( - deleteResult.isSuccess && deleteResult.data != null); - } - }, - child: const Text('All instances'), - ), - SimpleDialogOption( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: const Text('Cancel'), - ) - ], - ); - } -} diff --git a/example/pubspec.yaml b/example/pubspec.yaml deleted file mode 100644 index b32d2fb0..00000000 --- a/example/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -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" - flutter: ">=1.20.0" - -dependencies: - flutter: - sdk: flutter - intl: ^0.17.0 - uuid: ^3.0.6 - flutter_native_timezone: ^2.0.0 - device_calendar: - path: ../ - -dev_dependencies: - integration_test: - sdk: flutter - flutter_test: - sdk: flutter - flutter_lints: ^2.0.1 - -flutter: - uses-material-design: true \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100644 index 710ec6cf..00000000 --- a/ios/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/ios/Classes/DeviceCalendarPlugin.h b/ios/Classes/DeviceCalendarPlugin.h deleted file mode 100644 index 0d5ad0b9..00000000 --- a/ios/Classes/DeviceCalendarPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface DeviceCalendarPlugin : NSObject -@end diff --git a/ios/Classes/DeviceCalendarPlugin.m b/ios/Classes/DeviceCalendarPlugin.m deleted file mode 100644 index 774b46d3..00000000 --- a/ios/Classes/DeviceCalendarPlugin.m +++ /dev/null @@ -1,8 +0,0 @@ -#import "DeviceCalendarPlugin.h" -#import - -@implementation DeviceCalendarPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftDeviceCalendarPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift deleted file mode 100644 index f37d1a5a..00000000 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ /dev/null @@ -1,1136 +0,0 @@ -import EventKit -import EventKitUI -import Flutter -import Foundation -import UIKit - -extension Date { - var millisecondsSinceEpoch: Double { return self.timeIntervalSince1970 * 1000.0 } -} - -extension EKParticipant { - var emailAddress: String? { - return self.value(forKey: "emailAddress") as? String - } -} - -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" } - - if (!iCloudSources.isEmpty) { - return iCloudSources.first - } - - return nil - } - - 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 - } - - 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)) - } - } - - 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 { - 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"; - } - } - - 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 { - 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(fourYearsTimeInterval) - while currentEndDate <= endDate { - 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 [fourYearsTimeInterval] - currentStartDate = currentEndDate - currentEndDate = currentStartDate.addingTimeInterval(fourYearsTimeInterval) - } - - // If the cycle doesn't end exactly on the end date - if currentStartDate <= endDate { - 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) - } - } - } - - 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 { - for ekParticipant in ekEvent.attendees! { - let attendee = convertEkParticipantToAttendee(ekParticipant: ekParticipant) - 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, - calendarId: calendarId, - eventTitle: ekEvent.title ?? "New Event", - eventDescription: ekEvent.notes, - eventStartDate: Int64(ekEvent.startDate.millisecondsSinceEpoch), - eventEndDate: Int64(ekEvent.endDate.millisecondsSinceEpoch), - eventStartTimeZone: ekEvent.timeZone?.identifier, - eventAllDay: ekEvent.isAllDay, - attendees: attendees, - eventLocation: ekEvent.location, - eventURL: ekEvent.url?.absoluteString, - recurrenceRule: recurrenceRule, - organizer: convertEkParticipantToAttendee(ekParticipant: ekEvent.organizer), - reminders: reminders, - availability: convertEkEventAvailability(ekEventAvailability: ekEvent.availability), - eventStatus: convertEkEventStatus(ekEventStatus: ekEvent.status) - ) - - return event - } - - 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 - } - - private func convertEkEventAvailability(ekEventAvailability: EKEventAvailability?) -> Availability? { - switch ekEventAvailability { - case .busy: - return Availability.BUSY - case .free: - return Availability.FREE - case .tentative: - return Availability.TENTATIVE - case .unavailable: - return Availability.UNAVAILABLE - default: - return nil - } - } - - private func convertEkEventStatus(ekEventStatus: EKEventStatus?) -> EventStatus? { - switch ekEventStatus { - case .confirmed: - return EventStatus.CONFIRMED - case .tentative: - return EventStatus.TENTATIVE - case .canceled: - return EventStatus.CANCELED - case .none?: - return EventStatus.NONE - default: - return nil - } - } - - private func parseEKRecurrenceRules(_ ekEvent: EKEvent) -> RecurrenceRule? { - var recurrenceRule: RecurrenceRule? - if ekEvent.hasRecurrenceRules { - let ekRecurrenceRule = ekEvent.recurrenceRules![0] - var frequency: String - switch ekRecurrenceRule.frequency { - case EKRecurrenceFrequency.daily: - frequency = "DAILY" - case EKRecurrenceFrequency.weekly: - frequency = "WEEKLY" - case EKRecurrenceFrequency.monthly: - frequency = "MONTHLY" - case EKRecurrenceFrequency.yearly: - frequency = "YEARLY" - default: - frequency = "DAILY" - } - - var count: Int? - var endDate: String? - if(ekRecurrenceRule.recurrenceEnd?.occurrenceCount != nil && ekRecurrenceRule.recurrenceEnd?.occurrenceCount != 0) { - count = ekRecurrenceRule.recurrenceEnd?.occurrenceCount - } - - let endDateRaw = ekRecurrenceRule.recurrenceEnd?.endDate - if(endDateRaw != nil) { - endDate = formateDateTime(dateTime: endDateRaw!) - } - - 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( - freq: frequency, - count: count, - interval: ekRecurrenceRule.interval, - 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 recurrenceFrequency = recurrenceRuleArguments![recurrenceFrequencyArgument] as? String - let totalOccurrences = recurrenceRuleArguments![countArgument] as? NSInteger - let interval = recurrenceRuleArguments![intervalArgument] as? NSInteger - var recurrenceInterval = 1 - 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 { - 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 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!)} - } - } - - 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: 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 - } - if existingAttendee != nil && ekEvent!.organizer?.emailAddress != existingAttendee?.emailAddress{ - attendees.append(existingAttendee!) - 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 - } - - switch availabilityValue.uppercased() { - case Availability.BUSY.rawValue: - return .busy - case Availability.FREE.rawValue: - return .free - 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 - let calendarId = arguments[calendarIdArgument] as! String - let eventId = arguments[eventIdArgument] as? String - let isAllDay = arguments[eventAllDayArgument] as! Bool - let startDateMillisecondsSinceEpoch = arguments[eventStartDateArgument] as! NSNumber - let endDateDateMillisecondsSinceEpoch = arguments[eventEndDateArgument] as! NSNumber - let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch.doubleValue / 1000.0) - let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch.doubleValue / 1000.0) - let startTimeZoneString = arguments[eventStartTimeZoneArgument] as? String - let title = arguments[self.eventTitleArgument] as? String - let description = arguments[self.eventDescriptionArgument] as? String - let location = arguments[self.eventLocationArgument] as? String - let url = arguments[self.eventURLArgument] 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 - } - - var ekEvent: EKEvent? - if eventId == nil { - ekEvent = EKEvent.init(eventStore: self.eventStore) - } else { - ekEvent = self.eventStore.event(withIdentifier: eventId!) - if(ekEvent == nil) { - self.finishWithEventNotFoundError(result: result, eventId: eventId!) - return - } - } - - ekEvent!.title = title ?? "" - ekEvent!.notes = description - ekEvent!.isAllDay = isAllDay - ekEvent!.startDate = startDate - ekEvent!.endDate = endDate - - if (!isAllDay) { - let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current - ekEvent!.timeZone = timeZone - } - - ekEvent!.calendar = ekCalendar! - ekEvent!.location = location - - // Create and add URL object only when if the input string is not empty or nil - if let urlCheck = url, !urlCheck.isEmpty { - let iosUrl = URL(string: url ?? "") - ekEvent!.url = iosUrl - } - 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) - } catch { - self.eventStore.reset() - result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) - } - }, result: result) - } - - private func createParticipant(name: String, emailAddress: String, role: Int) -> EKParticipant? { - 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") - return participant as? EKParticipant - } - return nil - } - - private func deleteEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - let arguments = call.arguments as! Dictionary - let calendarId = arguments[calendarIdArgument] as! String - let eventId = arguments[eventIdArgument] as! String - 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) - } catch { - self.eventStore.reset() - result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) - } - } - 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) - } - else { - try self.eventStore.remove(ekEvent!, span: .futureEvents, commit: true) - } - - result(true) - } catch { - self.eventStore.reset() - result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) - } - } - }, 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 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() - let jsonData = try jsonEncoder.encode(codable) - let jsonString = String(data: jsonData, encoding: .utf8) - result(jsonString) - } catch { - result(FlutterError(code: genericError, message: error.localizedDescription, details: nil)) - } - } - - private func checkPermissionsThenExecute(permissionsGrantedAction: () -> Void, result: @escaping FlutterResult) { - if hasEventPermissions() { - permissionsGrantedAction() - return - } - self.finishWithUnauthorizedError(result: result) - } - - private func requestPermissions(_ completion: @escaping (Bool) -> Void) { - if hasEventPermissions() { - completion(true) - return - } - 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) - if #available(iOS 17, *) { - return status == EKAuthorizationStatus.fullAccess - } else { - return status == EKAuthorizationStatus.authorized - } - } -} - -extension Date { - func convert(from initTimeZone: TimeZone, to targetTimeZone: TimeZone) -> Date { - let delta = TimeInterval(initTimeZone.secondsFromGMT() - targetTimeZone.secondsFromGMT()) - return addingTimeInterval(delta) - } -} - -extension UIColor { - func rgb() -> Int? { - var fRed : CGFloat = 0 - var fGreen : CGFloat = 0 - var fBlue : CGFloat = 0 - var fAlpha: CGFloat = 0 - if self.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha) { - let iRed = Int(fRed * 255.0) - let iGreen = Int(fGreen * 255.0) - let iBlue = Int(fBlue * 255.0) - let iAlpha = Int(fAlpha * 255.0) - - // (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue). - let rgb = (iAlpha << 24) + (iRed << 16) + (iGreen << 8) + iBlue - return rgb - } else { - // Could not extract RGBA components: - return nil - } - } - - public convenience init?(hex: String) { - let r, g, b, a: CGFloat - - if hex.hasPrefix("0x") { - let start = hex.index(hex.startIndex, offsetBy: 2) - let hexColor = String(hex[start...]) - - if hexColor.count == 8 { - let scanner = Scanner(string: hexColor) - var hexNumber: UInt64 = 0 - - if scanner.scanHexInt64(&hexNumber) { - a = CGFloat((hexNumber & 0xff000000) >> 24) / 255 - 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 - } - } - } - - return nil - } - -} diff --git a/ios/device_calendar.podspec b/ios/device_calendar.podspec deleted file mode 100644 index 3230bcf3..00000000 --- a/ios/device_calendar.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'device_calendar' - s.version = '0.0.1' - s.summary = 'A new flutter plugin project.' - s.description = <<-DESC -A new flutter plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - s.swift_version = '5.0' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end - diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart deleted file mode 100644 index 3566d5df..00000000 --- a/lib/device_calendar.dart +++ /dev/null @@ -1,17 +0,0 @@ -library device_calendar; - -export 'src/common/calendar_enums.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 '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'; -export 'src/models/platform_specifics/android/attendance_status.dart'; -export 'src/device_calendar.dart'; -export 'package:timezone/timezone.dart'; diff --git a/lib/src/common/calendar_enums.dart b/lib/src/common/calendar_enums.dart deleted file mode 100644 index aa77ec02..00000000 --- a/lib/src/common/calendar_enums.dart +++ /dev/null @@ -1,315 +0,0 @@ -enum DayOfWeek { - Monday, - Tuesday, - Wednesday, - Thursday, - Friday, - Saturday, - Sunday, -} - -enum DayOfWeekGroup { - None, - Weekday, - Weekend, - AllDays, -} - -enum MonthOfYear { - January, - Feburary, - March, - April, - May, - June, - July, - August, - September, - October, - November, - December, -} - -enum WeekNumber { - First, - Second, - Third, - Fourth, - Last, -} - -enum AttendeeRole { - None, - Required, - Optional, - Resource, -} - -enum Availability { - Free, - Busy, - Tentative, - Unavailable, -} - -enum EventStatus { - None, - Confirmed, - Canceled, - Tentative, -} - -extension DayOfWeekExtension on DayOfWeek { - static int _value(DayOfWeek val) { - switch (val) { - case DayOfWeek.Monday: - return 1; - case DayOfWeek.Tuesday: - return 2; - case DayOfWeek.Wednesday: - return 3; - case DayOfWeek.Thursday: - return 4; - case DayOfWeek.Friday: - return 5; - case DayOfWeek.Saturday: - return 6; - case DayOfWeek.Sunday: - return 0; - default: - return 1; - } - } - - String _enumToString(DayOfWeek enumValue) { - return enumValue.toString().split('.').last; - } - - int get value => _value(this); - - String get enumToString => _enumToString(this); -} - -extension DaysOfWeekGroupExtension on DayOfWeekGroup { - static List _getDays(DayOfWeekGroup val) { - switch (val) { - case DayOfWeekGroup.Weekday: - return [ - DayOfWeek.Monday, - DayOfWeek.Tuesday, - DayOfWeek.Wednesday, - DayOfWeek.Thursday, - DayOfWeek.Friday - ]; - case DayOfWeekGroup.Weekend: - return [DayOfWeek.Saturday, DayOfWeek.Sunday]; - case DayOfWeekGroup.AllDays: - return [ - DayOfWeek.Monday, - DayOfWeek.Tuesday, - DayOfWeek.Wednesday, - DayOfWeek.Thursday, - DayOfWeek.Friday, - DayOfWeek.Saturday, - DayOfWeek.Sunday - ]; - default: - return []; - } - } - - String _enumToString(DayOfWeekGroup enumValue) { - return enumValue.toString().split('.').last; - } - - List get getDays => _getDays(this); - - String get enumToString => _enumToString(this); -} - -extension MonthOfYearExtension on MonthOfYear { - static int _value(MonthOfYear val) { - switch (val) { - case MonthOfYear.January: - return 1; - case MonthOfYear.Feburary: - return 2; - case MonthOfYear.March: - return 3; - case MonthOfYear.April: - return 4; - case MonthOfYear.May: - return 5; - case MonthOfYear.June: - return 6; - case MonthOfYear.July: - return 7; - case MonthOfYear.August: - return 8; - case MonthOfYear.September: - return 9; - case MonthOfYear.October: - return 10; - case MonthOfYear.November: - return 11; - case MonthOfYear.December: - return 12; - default: - return 1; - } - } - - String _enumToString(MonthOfYear enumValue) { - return enumValue.toString().split('.').last; - } - - int get value => _value(this); - - String get enumToString => _enumToString(this); -} - -extension WeekNumberExtension on WeekNumber { - static int _value(WeekNumber val) { - switch (val) { - case WeekNumber.First: - return 1; - case WeekNumber.Second: - return 2; - case WeekNumber.Third: - return 3; - case WeekNumber.Fourth: - return 4; - case WeekNumber.Last: - return -1; - default: - return 1; - } - } - - String _enumToString(WeekNumber enumValue) { - return enumValue.toString().split('.').last; - } - - int get value => _value(this); - - String get enumToString => _enumToString(this); -} - -extension IntExtensions on int { - static DayOfWeek _getDayOfWeekEnumValue(int val) { - switch (val) { - case 1: - return DayOfWeek.Monday; - case 2: - return DayOfWeek.Tuesday; - case 3: - return DayOfWeek.Wednesday; - case 4: - return DayOfWeek.Thursday; - case 5: - return DayOfWeek.Friday; - case 6: - return DayOfWeek.Saturday; - case 0: - return DayOfWeek.Sunday; - default: - return DayOfWeek.Monday; - } - } - - static MonthOfYear _getMonthOfYearEnumValue(int val) { - switch (val) { - case 1: - return MonthOfYear.January; - case 2: - return MonthOfYear.Feburary; - case 3: - return MonthOfYear.March; - case 4: - return MonthOfYear.April; - case 5: - return MonthOfYear.May; - case 6: - return MonthOfYear.June; - case 7: - return MonthOfYear.July; - case 8: - return MonthOfYear.August; - case 9: - return MonthOfYear.September; - case 10: - return MonthOfYear.October; - case 11: - return MonthOfYear.November; - case 12: - return MonthOfYear.December; - default: - return MonthOfYear.January; - } - } - - static WeekNumber _getWeekNumberEnumValue(int val) { - switch (val) { - case 1: - return WeekNumber.First; - case 2: - return WeekNumber.Second; - case 3: - return WeekNumber.Third; - case 4: - return WeekNumber.Fourth; - case -1: - return WeekNumber.Last; - default: - return WeekNumber.First; - } - } - - DayOfWeek get getDayOfWeekEnumValue => _getDayOfWeekEnumValue(this); - - MonthOfYear get getMonthOfYearEnumValue => _getMonthOfYearEnumValue(this); - - WeekNumber get getWeekNumberEnumValue => _getWeekNumberEnumValue(this); -} - -extension RoleExtensions on AttendeeRole { - String _enumToString(AttendeeRole enumValue) { - return enumValue.toString().split('.').last; - } - - String get enumToString => _enumToString(this); -} - -extension AvailabilityExtensions on Availability { - String _enumToString(Availability enumValue) { - switch (enumValue) { - case Availability.Busy: - return 'BUSY'; - case Availability.Free: - return 'FREE'; - case Availability.Tentative: - return 'TENTATIVE'; - case Availability.Unavailable: - return 'UNAVAILABLE'; - } - } - - String get enumToString => _enumToString(this); -} - -extension EventStatusExtensions on EventStatus { - String _enumToString(EventStatus enumValue) { - switch (enumValue) { - case EventStatus.Confirmed: - return 'CONFIRMED'; - case EventStatus.Tentative: - return 'TENTATIVE'; - case EventStatus.Canceled: - return 'CANCELED'; - case EventStatus.None: - return 'NONE'; - } - } - - String get enumToString => _enumToString(this); -} diff --git a/lib/src/common/channel_constants.dart b/lib/src/common/channel_constants.dart deleted file mode 100644 index 2eef3d2d..00000000 --- a/lib/src/common/channel_constants.dart +++ /dev/null @@ -1,26 +0,0 @@ -class ChannelConstants { - static const String channelName = 'plugins.builttoroam.com/device_calendar'; - - static const String methodNameRequestPermissions = 'requestPermissions'; - static const String methodNameHasPermissions = 'hasPermissions'; - static const String methodNameRetrieveCalendars = 'retrieveCalendars'; - static const String methodNameRetrieveEvents = 'retrieveEvents'; - static const String methodNameDeleteEvent = 'deleteEvent'; - static const String methodNameDeleteEventInstance = 'deleteEventInstance'; - static const String methodNameCreateOrUpdateEvent = 'createOrUpdateEvent'; - static const String methodNameCreateCalendar = 'createCalendar'; - static const String methodNameDeleteCalendar = 'deleteCalendar'; - static const String methodNameShowiOSEventModal = 'showiOSEventModal'; - - static const String parameterNameCalendarId = 'calendarId'; - static const String parameterNameStartDate = 'startDate'; - static const String parameterNameEndDate = 'endDate'; - static const String parameterNameEventId = 'eventId'; - static const String parameterNameEventIds = 'eventIds'; - static const String parameterNameEventStartDate = 'eventStartDate'; - static const String parameterNameEventEndDate = 'eventEndDate'; - static const String parameterNameFollowingInstances = 'followingInstances'; - static const String parameterNameCalendarName = 'calendarName'; - static const String parameterNameCalendarColor = 'calendarColor'; - static const String parameterNameLocalAccountName = 'localAccountName'; -} diff --git a/lib/src/common/error_codes.dart b/lib/src/common/error_codes.dart deleted file mode 100644 index e75b29de..00000000 --- a/lib/src/common/error_codes.dart +++ /dev/null @@ -1,6 +0,0 @@ -class ErrorCodes { - static const int invalidArguments = 400; - static const int platformSpecific = 599; - static const int generic = 500; - static const int unknown = 502; -} diff --git a/lib/src/common/error_messages.dart b/lib/src/common/error_messages.dart deleted file mode 100644 index f2126006..00000000 --- a/lib/src/common/error_messages.dart +++ /dev/null @@ -1,27 +0,0 @@ -class ErrorMessages { - static const String fromJsonMapIsNull = 'The json object is null'; - - static const String invalidMissingCalendarId = - 'Calendar ID is missing or invalid'; - - static const String invalidRetrieveEventsParams = - 'A valid instance of the RetrieveEventsParams class is required. Must the event ids to filter by or the start and end date to filter by or a combination of these'; - static const String deleteEventInvalidArgumentsMessage = - 'Calendar ID and/or Event ID argument(s) have not been specified or are invalid'; - static const String createOrUpdateEventInvalidArgumentsMessageAllDay = - "To create or update an all day event you must provide calendar ID, event with a title and event's start date"; - static const String createOrUpdateEventInvalidArgumentsMessage = - "To create or update an event you must provide calendar ID, event with a title and event's start date and end date (where start date must be before end date)"; - static const String createCalendarInvalidCalendarNameMessage = - 'Calendar name must not be null or empty'; - - static const String invalidRecurrencyFrequency = - 'Invalid recurrency frequency'; - - 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.'; - static const String unknownDeviceGenericExceptionTemplate = - 'Device calendar plugin ran into an issue, with message "%s"'; -} diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart deleted file mode 100644 index 4c1d12f1..00000000 --- a/lib/src/device_calendar.dart +++ /dev/null @@ -1,443 +0,0 @@ -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; - -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 { - static const MethodChannel channel = - MethodChannel(ChannelConstants.channelName); - - static final DeviceCalendarPlugin _instance = DeviceCalendarPlugin.private(); - - factory DeviceCalendarPlugin({bool shouldInitTimezone = true}) { - if (shouldInitTimezone) { - tz.initializeTimeZones(); - } - return _instance; - } - - @visibleForTesting - DeviceCalendarPlugin.private(); - - /// Requests permissions to modify the calendars on the device - /// - /// Returns a [Result] indicating if calendar READ and WRITE permissions - /// have (true) or have not (false) been granted - Future> requestPermissions() async { - return _invokeChannelMethod( - ChannelConstants.methodNameRequestPermissions, - ); - } - - /// Checks if permissions for modifying the device calendars have been granted - /// - /// Returns a [Result] indicating if calendar READ and WRITE permissions - /// have (true) or have not (false) been granted - Future> hasPermissions() async { - return _invokeChannelMethod( - ChannelConstants.methodNameHasPermissions, - ); - } - - /// Retrieves all of the device defined calendars - /// - /// Returns a [Result] containing a list of device [Calendar] - Future>> retrieveCalendars() async { - return _invokeChannelMethod( - ChannelConstants.methodNameRetrieveCalendars, - evaluateResponse: (rawData) => UnmodifiableListView( - json.decode(rawData).map( - (decodedCalendar) => Calendar.fromJson(decodedCalendar), - ), - ), - ); - } - - /// Retrieves the events from the specified calendar - /// - /// The `calendarId` paramter is the id of the calendar that plugin will return events for - /// The `retrieveEventsParams` parameter combines multiple properties that - /// specifies conditions of the events retrieval. For instance, defining [RetrieveEventsParams.startDate] - /// and [RetrieveEventsParams.endDate] will return events only happening in that time range - /// - /// Returns a [Result] containing a list [Event], that fall - /// into the specified parameters - Future>> retrieveEvents( - 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( - 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.\ - /// To delete individual instance of a recurring event, please use [deleteEventInstance()] - /// - /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\ - /// The `eventId` parameter is the id of the event that plugin will try to delete - /// - /// Returns a [Result] indicating if the event has (true) or has not (false) been deleted from the calendar - Future> deleteEvent( - String? calendarId, - String? eventId, - ) async { - return _invokeChannelMethod( - ChannelConstants.methodNameDeleteEvent, - assertParameters: (result) { - _validateCalendarIdParameter( - result, - calendarId, - ); - - _assertParameter( - result, - eventId?.isNotEmpty ?? false, - ErrorCodes.invalidArguments, - ErrorMessages.deleteEventInvalidArgumentsMessage, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarId: calendarId, - ChannelConstants.parameterNameEventId: eventId, - }, - ); - } - - /// Deletes an instance of a recurring event from a calendar. This should be used for a recurring event only.\ - /// If `startDate`, `endDate` or `deleteFollowingInstances` is not valid or null, then all instances of the event will be deleted. - /// - /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\ - /// The `eventId` parameter is the id of the event that plugin will try to delete\ - /// The `startDate` parameter is the start date of the instance to delete\ - /// The `endDate` parameter is the end date of the instance to delete\ - /// The `deleteFollowingInstances` parameter will also delete the following instances if set to true - /// - /// Returns a [Result] indicating if the instance of the event has (true) or has not (false) been deleted from the calendar - Future> deleteEventInstance( - String? calendarId, - String? eventId, - int? startDate, - int? endDate, - bool deleteFollowingInstances, - ) async { - return _invokeChannelMethod( - ChannelConstants.methodNameDeleteEventInstance, - assertParameters: (result) { - _validateCalendarIdParameter( - result, - calendarId, - ); - - _assertParameter( - result, - eventId?.isNotEmpty ?? false, - ErrorCodes.invalidArguments, - ErrorMessages.deleteEventInvalidArgumentsMessage, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarId: calendarId, - ChannelConstants.parameterNameEventId: eventId, - ChannelConstants.parameterNameEventStartDate: startDate, - ChannelConstants.parameterNameEventEndDate: endDate, - ChannelConstants.parameterNameFollowingInstances: - deleteFollowingInstances, - }, - ); - } - - /// Creates or updates an event - /// - /// The `event` paramter specifies how event data should be saved into the calendar - /// Always specify the [Event.calendarId], to inform the plugin in which calendar - /// it should create or update the event. - /// - /// Returns a [Result] with the newly created or updated [Event.eventId] - Future?> createOrUpdateEvent(Event? event) async { - if (event == null) return null; - return _invokeChannelMethod( - ChannelConstants.methodNameCreateOrUpdateEvent, - assertParameters: (result) { - // Setting time to 0 for all day events - if (event.allDay == true) { - if (event.start != null) { - var dateStart = DateTime(event.start!.year, event.start!.month, - event.start!.day, 0, 0, 0); - // 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) - : TZDateTime.from(dateStart, - timeZoneDatabase.locations[event.start!.location.name]!); - } - if (event.end != null) { - var dateEnd = DateTime( - event.end!.year, event.end!.month, event.end!.day, 0, 0, 0); - // allDay events on Android need to be at midnight UTC on the - // day after the last day. For example, a 2-day allDay event on - // 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(const Duration(days: 1)) - : TZDateTime.from(dateEnd, - timeZoneDatabase.locations[event.end!.location.name]!); - } - } - - _assertParameter( - result, - !(event.allDay == true && (event.calendarId?.isEmpty ?? true) || - event.start == null || - event.end == null), - ErrorCodes.invalidArguments, - ErrorMessages.createOrUpdateEventInvalidArgumentsMessageAllDay, - ); - - _assertParameter( - result, - !(event.allDay != true && - ((event.calendarId?.isEmpty ?? true) || - event.start == null || - event.end == null || - (event.start != null && - event.end != null && - event.start!.isAfter(event.end!)))), - ErrorCodes.invalidArguments, - ErrorMessages.createOrUpdateEventInvalidArgumentsMessage, - ); - }, - arguments: () => event.toJson(), - ); - } - - /// Creates a new local calendar for the current device. - /// - /// The `calendarName` parameter is the name of the new calendar\ - /// The `calendarColor` parameter is the color of the calendar. If null, - /// a default color (red) will be used\ - /// The `localAccountName` parameter is the name of the local account: - /// - [Android] Required. If `localAccountName` parameter is null or empty, it will default to 'Device Calendar'. - /// If the account name already exists in the device, it will add another calendar under the account, - /// otherwise a new local account and a new calendar will be created. - /// - [iOS] Not used. A local account will be picked up automatically, if not found, an error will be thrown. - /// - /// Returns a [Result] with the newly created [Calendar.id] - Future> createCalendar( - String? calendarName, { - Color? calendarColor, - String? localAccountName, - }) async { - return _invokeChannelMethod( - ChannelConstants.methodNameCreateCalendar, - assertParameters: (result) { - calendarColor ??= Colors.red; - - _assertParameter( - result, - calendarName?.isNotEmpty == true, - ErrorCodes.invalidArguments, - ErrorMessages.createCalendarInvalidCalendarNameMessage, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarName: calendarName, - ChannelConstants.parameterNameCalendarColor: - '0x${calendarColor?.value.toRadixString(16)}', - ChannelConstants.parameterNameLocalAccountName: - localAccountName?.isEmpty ?? true - ? 'Device Calendar' - : localAccountName - }, - ); - } - - /// Deletes a calendar. - /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\/// - /// Returns a [Result] indicating if the instance of the calendar has (true) or has not (false) been deleted - Future> deleteCalendar( - String calendarId, - ) async { - return _invokeChannelMethod( - ChannelConstants.methodNameDeleteCalendar, - assertParameters: (result) { - _validateCalendarIdParameter( - result, - calendarId, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarId: calendarId, - }, - ); - } - - /// 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 - Future> showiOSEventModal( - String eventId, - ) { - return _invokeChannelMethod( - ChannelConstants.methodNameShowiOSEventModal, - arguments: () => { - ChannelConstants.parameterNameEventId: eventId, - }, - ); - } - - Future> _invokeChannelMethod( - String channelMethodName, { - Function(Result)? assertParameters, - Map Function()? arguments, - T Function(dynamic)? evaluateResponse, - }) async { - final result = Result(); - - try { - if (assertParameters != null) { - assertParameters(result); - if (result.hasErrors) { - return result; - } - } - - var rawData = await channel.invokeMethod( - channelMethodName, - arguments != null ? arguments() : null, - ); - - if (evaluateResponse != null) { - result.data = evaluateResponse(rawData); - } else { - result.data = rawData; - } - } 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; - } - - void _parsePlatformExceptionAndUpdateResult( - Exception? exception, Result result) { - if (exception == null) { - result.errors.add( - const ResultError( - ErrorCodes.unknown, - ErrorMessages.unknownDeviceIssue, - ), - ); - return; - } - - debugPrint('$exception'); - - if (exception is PlatformException) { - result.errors.add( - ResultError( - ErrorCodes.platformSpecific, - '${ErrorMessages.unknownDeviceExceptionTemplate}, Code: ${exception.code}, Exception: ${exception.message}', - ), - ); - } else { - result.errors.add( - ResultError( - ErrorCodes.generic, - '${ErrorMessages.unknownDeviceGenericExceptionTemplate} ${exception.toString}', - ), - ); - } - } - - void _assertParameter( - Result result, - bool predicate, - int errorCode, - String errorMessage, - ) { - if (result.data != null) { - debugPrint("RESULT of _assertParameter: ${result.data}"); - } - if (!predicate) { - result.errors.add( - ResultError(errorCode, errorMessage), - ); - } - } - - void _validateCalendarIdParameter( - Result result, - String? calendarId, - ) { - _assertParameter( - result, - calendarId?.isNotEmpty ?? false, - ErrorCodes.invalidArguments, - ErrorMessages.invalidMissingCalendarId, - ); - } -} diff --git a/lib/src/models/attendee.dart b/lib/src/models/attendee.dart deleted file mode 100644 index 2e8e4cb9..00000000 --- a/lib/src/models/attendee.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:io' show Platform; - -import '../common/calendar_enums.dart'; -import '../common/error_messages.dart'; -import 'platform_specifics/android/attendee_details.dart'; -import 'platform_specifics/ios/attendee_details.dart'; - -/// A person attending an event -class Attendee { - /// The name of the attendee - String? name; - - /// The email address of the attendee - String? emailAddress; - - /// An attendee role: None, Optional, Required or Resource - AttendeeRole? role; - - /// Read-only. Returns true if the attendee is an organiser, else false - bool isOrganiser = false; - - /// Read-only. Returns true if the attendee is the current user, else false - bool isCurrentUser = false; - - /// Details about the attendee that are specific to iOS. - /// When reading details for an existing event, this will only be populated on iOS devices. - IosAttendeeDetails? iosAttendeeDetails; - - /// Details about the attendee that are specific to Android. - /// When reading details for an existing event, this will only be populated on Android devices. - AndroidAttendeeDetails? androidAttendeeDetails; - - Attendee({ - this.name, - this.emailAddress, - this.role, - this.isOrganiser = false, - this.isCurrentUser = false, - this.iosAttendeeDetails, - this.androidAttendeeDetails, - }); - - Attendee.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - - name = json['name']; - emailAddress = json['emailAddress']; - role = AttendeeRole.values[json['role'] ?? 0]; - isOrganiser = json['isOrganizer'] ?? - false; // Getting and setting an organiser for Android - isCurrentUser = json['isCurrentUser'] ?? false; - - if (Platform.isAndroid) { - androidAttendeeDetails = AndroidAttendeeDetails.fromJson(json); - } - - if (Platform.isIOS) { - iosAttendeeDetails = IosAttendeeDetails.fromJson(json); - } - } - - Map toJson() { - final data = { - 'name': name, - 'emailAddress': emailAddress, - 'role': role?.index, - 'isOrganizer': isOrganiser, - }; - - if (iosAttendeeDetails != null) { - data.addEntries(iosAttendeeDetails!.toJson().entries); - } - if (androidAttendeeDetails != null) { - data.addEntries(androidAttendeeDetails!.toJson().entries); - } - - return data; - } -} diff --git a/lib/src/models/calendar.dart b/lib/src/models/calendar.dart deleted file mode 100644 index c04ab405..00000000 --- a/lib/src/models/calendar.dart +++ /dev/null @@ -1,56 +0,0 @@ -/// A calendar on the user's device -class Calendar { - /// Read-only. The unique identifier for this calendar - String? id; - - /// The name of this calendar - String? name; - - /// Read-only. If the calendar is read-only - bool? isReadOnly; - - /// Read-only. If the calendar is the default - bool? isDefault; - - /// Read-only. Color of the calendar - int? color; - - // Read-only. Account name associated with the calendar - String? accountName; - - // Read-only. Account type associated with the calendar - String? accountType; - - Calendar( - {this.id, - this.name, - this.isReadOnly, - this.isDefault, - this.color, - this.accountName, - this.accountType}); - - Calendar.fromJson(Map json) { - id = json['id']; - name = json['name']; - isReadOnly = json['isReadOnly']; - isDefault = json['isDefault']; - color = json['color']; - accountName = json['accountName']; - accountType = json['accountType']; - } - - Map toJson() { - final data = { - 'id': id, - 'name': name, - 'isReadOnly': isReadOnly, - 'isDefault': isDefault, - 'color': color, - 'accountName': accountName, - 'accountType': accountType - }; - - return data; - } -} diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart deleted file mode 100644 index 00ebfa52..00000000 --- a/lib/src/models/event.dart +++ /dev/null @@ -1,306 +0,0 @@ -import 'dart:io'; - -import 'package:collection/collection.dart'; - -import '../../device_calendar.dart'; -import '../common/error_messages.dart'; - -/// An event associated with a calendar -class Event { - /// Read-only. The unique identifier for this event. This is auto-generated when a new event is created - String? eventId; - - /// Read-only. The identifier of the calendar that this event is associated with - String? calendarId; - - /// The title of this event - String? title; - - /// The description for this event - String? description; - - /// Indicates when the event starts - TZDateTime? start; - - /// Indicates when the event ends - TZDateTime? end; - - /// Indicates if this is an all-day event - bool? allDay; - - /// The location of this event - String? location; - - /// An URL for this event - Uri? url; - - /// A list of attendees for this event - List? attendees; - - /// The recurrence rule for this event - RecurrenceRule? recurrenceRule; - - /// A list of reminders (by minutes) for this event - List? reminders; - - /// Indicates if this event counts as busy time, tentative, unavaiable or is still free time - late Availability availability; - - /// Indicates if this event is of confirmed, canceled, tentative or none status - EventStatus? status; - - //##### - /// Read-only. Color of the event - int? color; - - ///Note for development: - /// - ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. - ///Make sure all locations are updated if changes needed to be made. - ///Swift: - ///`ios/Classes/SwiftDeviceCalendarPlugin.swift` - ///Kotlin: - ///`android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt` - ///`android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt` - ///`android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt` - Event( - this.calendarId, { - this.eventId, - this.title, - this.start, - this.end, - this.description, - this.attendees, - this.recurrenceRule, - this.reminders, - this.availability = Availability.Busy, - this.location, - this.url, - this.allDay = false, - this.status, - this.color, - }); - - ///Get Event from JSON. - /// - ///Sample JSON: - ///{calendarId: 00, eventId: 0000, eventTitle: Sample Event, eventDescription: This is a sample event, eventStartDate: 1563719400000, eventStartTimeZone: Asia/Hong_Kong, eventEndDate: 1640532600000, eventEndTimeZone: Asia/Hong_Kong, eventAllDay: false, eventLocation: Yuenlong Station, eventURL: null, availability: BUSY, attendees: [{name: commonfolk, emailAddress: total.loss@hong.com, role: 1, isOrganizer: false, attendanceStatus: 3}], reminders: [{minutes: 39}]} - Event.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - String? foundUrl; - String? startLocationName; - String? endLocationName; - int? startTimestamp; - int? endTimestamp; - bool legacyJSON = false; - var legacyName = { - title: 'title', - description: 'description', - startTimestamp: 'start', - endTimestamp: 'end', - startLocationName: 'startTimeZone', - endLocationName: 'endTimeZone', - allDay: 'allDay', - location: 'location', - foundUrl: 'url', - }; - legacyName.forEach((key, value) { - if (json[value] != null) { - key = json[value]; - legacyJSON = true; - } - }); - - eventId = json['eventId']; - calendarId = json['calendarId']; - title = json['eventTitle']; - description = json['eventDescription']; - color = json['eventColor']; //##### - - startTimestamp = json['eventStartDate']; - startLocationName = json['eventStartTimeZone']; - var startTimeZone = timeZoneDatabase.locations[startLocationName]; - startTimeZone ??= local; - start = startTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(startTimeZone, startTimestamp) : TZDateTime.now(local); - - endTimestamp = json['eventEndDate']; - endLocationName = json['eventEndTimeZone']; - var endLocation = timeZoneDatabase.locations[endLocationName]; - endLocation ??= startTimeZone; - end = endTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(endLocation, endTimestamp) : TZDateTime.now(local); - allDay = json['eventAllDay'] ?? false; - if (Platform.isAndroid && (allDay ?? false)) { - // On Android, the datetime in an allDay event is adjusted to local - // timezone, which can result in the wrong day, so we need to bring the - // date back to midnight UTC to get the correct date - var startOffset = start?.timeZoneOffset.inMilliseconds ?? 0; - var endOffset = end?.timeZoneOffset.inMilliseconds ?? 0; - // subtract the offset to get back to midnight on the correct date - start = start?.subtract(Duration(milliseconds: startOffset)); - 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(const Duration(days: 1)); - } - location = json['eventLocation']; - availability = parseStringToAvailability(json['availability']); - status = parseStringToEventStatus(json['eventStatus']); - - foundUrl = json['eventURL']?.toString(); - if (foundUrl?.isEmpty ?? true) { - url = null; - } else { - url = Uri.dataFromString(foundUrl as String); - } - - if (json['attendees'] != null) { - attendees = json['attendees'].map((decodedAttendee) { - return Attendee.fromJson(decodedAttendee); - }).toList(); - } - - if (json['organizer'] != null) { - // Getting and setting an organiser for iOS - var organiser = Attendee.fromJson(json['organizer']); - - var attendee = attendees?.firstWhereOrNull((at) => at?.name == organiser.name && at?.emailAddress == organiser.emailAddress); - if (attendee != null) { - attendee.isOrganiser = true; - } - } - - 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) { - reminders = json['reminders'].map((decodedReminder) { - return Reminder.fromJson(decodedReminder); - }).toList(); - } - if (legacyJSON) { - throw const FormatException('legacy JSON detected. Please update your current JSONs as they may not be supported later on.'); - } - } - - Map toJson() { - final data = {}; - - data['calendarId'] = calendarId; - data['eventId'] = eventId; - data['eventTitle'] = title; - data['eventDescription'] = description; - data['eventStartDate'] = start?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; - data['eventStartTimeZone'] = start?.location.name; - data['eventEndDate'] = end?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; - data['eventEndTimeZone'] = end?.location.name; - data['eventAllDay'] = allDay; - data['eventLocation'] = location; - data['eventURL'] = url?.data?.contentText; - data['availability'] = availability.enumToString; - data['eventStatus'] = status?.enumToString; - data['eventColor'] = color; //##### - - if (attendees != null) { - data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); - } - - if (attendees != null) { - data['organizer'] = attendees?.firstWhereOrNull((a) => a!.isOrganiser)?.toJson(); - } - - 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; - } - - Availability parseStringToAvailability(String? value) { - var testValue = value?.toUpperCase(); - switch (testValue) { - case 'BUSY': - return Availability.Busy; - case 'FREE': - return Availability.Free; - case 'TENTATIVE': - return Availability.Tentative; - case 'UNAVAILABLE': - return Availability.Unavailable; - } - return Availability.Busy; - } - - EventStatus? parseStringToEventStatus(String? value) { - var testValue = value?.toUpperCase(); - switch (testValue) { - case 'CONFIRMED': - return EventStatus.Confirmed; - case 'TENTATIVE': - return EventStatus.Tentative; - case 'CANCELED': - return EventStatus.Canceled; - case 'NONE': - return EventStatus.None; - } - return null; - } - - bool updateStartLocation(String? newStartLocation) { - if (newStartLocation == null) return false; - try { - var location = timeZoneDatabase.get(newStartLocation); - start = TZDateTime.from(start as TZDateTime, location); - return true; - } on LocationNotFoundException { - return false; - } - } - - bool updateEndLocation(String? newEndLocation) { - if (newEndLocation == null) return false; - try { - var location = timeZoneDatabase.get(newEndLocation); - end = TZDateTime.from(end as TZDateTime, location); - return true; - } on LocationNotFoundException { - return false; - } - } -} diff --git a/lib/src/models/platform_specifics/android/attendance_status.dart b/lib/src/models/platform_specifics/android/attendance_status.dart deleted file mode 100644 index d895877f..00000000 --- a/lib/src/models/platform_specifics/android/attendance_status.dart +++ /dev/null @@ -1,15 +0,0 @@ -enum AndroidAttendanceStatus { - None, - Accepted, - Declined, - Invited, - Tentative, -} - -extension AndroidAttendanceStatusExtensions on AndroidAttendanceStatus { - String _enumToString(AndroidAttendanceStatus enumValue) { - return enumValue.toString().split('.').last; - } - - String get enumToString => _enumToString(this); -} diff --git a/lib/src/models/platform_specifics/android/attendee_details.dart b/lib/src/models/platform_specifics/android/attendee_details.dart deleted file mode 100644 index ba6f3b7a..00000000 --- a/lib/src/models/platform_specifics/android/attendee_details.dart +++ /dev/null @@ -1,23 +0,0 @@ -import '../../../common/error_messages.dart'; -import 'attendance_status.dart'; - -class AndroidAttendeeDetails { - AndroidAttendanceStatus? attendanceStatus; - - AndroidAttendeeDetails({this.attendanceStatus}); - - AndroidAttendeeDetails.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - - if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { - attendanceStatus = - AndroidAttendanceStatus.values[json['attendanceStatus']]; - } - } - - Map toJson() { - return {'attendanceStatus': attendanceStatus?.index}; - } -} diff --git a/lib/src/models/platform_specifics/ios/attendance_status.dart b/lib/src/models/platform_specifics/ios/attendance_status.dart deleted file mode 100644 index bd958391..00000000 --- a/lib/src/models/platform_specifics/ios/attendance_status.dart +++ /dev/null @@ -1,18 +0,0 @@ -enum IosAttendanceStatus { - Unknown, - Pending, - Accepted, - Declined, - Tentative, - Delegated, - Completed, - InProcess, -} - -extension IosAttendanceStatusExtensions on IosAttendanceStatus { - String _enumToString(IosAttendanceStatus enumValue) { - return enumValue.toString().split('.').last; - } - - String get enumToString => _enumToString(this); -} diff --git a/lib/src/models/platform_specifics/ios/attendee_details.dart b/lib/src/models/platform_specifics/ios/attendee_details.dart deleted file mode 100644 index 305a1b42..00000000 --- a/lib/src/models/platform_specifics/ios/attendee_details.dart +++ /dev/null @@ -1,21 +0,0 @@ -import '../../../common/error_messages.dart'; -import 'attendance_status.dart'; - -class IosAttendeeDetails { - IosAttendanceStatus? attendanceStatus; - IosAttendeeDetails({this.attendanceStatus}); - - IosAttendeeDetails.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - - if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { - attendanceStatus = IosAttendanceStatus.values[json['attendanceStatus']]; - } - } - - Map toJson() { - return {'attendanceStatus': attendanceStatus?.index}; - } -} diff --git a/lib/src/models/reminder.dart b/lib/src/models/reminder.dart deleted file mode 100644 index 761ab676..00000000 --- a/lib/src/models/reminder.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class Reminder { - /// The time when the reminder should be triggered expressed in terms of minutes before the start of the event - int? minutes; - - Reminder({@required this.minutes}) - : assert(minutes != null && minutes >= 0, - 'Minutes must be greater than or equal than zero'); - - Reminder.fromJson(Map json) { - minutes = json['minutes'] as int; - } - - Map toJson() { - return {'minutes': minutes}; - } -} diff --git a/lib/src/models/result.dart b/lib/src/models/result.dart deleted file mode 100644 index fff23c71..00000000 --- a/lib/src/models/result.dart +++ /dev/null @@ -1,33 +0,0 @@ -class Result { - /// Indicates if the request was successfull or not - /// - /// Returns true if data is not null and there're no error messages, otherwise returns false - bool get isSuccess { - var res = data != null && errors.isEmpty; - if (res) { - if (data is String) { - res = (data as String).isNotEmpty; - } - } - - return res; - } - - /// Indicates if there are errors. This isn't exactly the same as !isSuccess since - /// it doesn't look at the state of the data. - /// - /// Returns true if there are error messages, otherwise false - bool get hasErrors { - return errors.isNotEmpty; - } - - T? data; - List errors = []; -} - -class ResultError { - final int errorCode; - final String errorMessage; - - const ResultError(this.errorCode, this.errorMessage); -} diff --git a/lib/src/models/retrieve_events_params.dart b/lib/src/models/retrieve_events_params.dart deleted file mode 100644 index 7965b0e1..00000000 --- a/lib/src/models/retrieve_events_params.dart +++ /dev/null @@ -1,7 +0,0 @@ -class RetrieveEventsParams { - final List? eventIds; - final DateTime? startDate; - final DateTime? endDate; - - const RetrieveEventsParams({this.eventIds, this.startDate, this.endDate}); -} diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100644 index 7c99a9c5..00000000 --- a/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: device_calendar -description: A cross platform plugin for modifying calendars on the user's device. -version: 4.3.1 -homepage: https://github.com/builttoroam/device_calendar/tree/master - -dependencies: - flutter: - sdk: flutter - collection: ^1.16.0 - timezone: ^0.9.0 - rrule: ^0.2.10 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^2.0.1 - -flutter: - plugin: - platforms: - android: - package: com.builttoroam.devicecalendar - pluginClass: DeviceCalendarPlugin - ios: - pluginClass: DeviceCalendarPlugin - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart deleted file mode 100644 index 132aad61..00000000 --- a/test/device_calendar_test.dart +++ /dev/null @@ -1,245 +0,0 @@ -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'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - const channel = MethodChannel('plugins.builttoroam.com/device_calendar'); - var deviceCalendarPlugin = DeviceCalendarPlugin(); - - final log = []; - - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - print('Calling channel method ${methodCall.method}'); - log.add(methodCall); - - return null; - }); - - log.clear(); - }); - - test('HasPermissions_Returns_Successfully', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - return true; - }); - - final result = await deviceCalendarPlugin.hasPermissions(); - expect(result.isSuccess, true); - expect(result.errors, isEmpty); - expect(result.data, true); - }); - - test('RequestPermissions_Returns_Successfully', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - return true; - }); - - final result = await deviceCalendarPlugin.requestPermissions(); - expect(result.isSuccess, true); - expect(result.errors, isEmpty); - expect(result.data, true); - }); - - test('RetrieveCalendars_Returns_Successfully', () async { - const fakeCalendarName = 'fakeCalendarName'; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - return '[{"id":"1","isReadOnly":false,"name":"$fakeCalendarName"}]'; - }); - - final result = await deviceCalendarPlugin.retrieveCalendars(); - expect(result.isSuccess, true); - expect(result.errors, isEmpty); - expect(result.data, isNotNull); - expect(result.data, isNotEmpty); - expect(result.data?[0].name, fakeCalendarName); - }); - - test('RetrieveEvents_CalendarId_IsRequired', () async { - const String? calendarId = null; - const params = RetrieveEventsParams(); - - final result = - await deviceCalendarPlugin.retrieveEvents(calendarId, params); - expect(result.isSuccess, false); - expect(result.errors.length, greaterThan(0)); - expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); - }); - - test('DeleteEvent_CalendarId_IsRequired', () async { - const String? calendarId = null; - const eventId = 'fakeEventId'; - - final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); - expect(result.isSuccess, false); - expect(result.errors.length, greaterThan(0)); - expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); - }); - - test('DeleteEvent_EventId_IsRequired', () async { - const calendarId = 'fakeCalendarId'; - const String? eventId = null; - - final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); - expect(result.isSuccess, false); - expect(result.errors.length, greaterThan(0)); - expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); - }); - - test('DeleteEvent_PassesArguments_Correctly', () async { - const calendarId = 'fakeCalendarId'; - const eventId = 'fakeEventId'; - - await deviceCalendarPlugin.deleteEvent(calendarId, eventId); - expect(log, [ - isMethodCall('deleteEvent', arguments: { - 'calendarId': calendarId, - 'eventId': eventId - }) - ]); - }); - - test('CreateEvent_Arguments_Invalid', () async { - const String? fakeCalendarId = null; - final event = Event(fakeCalendarId); - - final result = await deviceCalendarPlugin.createOrUpdateEvent(event); - expect(result!.isSuccess, false); - expect(result.errors, isNotEmpty); - expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); - }); - - test('CreateEvent_Returns_Successfully', () async { - const fakeNewEventId = 'fakeNewEventId'; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - return fakeNewEventId; - }); - - const fakeCalendarId = 'fakeCalendarId'; - final event = Event(fakeCalendarId); - event.title = 'fakeEventTitle'; - event.start = TZDateTime.now(local); - event.end = event.start!.add(const Duration(hours: 1)); - - final result = await deviceCalendarPlugin.createOrUpdateEvent(event); - expect(result?.isSuccess, true); - expect(result?.errors, isEmpty); - expect(result?.data, isNotEmpty); - expect(result?.data, fakeNewEventId); - }); - - test('UpdateEvent_Returns_Successfully', () async { - const fakeNewEventId = 'fakeNewEventId'; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - final arguments = methodCall.arguments as Map; - if (!arguments.containsKey('eventId') || arguments['eventId'] == null) { - return null; - } - - return fakeNewEventId; - }); - - const fakeCalendarId = 'fakeCalendarId'; - final event = Event(fakeCalendarId); - event.eventId = 'fakeEventId'; - event.title = 'fakeEventTitle'; - event.start = TZDateTime.now(local); - event.end = event.start!.add(const Duration(hours: 1)); - - final result = await deviceCalendarPlugin.createOrUpdateEvent(event); - expect(result?.isSuccess, true); - expect(result?.errors, isEmpty); - expect(result?.data, isNotEmpty); - expect(result?.data, fakeNewEventId); - }); - - test('Attendee_Serialises_Correctly', () async { - final attendee = Attendee( - name: 'Test Attendee', - emailAddress: 'test@t.com', - role: AttendeeRole.Required, - isOrganiser: true); - final stringAttendee = attendee.toJson(); - expect(stringAttendee, isNotNull); - final newAttendee = Attendee.fromJson(stringAttendee); - expect(newAttendee, isNotNull); - expect(newAttendee.name, equals(attendee.name)); - expect(newAttendee.emailAddress, equals(attendee.emailAddress)); - expect(newAttendee.role, equals(attendee.role)); - expect(newAttendee.isOrganiser, equals(attendee.isOrganiser)); - expect(newAttendee.iosAttendeeDetails, isNull); - expect(newAttendee.androidAttendeeDetails, isNull); - }); - - test('Event_Serializes_Correctly', () async { - final startTime = TZDateTime( - timeZoneDatabase.locations.entries.skip(20).first.value, - 1980, - 10, - 1, - 0, - 0, - 0); - final endTime = TZDateTime( - timeZoneDatabase.locations.entries.skip(21).first.value, - 1980, - 10, - 2, - 0, - 0, - 0); - final attendee = Attendee( - name: 'Test Attendee', - emailAddress: 'test@t.com', - role: AttendeeRole.Required, - isOrganiser: true); - final recurrence = RecurrenceRule(frequency: Frequency.daily); - final reminder = Reminder(minutes: 10); - var event = Event('calendarId', - eventId: 'eventId', - title: 'Test Event', - start: startTime, - location: 'Seattle, Washington', - url: Uri.dataFromString('http://www.example.com'), - end: endTime, - attendees: [attendee], - description: 'Test description', - recurrenceRule: recurrence, - reminders: [reminder], - availability: Availability.Busy, - status: EventStatus.Confirmed); - - final stringEvent = event.toJson(); - expect(stringEvent, isNotNull); - final newEvent = Event.fromJson(stringEvent); - expect(newEvent, isNotNull); - expect(newEvent.calendarId, equals(event.calendarId)); - expect(newEvent.eventId, equals(event.eventId)); - expect(newEvent.title, equals(event.title)); - expect(newEvent.start!.millisecondsSinceEpoch, - equals(event.start!.millisecondsSinceEpoch)); - expect(newEvent.end!.millisecondsSinceEpoch, - equals(event.end!.millisecondsSinceEpoch)); - expect(newEvent.description, equals(event.description)); - expect(newEvent.url, equals(event.url)); - expect(newEvent.location, equals(event.location)); - expect(newEvent.attendees, isNotNull); - expect(newEvent.attendees?.length, equals(1)); - expect(newEvent.recurrenceRule, isNotNull); - expect(newEvent.recurrenceRule?.frequency, - equals(event.recurrenceRule?.frequency)); - expect(newEvent.reminders, isNotNull); - expect(newEvent.reminders?.length, equals(1)); - expect(newEvent.availability, equals(event.availability)); - expect(newEvent.status, equals(event.status)); - }); -} From 9d85bebdd79ac992991fab108442fe7ab42ee5de Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Tue, 28 Nov 2023 11:29:47 +0100 Subject: [PATCH 03/11] Added event color field. --- device_calendar/CHANGELOG.md | 200 +++ device_calendar/LICENSE | 27 + device_calendar/README.md | 121 ++ device_calendar/analysis_options.yaml | 30 + device_calendar/android/.gitignore | 8 + device_calendar/android/build.gradle | 58 + device_calendar/android/gradle.properties | 3 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54708 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + device_calendar/android/gradlew | 172 +++ device_calendar/android/gradlew.bat | 84 ++ device_calendar/android/proguard-rules.pro | 1 + device_calendar/android/settings.gradle | 1 + .../android/src/main/AndroidManifest.xml | 3 + .../devicecalendar/AvailabilitySerializer.kt | 18 + .../devicecalendar/CalendarDelegate.kt | 1268 +++++++++++++++++ .../devicecalendar/DeviceCalendarPlugin.kt | 301 ++++ .../devicecalendar/EventStatusSerializer.kt | 15 + .../devicecalendar/common/Constants.kt | 117 ++ .../devicecalendar/common/ErrorCodes.kt | 11 + .../devicecalendar/common/ErrorMessages.kt | 16 + .../devicecalendar/models/Attendee.kt | 10 + .../devicecalendar/models/Availability.kt | 7 + .../devicecalendar/models/Calendar.kt | 13 + .../CalendarMethodsParametersCacheModel.kt | 16 + .../devicecalendar/models/Event.kt | 23 + .../devicecalendar/models/EventStatus.kt | 7 + .../devicecalendar/models/RecurrenceRule.kt | 17 + .../devicecalendar/models/Reminder.kt | 3 + device_calendar/device_calendar.iml | 19 + device_calendar/device_calendar_android.iml | 30 + .../example/.flutter-plugins-dependencies | 1 + device_calendar/example/.gitignore | 9 + device_calendar/example/.metadata | 8 + device_calendar/example/README.md | 195 +++ device_calendar/example/analysis_options.yaml | 30 + device_calendar/example/android/.gitignore | 10 + .../example/android/app/build.gradle | 60 + .../example/android/app/proguard-rules.pro | 1 + .../android/app/src/main/AndroidManifest.xml | 44 + .../devicecalendarexample/MainActivity.kt | 6 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values/styles.xml | 8 + device_calendar/example/android/build.gradle | 29 + .../example/android/gradle.properties | 3 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + device_calendar/example/android/gradlew | 160 +++ device_calendar/example/android/gradlew.bat | 90 ++ .../example/android/settings.gradle | 15 + .../example/device_calendar_example.iml | 17 + .../device_calendar_example_android.iml | 27 + .../example/integration_test/app_test.dart | 89 ++ .../integration_test/integration_test.dart | 8 + .../integration_test_android.dart | 29 + .../example/integration_test/ios.sh | 24 + device_calendar/example/ios/.gitignore | 45 + .../ios/Flutter/AppFrameworkInfo.plist | 30 + .../example/ios/Flutter/Debug.xcconfig | 2 + .../example/ios/Flutter/Release.xcconfig | 2 + .../ios/Flutter/flutter_export_environment.sh | 13 + device_calendar/example/ios/Podfile | 45 + device_calendar/example/ios/Podfile.lock | 34 + .../ios/Runner.xcodeproj/project.pbxproj | 507 +++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 11112 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + device_calendar/example/ios/Runner/Info.plist | 55 + .../ios/Runner/Runner-Bridging-Header.h | 1 + .../example/lib/common/app_routes.dart | 3 + device_calendar/example/lib/main.dart | 29 + .../lib/presentation/date_time_picker.dart | 81 ++ .../example/lib/presentation/event_item.dart | 342 +++++ .../lib/presentation/input_dropdown.dart | 42 + .../lib/presentation/pages/calendar_add.dart | 164 +++ .../presentation/pages/calendar_event.dart | 1261 ++++++++++++++++ .../presentation/pages/calendar_events.dart | 190 +++ .../lib/presentation/pages/calendars.dart | 161 +++ .../presentation/pages/event_attendee.dart | 174 +++ .../presentation/pages/event_reminders.dart | 102 ++ .../presentation/recurring_event_dialog.dart | 101 ++ device_calendar/example/pubspec.yaml | 27 + device_calendar/ios/.gitignore | 36 + device_calendar/ios/Assets/.gitkeep | 0 .../ios/Classes/DeviceCalendarPlugin.h | 4 + .../ios/Classes/DeviceCalendarPlugin.m | 8 + .../Classes/SwiftDeviceCalendarPlugin.swift | 1136 +++++++++++++++ device_calendar/ios/device_calendar.podspec | 22 + device_calendar/lib/device_calendar.dart | 17 + .../lib/src/common/calendar_enums.dart | 315 ++++ .../lib/src/common/channel_constants.dart | 26 + .../lib/src/common/error_codes.dart | 6 + .../lib/src/common/error_messages.dart | 27 + device_calendar/lib/src/device_calendar.dart | 443 ++++++ device_calendar/lib/src/models/attendee.dart | 81 ++ device_calendar/lib/src/models/calendar.dart | 56 + device_calendar/lib/src/models/event.dart | 306 ++++ .../android/attendance_status.dart | 15 + .../android/attendee_details.dart | 23 + .../ios/attendance_status.dart | 18 + .../ios/attendee_details.dart | 21 + device_calendar/lib/src/models/reminder.dart | 18 + device_calendar/lib/src/models/result.dart | 33 + .../src/models/retrieve_events_params.dart | 7 + device_calendar/pubspec.yaml | 29 + .../test/device_calendar_test.dart | 245 ++++ 137 files changed, 10134 insertions(+) create mode 100644 device_calendar/CHANGELOG.md create mode 100644 device_calendar/LICENSE create mode 100644 device_calendar/README.md create mode 100644 device_calendar/analysis_options.yaml create mode 100644 device_calendar/android/.gitignore create mode 100644 device_calendar/android/build.gradle create mode 100644 device_calendar/android/gradle.properties create mode 100644 device_calendar/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 device_calendar/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 device_calendar/android/gradlew create mode 100644 device_calendar/android/gradlew.bat create mode 100644 device_calendar/android/proguard-rules.pro create mode 100644 device_calendar/android/settings.gradle create mode 100644 device_calendar/android/src/main/AndroidManifest.xml create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt create mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt create mode 100644 device_calendar/device_calendar.iml create mode 100644 device_calendar/device_calendar_android.iml create mode 100644 device_calendar/example/.flutter-plugins-dependencies create mode 100644 device_calendar/example/.gitignore create mode 100644 device_calendar/example/.metadata create mode 100644 device_calendar/example/README.md create mode 100644 device_calendar/example/analysis_options.yaml create mode 100644 device_calendar/example/android/.gitignore create mode 100644 device_calendar/example/android/app/build.gradle create mode 100644 device_calendar/example/android/app/proguard-rules.pro create mode 100644 device_calendar/example/android/app/src/main/AndroidManifest.xml create mode 100644 device_calendar/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt create mode 100644 device_calendar/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 device_calendar/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 device_calendar/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 device_calendar/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 device_calendar/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 device_calendar/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 device_calendar/example/android/app/src/main/res/values/styles.xml create mode 100644 device_calendar/example/android/build.gradle create mode 100644 device_calendar/example/android/gradle.properties create mode 100644 device_calendar/example/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 device_calendar/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 device_calendar/example/android/gradlew create mode 100644 device_calendar/example/android/gradlew.bat create mode 100644 device_calendar/example/android/settings.gradle create mode 100644 device_calendar/example/device_calendar_example.iml create mode 100644 device_calendar/example/device_calendar_example_android.iml create mode 100644 device_calendar/example/integration_test/app_test.dart create mode 100644 device_calendar/example/integration_test/integration_test.dart create mode 100644 device_calendar/example/integration_test/integration_test_android.dart create mode 100755 device_calendar/example/integration_test/ios.sh create mode 100755 device_calendar/example/ios/.gitignore create mode 100755 device_calendar/example/ios/Flutter/AppFrameworkInfo.plist create mode 100755 device_calendar/example/ios/Flutter/Debug.xcconfig create mode 100755 device_calendar/example/ios/Flutter/Release.xcconfig create mode 100755 device_calendar/example/ios/Flutter/flutter_export_environment.sh create mode 100644 device_calendar/example/ios/Podfile create mode 100755 device_calendar/example/ios/Podfile.lock create mode 100644 device_calendar/example/ios/Runner.xcodeproj/project.pbxproj create mode 100755 device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 device_calendar/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100755 device_calendar/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100755 device_calendar/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 device_calendar/example/ios/Runner/AppDelegate.swift create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100755 device_calendar/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100755 device_calendar/example/ios/Runner/Base.lproj/Main.storyboard create mode 100755 device_calendar/example/ios/Runner/Info.plist create mode 100755 device_calendar/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 device_calendar/example/lib/common/app_routes.dart create mode 100644 device_calendar/example/lib/main.dart create mode 100644 device_calendar/example/lib/presentation/date_time_picker.dart create mode 100644 device_calendar/example/lib/presentation/event_item.dart create mode 100644 device_calendar/example/lib/presentation/input_dropdown.dart create mode 100644 device_calendar/example/lib/presentation/pages/calendar_add.dart create mode 100644 device_calendar/example/lib/presentation/pages/calendar_event.dart create mode 100644 device_calendar/example/lib/presentation/pages/calendar_events.dart create mode 100644 device_calendar/example/lib/presentation/pages/calendars.dart create mode 100644 device_calendar/example/lib/presentation/pages/event_attendee.dart create mode 100644 device_calendar/example/lib/presentation/pages/event_reminders.dart create mode 100644 device_calendar/example/lib/presentation/recurring_event_dialog.dart create mode 100644 device_calendar/example/pubspec.yaml create mode 100644 device_calendar/ios/.gitignore create mode 100644 device_calendar/ios/Assets/.gitkeep create mode 100644 device_calendar/ios/Classes/DeviceCalendarPlugin.h create mode 100644 device_calendar/ios/Classes/DeviceCalendarPlugin.m create mode 100644 device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift create mode 100644 device_calendar/ios/device_calendar.podspec create mode 100644 device_calendar/lib/device_calendar.dart create mode 100644 device_calendar/lib/src/common/calendar_enums.dart create mode 100644 device_calendar/lib/src/common/channel_constants.dart create mode 100644 device_calendar/lib/src/common/error_codes.dart create mode 100644 device_calendar/lib/src/common/error_messages.dart create mode 100644 device_calendar/lib/src/device_calendar.dart create mode 100644 device_calendar/lib/src/models/attendee.dart create mode 100644 device_calendar/lib/src/models/calendar.dart create mode 100644 device_calendar/lib/src/models/event.dart create mode 100644 device_calendar/lib/src/models/platform_specifics/android/attendance_status.dart create mode 100644 device_calendar/lib/src/models/platform_specifics/android/attendee_details.dart create mode 100644 device_calendar/lib/src/models/platform_specifics/ios/attendance_status.dart create mode 100644 device_calendar/lib/src/models/platform_specifics/ios/attendee_details.dart create mode 100644 device_calendar/lib/src/models/reminder.dart create mode 100644 device_calendar/lib/src/models/result.dart create mode 100644 device_calendar/lib/src/models/retrieve_events_params.dart create mode 100644 device_calendar/pubspec.yaml create mode 100644 device_calendar/test/device_calendar_test.dart diff --git a/device_calendar/CHANGELOG.md b/device_calendar/CHANGELOG.md new file mode 100644 index 00000000..2fa175fa --- /dev/null +++ b/device_calendar/CHANGELOG.md @@ -0,0 +1,200 @@ +# Changelog + + + +## [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 + - *Note:* `timezone 0.9.0` [removed named database files](https://pub.dev/packages/timezone/changelog#090). If you are only using `device_calendar`, you can ignore this note. +- Added support for all-day multi-day events on iOS +- Fixed iOS issue of adding attendees to events +- Fixed Android issue of the `ownerAccount` being null + +## [4.2.0](https://github.com/builttoroam/device_calendar/releases/tag/4.2.0) + +- Fix: apks can be build correctly now +- Support for viewing and editing attendee status + - iOS needs a specific native view and permissions to edit attendees due to iOS restrictions. See README and example app. + +## [4.1.0](https://github.com/builttoroam/device_calendar/releases/tag/4.1.0) + +- Fix: title, descriptions etc are now retrieved properly. +- Fix: Event JSONs created and are now readable. Previous (mislabeled) JSONs are also readable with warnings. +- Fix: removed depreceated plugins from Example. +- Integration tests are now working. Android instructions are ready. +- Gradle plug-ins are updated. +- Compiles with jvm 1.8, should be compilable for Flutter 2.9+ +- Android: proper support for all day events, and multi-day all day events. + +## [4.0.1](https://github.com/builttoroam/device_calendar/releases/tag/4.0.1) + +- Fix: event time are now properly retrieved + +## [4.0.0](https://github.com/builttoroam/device_calendar/releases/tag/4.0.0) + +- Timezone plugin and logic implemented. All issues related to timezone shoulde be fixed. +- Events.availability defaults to busy when not specified [354](https://github.com/builttoroam/device_calendar/pull/354) +- Events parameter now includes location and url. [319](https://github.com/builttoroam/device_calendar/pull/319) +- Android: Fixed bug where platform exception appeared, when Events.availability was null on Event [241](https://github.com/builttoroam/device_calendar/issues/241) +- Fixed various issues in example [270](https://github.com/builttoroam/device_calendar/issues/270), [268](https://github.com/builttoroam/device_calendar/issues/268) +- Android: deleteEvent code aligned with flutter [258](https://github.com/builttoroam/device_calendar/issues/258) +- Android: Updated to V2 embeddding [326](https://github.com/builttoroam/device_calendar/issues/326) +- iOS: Updated swift versions, possibly improved compability with Obj-C [flutter/flutter#16049 (comment)](https://github.com/flutter/flutter/issues/16049#issuecomment-611192738) + +## [3.9.0](https://github.com/builttoroam/device_calendar/releases/tag/3.9.0) + +- Migrated to null safety +- Updated multiple underlying dependencies +- Rebuilt iOS podfile +- Upgraded to new Android plugins APIs for flutter + +## 3.1.0 25th March 2020 - Bug fixes and new features + +- Boolean variable `isDefault` added for issue [145](https://github.com/builttoroam/device_calendar/issues/145) (**NOTE**: This is not supported Android API 16 or lower, `isDefault` will always be false) +- Events with 'null' title now defaults to 'New Event', issue [126](https://github.com/builttoroam/device_calendar/issues/126) +- Updated property summaries for issues [121](https://github.com/builttoroam/device_calendar/issues/121) and [122](https://github.com/builttoroam/device_calendar/issues/122) +- Updated example documentation for issue [119](https://github.com/builttoroam/device_calendar/issues/119) +- Read-only calendars cannot be edited or deleted for the example app +- Added `DayOfWeekGroup` enum and an extension `getDays` to get corresponding dates of the enum values +- Added to retrieve colour for calendars. Thanks to [nadavfima](https://github.com/nadavfima) for the contribution and PR to add colour support for both Android and iOS +- Added compatibility with a new Flutter plugin for Android. Thanks to the PR submitted by [RohitKumarMishra](https://github.com/RohitKumarMishra) +- [Android] Fixed all day timezone issue [164](https://github.com/builttoroam/device_calendar/issues/164) +- Added support for deleting individual or multiple instances of a recurring event for issue [108](https://github.com/builttoroam/device_calendar/issues/108) +- Ability to add local calendars with a desired colour for issue [115](https://github.com/builttoroam/device_calendar/issues/115) +- Returns account name and type for each calendars for issue [179](https://github.com/builttoroam/device_calendar/issues/179) + +## 3.0.0+3 3rd February 2020 + +- Fixed all day conditional check for issue [162](https://github.com/builttoroam/device_calendar/issues/162) + +## 3.0.0+2 30th January 2020 + +- Updated `event.allDay` property in `createOrUpdateEvent` method to be null-aware + +## 3.0.0+1 28th January 2020 + +- Updated `event.url` property in `createOrUpdateEvent` method to be null-aware for issue [152](https://github.com/builttoroam/device_calendar/issues/152) + +## 3.0.0 21st January 2020 + +- **BREAKING CHANGE** Properties for the attendee model in `attendee.dart` file have been changed: + - Boolean property `isRequired` has been replaced to `AttendeeRole` enum + - New arugment added for `AttendeeRole` property +- **BREAKING CHANGE** Package updates: + - [Android] Updated Gradle plugin to 3.5.2 and Gradle wrapper to 5.4.1 + - [iOS] Updated Swift to 5 +- `name` and `isOrganiser` (read-only) properties have been added +- Attendee UI update for the example app +- Ability to add, modify or remove an attendee + +## 2.0.0 17th January 2020 + +- **BREAKING CHANGE** The recurrence models in `recurrence_rule.dart` file have been chaged +- **BREAKING CHANGE** All articles used in property names or arugments have been removed (i.e. enum `DayOfTheWeek` to `DayOfWeek`) +- Recurrence fix for monthly and yearly frequencies +- UI update for the example app +- Add support for all day events + +## 1.0.0+3 9th January 2020 + +- Flutter upgrade to 1.12.13 +- Added an URL input for calendar events for issue [132](https://github.com/builttoroam/device_calendar/issues/132) + +## 1.0.0+2 30th August 2019 + +- Fix home page URL + +## 1.0.0+1 30th August 2019 + +- Add integration tests to example app. Note that this is more for internal use at the moment as it currently requires an Android device with a calendar that can be written to and assumes that the tests are executed from a Mac. + +## 1.0.0 28th August 2019 + +- **BREAKING CHANGE** `retrieveCalendars` and `retrieveEvents` now return lists that cannot be modified (`UnmodifiableListView`) to address part of issue [113](https://github.com/builttoroam/device_calendar/issues/113) +- Support for more advanced recurrence rules +- Update README to include information about using ProGuard for issue [99](https://github.com/builttoroam/device_calendar/issues/99) +- Made event title optional to fix issue [72](https://github.com/builttoroam/device_calendar/issues/72) +- Return information about the organiser of the event as per issue [73](https://github.com/builttoroam/device_calendar/issues/73) +- Return attendance status of attendees and if they're required for an event. These are details are different across iOS and Android and so are returned within platform-specific objects +- Ability to modify attendees for an event +- Ability to create reminders for events expressed in minutes before the event starts + +## 0.2.2 19th August 2019 + +- Add support for specifying the location of an event. Thanks to [oli06](https://github.com/oli06) and [zemanux](https://github.com/zemanux) for submitting PRs to add the functionality to iOS and Android respectively + +## 0.2.1+1 5th August 2019 + +- Fixing date in changelog for version 0.2.1 + +## 0.2.1 5th August 2019 + +- [Android] Fixes issue [101](https://github.com/builttoroam/device_calendar/issues/101) where plugin results in a crash with headless execution + +## 0.2.0 30th July 2019 + +- **BREAKING CHANGE** [Android] Updated to use Gradle plugin to 3.4.2, Gradle wrapper to 5.1.1, Kotlin version to 1.3.41 and bumped Android dependencies +- Add initial support for recurring events. Note that currently editing or deleting a recurring event will affect all instances of it. Future releases will look at supporting more advanced recurrence rules +- Remove old example app to avoid confusion + +## 0.1.3 5th July 2019 + +- [iOS] Fixes issue [94](https://github.com/builttoroam/device_calendar/issues/94) that occurred on 32-bit iOS devices around date of events. Thanks to the PR submitted by [duzenko](https://github.com/duzenko) + +## 0.1.2+2 28th May 2019 + +- Non-functional release. Minor refactoring in Android code to address issues found in Codefactor and fix build status badge in README + +## 0.1.2+1 17th May 2019 + +- Non-functional release. Fixed formatting in changelog and code comments +- Added more info about potential issues in consuming the plugin within an Objective-C project + +## 0.1.2 - 16th May 2019 + +- [Android] An updated fix to address issue [79](https://github.com/builttoroam/device_calendar/issues/79), thanks to the PR submitted by [Gerry High](https://github.com/gerryhigh) + +## 0.1.1 - 1st March 2019 + +- Fixed issue [79](https://github.com/builttoroam/device_calendar/issues/79) where on Android, the plugin was indicating that it was handling permissions that it shouldn't have + +## 0.1.0 - 26th February 2019 + +- **BREAKING CHANGE** Migrated to the plugin to use AndroidX instead of the deprecated Android support libraries. Please ensure you have migrated your application following the guide [here](https://developer.android.com/jetpack/androidx/migrate) +- **BREAKING CHANGE** Updated Kotlin to version 1.3.21 +- **BREAKING CHANGE** Updated Gradle plugin to 3.3.1 and distribution to 4.10.2 + +## 0.0.8 - 26th February 2019 + +- This was a breaking change that should've been incremented as minor version update instead of a patch version update. See changelog for 0.1.0 for the details of this update + +## 0.0.7 - 16th November 2018 + +- Fixes issue [##67](https://github.com/builttoroam/device_calendar/issues/67) and [##68](https://github.com/builttoroam/device_calendar/issues/68). Thanks to PR submitted by huzhiren. + +## 0.0.6 - 18th June 2018 + +- [iOS] Fix an issue when adding/updating an event with a null description + +## 0.0.5 - 14th June 2018 + +- [Android] Fixed an issue with retrieving events by id only + +## 0.0.4 - 12th June 2018 + +- Reordering changelog +- Creating new example for the Pub Dart Example tab +- Moving existing example to the example_app GitHub folder + +## 0.0.2 - 0.0.3 - 7th June 2018 + +- Fixing incorrect Travis build links + +## 0.0.1 - 7th June 2018 + +- Ability to retrieve device calendars +- CRUD operations on calendar events diff --git a/device_calendar/LICENSE b/device_calendar/LICENSE new file mode 100644 index 00000000..81f1b5da --- /dev/null +++ b/device_calendar/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2018 Built to Roam. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Built to Roam nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/device_calendar/README.md b/device_calendar/README.md new file mode 100644 index 00000000..2af1e99d --- /dev/null +++ b/device_calendar/README.md @@ -0,0 +1,121 @@ +# Device Calendar Plugin + +[![pub package](https://img.shields.io/pub/v/device_calendar.svg)](https://pub.dartlang.org/packages/device_calendar) ![Pub Version (including pre-releases)](https://img.shields.io/pub/v/device_calendar?include_prereleases&label=Prerelease) [![build](https://github.com/builttoroam/device_calendar/actions/workflows/dart.yml/badge.svg?branch=develop)](https://github.com/builttoroam/device_calendar/actions/workflows/dart.yml) + +A cross platform plugin for modifying calendars on the user's device. + +## Breaking changes at v4 + +* **If you're upgrading from previous versions, your code will need to be modified (slightly), otherwise it will not run after update. See [Timezone support](https://github.com/builttoroam/device_calendar#timezone-support-with-tzdatetime) for more details.** +* **There are some changes to event JSON formats at v4. Pay extra care if you handle event JSONs. Directly calling to and from device calendars should be unaffected.** + +## Features + +* Request permissions to modify calendars on the user's device +* Check if permissions to modify the calendars on the user's device have been granted +* Add or retrieve calendars on the user's device +* Retrieve events associated with a calendar +* Add, update or delete events from a calendar +* Set up, edit or delete recurring events + * **NOTE**: Editing a recurring event will currently edit all instances of it + * **NOTE**: Deleting multiple instances in **Android** takes time to update, you'll see the changes after a few seconds +* Add, modify or remove attendees and receive if an attendee is an organiser for an event +* Setup reminders for an event +* Specify a time zone for event start and end date + * **NOTE**: Due to a limitation of iOS API, single time zone property is used for iOS (`event.startTimeZone`) + * **NOTE**: For the time zone list, please refer to the `TZ database name` column on [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) + +## Timezone support with TZDateTime + +Due to feedback we received, starting from `4.0.0` we will be using the `timezone` package to better handle all timezone data. + +This is already included in this package. However, you need to add this line whenever the package is needed. + +```dart +import 'package:timezone/timezone.dart'; +``` + +If you don't need any timezone specific features in your app, you may use `flutter_native_timezone` to get your devices' current timezone, then convert your previous `DateTime` with it. + +```dart +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; + +initializeTimeZones(); + +// As an example, our default timezone is UTC. +Location _currentLocation = getLocation('Etc/UTC'); + +Future setCurentLocation() async { + String timezone = 'Etc/UTC'; + try { + timezone = await FlutterNativeTimezone.getLocalTimezone(); + } catch (e) { + print('Could not get the local timezone'); + } + _currentLocation = getLocation(timezone); + setLocalLocation(_currentLocation); +} + +... + +event.start = TZDateTime.from(oldDateTime, _currentLocation); +``` + +For other use cases, feedback or future developments on the feature, feel free to open a discussion on GitHub. + +## Null-safety migration + +From `v3.9.0`, device_calendar is null-safe. However, not all workflows have been checked and bugs from older versions still persist. + +You are strongly advised to test your workflow with the new package before shipping. +Better yet, please leave a note for what works and what doesn't, or contribute some bug fixes! + +## Android Integration + +The following will need to be added to the `AndroidManifest.xml` file for your application to indicate permissions to modify calendars are needed + +```xml + + +``` + +### 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()`. + +You may add the following setting to the ProGuard rules file `proguard-rules.pro` (thanks to [Britannio Jarrett](https://github.com/britannio)). Read more about the issue [here](https://github.com/builttoroam/device_calendar/issues/99) + +``` +-keep class com.builttoroam.devicecalendar.** { *; } +``` + +See [here](https://github.com/builttoroam/device_calendar/issues/99#issuecomment-612449677) for an example setup. + +For more information, refer to the guide at [Android Developer](https://developer.android.com/studio/build/shrink-code#keep-code) + +### AndroidX migration + +Since `v.1.0`, this version has migrated to use AndroidX instead of the deprecated Android support libraries. When using `0.10.0` and onwards for this plugin, please ensure your application has been migrated following the guide [here](https://developer.android.com/jetpack/androidx/migrate) + +## iOS Integration + +For iOS 10+ support, you'll need to modify the `Info.plist` to add the following key/value pair + +```xml +NSCalendarsUsageDescription +Access most functions for calendar viewing and editing. + +NSContactsUsageDescription +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/device_calendar/analysis_options.yaml b/device_calendar/analysis_options.yaml new file mode 100644 index 00000000..68a79339 --- /dev/null +++ b/device_calendar/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/device_calendar/android/.gitignore b/device_calendar/android/.gitignore new file mode 100644 index 00000000..c6cbe562 --- /dev/null +++ b/device_calendar/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/device_calendar/android/build.gradle b/device_calendar/android/build.gradle new file mode 100644 index 00000000..1f5ff500 --- /dev/null +++ b/device_calendar/android/build.gradle @@ -0,0 +1,58 @@ +group 'com.builttoroam.devicecalendar' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.6.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 19 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard-rules.pro' + } + lintOptions { + disable 'InvalidPackage' + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + namespace 'com.builttoroam.devicecalendar' +} + +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.12.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' +} diff --git a/device_calendar/android/gradle.properties b/device_calendar/android/gradle.properties new file mode 100644 index 00000000..4d3226ab --- /dev/null +++ b/device_calendar/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/device_calendar/android/gradle/wrapper/gradle-wrapper.jar b/device_calendar/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 GIT binary patch literal 54708 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2girk4u zvO<3q)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^ShTtO;VyD{dezY;XD@Rwl_9#j4Uo!1W&ZHVe0H>f=h#9k>~KUj^iUJ%@wU{Xuy z3FItk0<;}6D02$u(RtEY#O^hrB>qgxnOD^0AJPGC9*WXw_$k%1a%-`>uRIeeAIf3! zbx{GRnG4R$4)3rVmg63gW?4yIWW_>;t3>4@?3}&ct0Tk}<5ljU>jIN1 z&+mzA&1B6`v(}i#vAzvqWH~utZzQR;fCQGLuCN|p0hey7iCQ8^^dr*hi^wC$bTk`8M(JRKtQuXlSf$d(EISvuY0dM z7&ff;p-Ym}tT8^MF5ACG4sZmAV!l;0h&Mf#ZPd--_A$uv2@3H!y^^%_&Iw$*p79Uc5@ZXLGK;edg%)6QlvrN`U7H@e^P*0Atd zQB%>4--B1!9yeF(3vk;{>I8+2D;j`zdR8gd8dHuCQ_6|F(5-?gd&{YhLeyq_-V--4 z(SP#rP=-rsSHJSHDpT1{dMAb7-=9K1-@co_!$dG^?c(R-W&a_C5qy2~m3@%vBGhgnrw|H#g9ABb7k{NE?m4xD?;EV+fPdE>S2g$U(&_zGV+TPvaot>W_ zf8yY@)yP8k$y}UHVgF*uxtjW2zX4Hc3;W&?*}K&kqYpi%FHarfaC$ETHpSoP;A692 zR*LxY1^BO1ry@7Hc9p->hd==U@cuo*CiTnozxen;3Gct=?{5P94TgQ(UJoBb`7z@BqY z;q&?V2D1Y%n;^Dh0+eD)>9<}=A|F5{q#epBu#sf@lRs`oFEpkE%mrfwqJNFCpJC$| zy6#N;GF8XgqX(m2yMM2yq@TxStIR7whUIs2ar$t%Avh;nWLwElVBSI#j`l2$lb-!y zK|!?0hJ1T-wL{4uJhOFHp4?@28J^Oh61DbeTeSWub(|dL-KfxFCp0CjQjV`WaPW|U z=ev@VyC>IS@{ndzPy||b3z-bj5{Y53ff}|TW8&&*pu#?qs?)#&M`ACfb;%m+qX{Or zb+FNNHU}mz!@!EdrxmP_6eb3Cah!mL0ArL#EA1{nCY-!jL8zzz7wR6wAw(8K|IpW; zUvH*b1wbuRlwlUt;dQhx&pgsvJcUpm67rzkNc}2XbC6mZAgUn?VxO6YYg=M!#e=z8 zjX5ZLyMyz(VdPVyosL0}ULO!Mxu>hh`-MItnGeuQ;wGaU0)gIq3ZD=pDc(Qtk}APj z#HtA;?idVKNF)&0r|&w#l7DbX%b91b2;l2=L8q#}auVdk{RuYn3SMDo1%WW0tD*62 zaIj65Y38;?-~@b82AF!?Nra2;PU)t~qYUhl!GDK3*}%@~N0GQH7zflSpfP-ydOwNe zOK~w((+pCD&>f!b!On);5m+zUBFJtQ)mV^prS3?XgPybC2%2LiE5w+S4B|lP z+_>3$`g=%P{IrN|1Oxz30R{kI`}ZL!r|)RS@8Do;ZD3_=PbBrrP~S@EdsD{V+`!4v z{MSF}j!6odl33rA+$odIMaK%ersg%xMz>JQ^R+!qNq$5S{KgmGN#gAApX*3ib)TDsVVi>4ypIX|Ik4d6E}v z=8+hs9J=k3@Eiga^^O|ESMQB-O6i+BL*~*8coxjGs{tJ9wXjGZ^Vw@j93O<&+bzAH z9+N^ALvDCV<##cGoo5fX;wySGGmbH zHsslio)cxlud=iP2y=nM>v8vBn*hJ0KGyNOy7dr8yJKRh zywBOa4Lhh58y06`5>ESYXqLt8ZM1axd*UEp$wl`APU}C9m1H8-ModG!(wfSUQ%}rT3JD*ud~?WJdM}x>84)Cra!^J9wGs6^G^ze~eV(d&oAfm$ z_gwq4SHe=<#*FN}$5(0d_NumIZYaqs|MjFtI_rJb^+ZO?*XQ*47mzLNSL7~Nq+nw8 zuw0KwWITC43`Vx9eB!0Fx*CN9{ea$xjCvtjeyy>yf!ywxvv6<*h0UNXwkEyRxX{!e$TgHZ^db3r;1qhT)+yt@|_!@ zQG2aT`;lj>qjY`RGfQE?KTt2mn=HmSR>2!E38n8PlFs=1zsEM}AMICb z86Dbx(+`!hl$p=Z)*W~+?_HYp+CJacrCS-Fllz!7E>8*!E(yCh-cWbKc7)mPT6xu= zfKpF3I+p%yFXkMIq!ALiXF89-aV{I6v+^k#!_xwtQ*Nl#V|hKg=nP=fG}5VB8Ki7) z;19!on-iq&Xyo#AowvpA)RRgF?YBdDc$J8*)2Wko;Y?V6XMOCqT(4F#U2n1jg*4=< z8$MfDYL|z731iEKB3WW#kz|c3qh7AXjyZ}wtSg9xA(ou-pLoxF{4qk^KS?!d3J0!! zqE#R9NYGUyy>DEs%^xW;oQ5Cs@fomcrsN}rI2Hg^6y9kwLPF`K3llX00aM_r)c?ay zevlHA#N^8N+AI=)vx?4(=?j^ba^{umw140V#g58#vtnh8i7vRs*UD=lge;T+I zl1byCNr5H%DF58I2(rk%8hQ;zuCXs=sipbQy?Hd;umv4!fav@LE4JQ^>J{aZ=!@Gc~p$JudMy%0{=5QY~S8YVP zaP6gRqfZ0>q9nR3p+Wa8icNyl0Zn4k*bNto-(+o@-D8cd1Ed7`}dN3%wezkFxj_#_K zyV{msOOG;n+qbU=jBZk+&S$GEwJ99zSHGz8hF1`Xxa^&l8aaD8OtnIVsdF0cz=Y)? zP$MEdfKZ}_&#AC)R%E?G)tjrKsa-$KW_-$QL}x$@$NngmX2bHJQG~77D1J%3bGK!- zl!@kh5-uKc@U4I_Er;~epL!gej`kdX>tSXVFP-BH#D-%VJOCpM(-&pOY+b#}lOe)Z z0MP5>av1Sy-dfYFy%?`p`$P|`2yDFlv(8MEsa++Qv5M?7;%NFQK0E`Ggf3@2aUwtBpCoh`D}QLY%QAnJ z%qcf6!;cjOTYyg&2G27K(F8l^RgdV-V!~b$G%E=HP}M*Q*%xJV3}I8UYYd)>*nMvw zemWg`K6Rgy+m|y!8&*}=+`STm(dK-#b%)8nLsL&0<8Zd^|# z;I2gR&e1WUS#v!jX`+cuR;+yi(EiDcRCouW0AHNd?;5WVnC_Vg#4x56#0FOwTH6_p z#GILFF0>bb_tbmMM0|sd7r%l{U!fI0tGza&?65_D7+x9G zf3GA{c|mnO(|>}y(}%>|2>p0X8wRS&Eb0g)rcICIctfD_I9Wd+hKuEqv?gzEZBxG-rG~e!-2hqaR$Y$I@k{rLyCccE}3d)7Fn3EvfsEhA|bnJ374&pZDq&i zr(9#eq(g8^tG??ZzVk(#jU+-ce`|yiQ1dgrJ)$|wk?XLEqv&M+)I*OZ*oBCizjHuT zjZ|mW=<1u$wPhyo#&rIO;qH~pu4e3X;!%BRgmX%?&KZ6tNl386-l#a>ug5nHU2M~{fM2jvY*Py< zbR&^o&!T19G6V-pV@CB)YnEOfmrdPG%QByD?=if99ihLxP6iA8$??wUPWzptC{u5H z38Q|!=IW`)5Gef4+pz|9fIRXt>nlW)XQvUXBO8>)Q=$@gtwb1iEkU4EOWI4`I4DN5 zTC-Pk6N>2%7Hikg?`Poj5lkM0T_i zoCXfXB&}{TG%IB)ENSfI_Xg3=lxYc6-P059>oK;L+vGMy_h{y9soj#&^q5E!pl(Oq zl)oCBi56u;YHkD)d`!iOAhEJ0A^~T;uE9~Yp0{E%G~0q|9f34F!`P56-ZF{2hSaWj zio%9RR%oe~he22r@&j_d(y&nAUL*ayBY4#CWG&gZ8ybs#UcF?8K#HzziqOYM-<`C& z1gD?j)M0bp1w*U>X_b1@ag1Fx=d*wlr zEAcpmI#5LtqcX95LeS=LXlzh*l;^yPl_6MKk)zPuTz_p8ynQ5;oIOUAoPED=+M6Q( z8YR!DUm#$zTM9tbNhxZ4)J0L&Hpn%U>wj3z<=g;`&c_`fGufS!o|1%I_sA&;14bRC z3`BtzpAB-yl!%zM{Aiok8*X%lDNrPiAjBnzHbF0=Ua*3Lxl(zN3Thj2x6nWi^H7Jlwd2fxIvnI-SiC%*j z2~wIWWKT^5fYipo-#HSrr;(RkzzCSt?THVEH2EPvV-4c#Gu4&1X% z<1zTAM7ZM(LuD@ZPS?c30Ur`;2w;PXPVevxT)Ti25o}1JL>MN5i1^(aCF3 zbp>RI?X(CkR9*Hnv!({Ti@FBm;`Ip%e*D2tWEOc62@$n7+gWb;;j}@G()~V)>s}Bd zw+uTg^ibA(gsp*|&m7Vm=heuIF_pIukOedw2b_uO8hEbM4l=aq?E-7M_J`e(x9?{5 zpbgu7h}#>kDQAZL;Q2t?^pv}Y9Zlu=lO5e18twH&G&byq9XszEeXt$V93dQ@Fz2DV zs~zm*L0uB`+o&#{`uVYGXd?)Fv^*9mwLW4)IKoOJ&(8uljK?3J`mdlhJF1aK;#vlc zJdTJc2Q>N*@GfafVw45B03)Ty8qe>Ou*=f#C-!5uiyQ^|6@Dzp9^n-zidp*O`YuZ|GO28 zO0bqi;)fspT0dS2;PLm(&nLLV&&=Ingn(0~SB6Fr^AxPMO(r~y-q2>gRWv7{zYW6c zfiuqR)Xc41A7Eu{V7$-yxYT-opPtqQIJzMVkxU)cV~N0ygub%l9iHT3eQtB>nH0c` zFy}Iwd9vocxlm!P)eh0GwKMZ(fEk92teSi*fezYw3qRF_E-EcCh-&1T)?beW?9Q_+pde8&UW*(avPF4P}M#z*t~KlF~#5TT!&nu z>FAKF8vQl>Zm(G9UKi4kTqHj`Pf@Z@Q(bmZkseb1^;9k*`a9lKXceKX#dMd@ds`t| z2~UPsbn2R0D9Nm~G*oc@(%oYTD&yK)scA?36B7mndR9l*hNg!3?6>CR+tF1;6sr?V zzz8FBrZ@g4F_!O2igIGZcWd zRe_0*{d6cyy9QQ(|Ct~WTM1pC3({5qHahk*M*O}IPE6icikx48VZ?!0Oc^FVoq`}eu~ zpRq0MYHaBA-`b_BVID}|oo-bem76;B2zo7j7yz(9JiSY6JTjKz#+w{9mc{&#x}>E? zSS3mY$_|scfP3Mo_F5x;r>y&Mquy*Q1b3eF^*hg3tap~%?@ASeyodYa=dF&k=ZyWy z3C+&C95h|9TAVM~-8y(&xcy0nvl}6B*)j0FOlSz%+bK-}S4;F?P`j55*+ZO0Ogk7D z5q30zE@Nup4lqQoG`L%n{T?qn9&WC94%>J`KU{gHIq?n_L;75kkKyib;^?yXUx6BO zju%DyU(l!Vj(3stJ>!pMZ*NZFd60%oSAD1JUXG0~2GCXpB0Am(YPyhzQda-e)b^+f zzFaEZdVTJRJXPJo%w z$?T;xq^&(XjmO>0bNGsT|1{1UqGHHhasPC;H!oX52(AQ7h9*^npOIRdQbNrS0X5#5G?L4V}WsAYcpq-+JNXhSl)XbxZ)L@5Q+?wm{GAU z9a7X8hAjAo;4r_eOdZfXGL@YpmT|#qECEcPTQ;nsjIkQ;!0}g?T>Zr*Fg}%BZVA)4 zCAzvWr?M&)KEk`t9eyFi_GlPV9a2kj9G(JgiZadd_&Eb~#DyZ%2Zcvrda_A47G&uW z^6TnBK|th;wHSo8ivpScU?AM5HDu2+ayzExMJc@?4{h-c`!b($ExB`ro#vkl<;=BA z961c*n(4OR!ebT*7UV7sqL;rZ3+Z)BYs<1I|9F|TOKebtLPxahl|ZXxj4j!gjj!3*+iSb5Zni&EKVt$S{0?2>A}d@3PSF3LUu)5 z*Y#a1uD6Y!$=_ghsPrOqX!OcIP`IW};tZzx1)h_~mgl;0=n zdP|Te_7)~R?c9s>W(-d!@nzQyxqakrME{Tn@>0G)kqV<4;{Q?Z-M)E-|IFLTc}WQr z1Qt;u@_dN2kru_9HMtz8MQx1aDYINH&3<+|HA$D#sl3HZ&YsjfQBv~S>4=u z7gA2*X6_cI$2}JYLIq`4NeXTz6Q3zyE717#>RD&M?0Eb|KIyF;xj;+3#DhC-xOj~! z$-Kx#pQ)_$eHE3Zg?V>1z^A%3jW0JBnd@z`kt$p@lch?A9{j6hXxt$(3|b>SZiBxOjA%LsIPii{=o(B`yRJ>OK;z_ELTi8xHX)il z--qJ~RWsZ%9KCNuRNUypn~<2+mQ=O)kd59$Lul?1ev3c&Lq5=M#I{ zJby%%+Top_ocqv!jG6O6;r0Xwb%vL6SP{O(hUf@8riADSI<|y#g`D)`x^vHR4!&HY`#TQMqM`Su}2(C|KOmG`wyK>uh@3;(prdL{2^7T3XFGznp{-sNLLJH@mh* z^vIyicj9yH9(>~I-Ev7p=yndfh}l!;3Q65}K}()(jp|tC;{|Ln1a+2kbctWEX&>Vr zXp5=#pw)@-O6~Q|><8rd0>H-}0Nsc|J6TgCum{XnH2@hFB09FsoZ_ow^Nv@uGgz3# z<6dRDt1>>-!kN58&K1HFrgjTZ^q<>hNI#n8=hP&pKAL4uDcw*J66((I?!pE0fvY6N zu^N=X8lS}(=w$O_jlE(;M9F={-;4R(K5qa=P#ZVW>}J&s$d0?JG8DZJwZcx3{CjLg zJA>q-&=Ekous)vT9J>fbnZYNUtvox|!Rl@e^a6ue_4-_v=(sNB^I1EPtHCFEs!>kK6B@-MS!(B zST${=v9q6q8YdSwk4}@c6cm$`qZ86ipntH8G~51qIlsYQ)+2_Fg1@Y-ztI#aa~tFD_QUxb zU-?g5B}wU@`tnc_l+B^mRogRghXs!7JZS=A;In1|f(1T(+xfIi zvjccLF$`Pkv2w|c5BkSj>>k%`4o6#?ygojkV78%zzz`QFE6nh{(SSJ9NzVdq>^N>X zpg6+8u7i(S>c*i*cO}poo7c9%i^1o&3HmjY!s8Y$5aO(!>u1>-eai0;rK8hVzIh8b zL53WCXO3;=F4_%CxMKRN^;ggC$;YGFTtHtLmX%@MuMxvgn>396~ zEp>V(dbfYjBX^!8CSg>P2c5I~HItbe(dl^Ax#_ldvCh;D+g6-%WD|$@S6}Fvv*eHc zaKxji+OG|_KyMe2D*fhP<3VP0J1gTgs6JZjE{gZ{SO-ryEhh;W237Q0 z{yrDobsM6S`bPMUzr|lT|99m6XDI$RzW4tQ$|@C2RjhBYPliEXFV#M*5G4;Kb|J8E z0IH}-d^S-53kFRZ)ZFrd2%~Sth-6BN?hnMa_PC4gdWyW3q-xFw&L^x>j<^^S$y_3_ zdZxouw%6;^mg#jG@7L!g9Kdw}{w^X9>TOtHgxLLIbfEG^Qf;tD=AXozE6I`XmOF=# zGt$Wl+7L<8^VI-eSK%F%dqXieK^b!Z3yEA$KL}X@>fD9)g@=DGt|=d(9W%8@Y@!{PI@`Nd zyF?Us(0z{*u6|X?D`kKSa}}Q*HP%9BtDEA^buTlI5ihwe)CR%OR46b+>NakH3SDbZmB2X>c8na&$lk zYg$SzY+EXtq2~$Ep_x<~+YVl<-F&_fbayzTnf<7?Y-un3#+T~ahT+eW!l83sofNt; zZY`eKrGqOux)+RMLgGgsJdcA3I$!#zy!f<$zL0udm*?M5w=h$Boj*RUk8mDPVUC1RC8A`@7PgoBIU+xjB7 z25vky+^7k_|1n1&jKNZkBWUu1VCmS}a|6_+*;fdUZAaIR4G!wv=bAZEXBhcjch6WH zdKUr&>z^P%_LIx*M&x{!w|gij?nigT8)Ol3VicXRL0tU}{vp2fi!;QkVc#I38op3O z=q#WtNdN{x)OzmH;)j{cor)DQ;2%m>xMu_KmTisaeCC@~rQwQTfMml7FZ_ zU2AR8yCY_CT$&IAn3n#Acf*VKzJD8-aphMg(12O9cv^AvLQ9>;f!4mjyxq_a%YH2+{~=3TMNE1 z#r3@ynnZ#p?RCkPK36?o{ILiHq^N5`si(T_cKvO9r3^4pKG0AgDEB@_72(2rvU^-; z%&@st2+HjP%H)u50t81p>(McL{`dTq6u-{JM|d=G1&h-mtjc2{W0%*xuZVlJpUSP-1=U6@5Q#g(|nTVN0icr-sdD~DWR=s}`$#=Wa zt5?|$`5`=TWZevaY9J9fV#Wh~Fw@G~0vP?V#Pd=|nMpSmA>bs`j2e{)(827mU7rxM zJ@ku%Xqhq!H)It~yXm=)6XaPk=$Rpk*4i4*aSBZe+h*M%w6?3&0>>|>GHL>^e4zR!o%aGzUn40SR+TdN%=Dbn zsRfXzGcH#vjc-}7v6yRhl{V5PhE-r~)dnmNz=sDt?*1knNZ>xI5&vBwrosF#qRL-Y z;{W)4W&cO0XMKy?{^d`Xh(2B?j0ioji~G~p5NQJyD6vouyoFE9w@_R#SGZ1DR4GnN z{b=sJ^8>2mq3W;*u2HeCaKiCzK+yD!^i6QhTU5npwO+C~A#5spF?;iuOE>o&p3m1C zmT$_fH8v+5u^~q^ic#pQN_VYvU>6iv$tqx#Sulc%|S7f zshYrWq7IXCiGd~J(^5B1nGMV$)lo6FCTm1LshfcOrGc?HW7g>pV%#4lFbnt#94&Rg{%Zbg;Rh?deMeOP(du*)HryI zCdhO$3|SeaWK<>(jSi%qst${Z(q@{cYz7NA^QO}eZ$K@%YQ^Dt4CXzmvx~lLG{ef8 zyckIVSufk>9^e_O7*w2z>Q$8me4T~NQDq=&F}Ogo#v1u$0xJV~>YS%mLVYqEf~g*j zGkY#anOI9{(f4^v21OvYG<(u}UM!-k;ziH%GOVU1`$0VuO@Uw2N{$7&5MYjTE?Er) zr?oZAc~Xc==KZx-pmoh9KiF_JKU7u0#b_}!dWgC>^fmbVOjuiP2FMq5OD9+4TKg^2 z>y6s|sQhI`=fC<>BnQYV433-b+jBi+N6unz%6EQR%{8L#=4sktI>*3KhX+qAS>+K#}y5KnJ8YuOuzG(Ea5;$*1P$-9Z+V4guyJ#s) zRPH(JPN;Es;H72%c8}(U)CEN}Xm>HMn{n!d(=r*YP0qo*^APwwU5YTTeHKy#85Xj< zEboiH=$~uIVMPg!qbx~0S=g&LZ*IyTJG$hTN zv%2>XF``@S9lnLPC?|myt#P)%7?%e_j*aU4TbTyxO|3!h%=Udp;THL+^oPp<6;TLlIOa$&xeTG_a*dbRDy+(&n1T=MU z+|G5{2UprrhN^AqODLo$9Z2h(3^wtdVIoSk@}wPajVgIoZipRft}^L)2Y@mu;X-F{LUw|s7AQD-0!otW#W9M@A~08`o%W;Bq-SOQavG*e-sy8) zwtaucR0+64B&Pm++-m56MQ$@+t{_)7l-|`1kT~1s!swfc4D9chbawUt`RUOdoxU|j z$NE$4{Ysr@2Qu|K8pD37Yv&}>{_I5N49a@0<@rGHEs}t zwh_+9T0oh@ptMbjy*kbz<&3>LGR-GNsT8{x1g{!S&V7{5tPYX(GF>6qZh>O&F)%_I zkPE-pYo3dayjNQAG+xrI&yMZy590FA1unQ*k*Zfm#f9Z5GljOHBj-B83KNIP1a?<^1vOhDJkma0o- zs(TP=@e&s6fRrU(R}{7eHL*(AElZ&80>9;wqj{|1YQG=o2Le-m!UzUd?Xrn&qd8SJ0mmEYtW;t(;ncW_j6 zGWh4y|KMK^s+=p#%fWxjXo434N`MY<8W`tNH-aM6x{@o?D3GZM&+6t4V3I*3fZd{a z0&D}DI?AQl{W*?|*%M^D5{E>V%;=-r&uQ>*e)cqVY52|F{ptA*`!iS=VKS6y4iRP6 zKUA!qpElT5vZvN}U5k-IpeNOr6KF`-)lN1r^c@HnT#RlZbi(;yuvm9t-Noh5AfRxL@j5dU-X37(?S)hZhRDbf5cbhDO5nSX@WtApyp` zT$5IZ*4*)h8wShkPI45stQH2Y7yD*CX^Dh@B%1MJSEn@++D$AV^ttKXZdQMU`rxiR z+M#45Z2+{N#uR-hhS&HAMFK@lYBWOzU^Xs-BlqQDyN4HwRtP2$kks@UhAr@wlJii%Rq?qy25?Egs z*a&iAr^rbJWlv+pYAVUq9lor}#Cm|D$_ev2d2Ko}`8kuP(ljz$nv3OCDc7zQp|j6W zbS6949zRvj`bhbO(LN3}Pq=$Ld3a_*9r_24u_n)1)}-gRq?I6pdHPYHgIsn$#XQi~ z%&m_&nnO9BKy;G%e~fa7i9WH#MEDNQ8WCXhqqI+oeE5R7hLZT_?7RWVzEGZNz4*Po ze&*a<^Q*ze72}UM&$c%FuuEIN?EQ@mnILwyt;%wV-MV+|d%>=;3f0(P46;Hwo|Wr0 z>&FS9CCb{?+lDpJMs`95)C$oOQ}BSQEv0Dor%-Qj0@kqlIAm1-qSY3FCO2j$br7_w zlpRfAWz3>Gh~5`Uh?ER?@?r0cXjD0WnTx6^AOFii;oqM?|M9QjHd*GK3WwA}``?dK15`ZvG>_nB2pSTGc{n2hYT6QF^+&;(0c`{)*u*X7L_ zaxqyvVm$^VX!0YdpSNS~reC+(uRqF2o>jqIJQkC&X>r8|mBHvLaduM^Mh|OI60<;G zDHx@&jUfV>cYj5+fAqvv(XSmc(nd@WhIDvpj~C#jhZ6@M3cWF2HywB1yJv2#=qoY| zIiaxLsSQa7w;4YE?7y&U&e6Yp+2m(sb5q4AZkKtey{904rT08pJpanm->Z75IdvW^ z!kVBy|CIUZn)G}92_MgoLgHa?LZJDp_JTbAEq8>6a2&uKPF&G!;?xQ*+{TmNB1H)_ z-~m@CTxDry_-rOM2xwJg{fcZ41YQDh{DeI$4!m8c;6XtFkFyf`fOsREJ`q+Bf4nS~ zKDYs4AE7Gugv?X)tu4<-M8ag{`4pfQ14z<(8MYQ4u*fl*DCpq66+Q1-gxNCQ!c$me zyTrmi7{W-MGP!&S-_qJ%9+e08_9`wWGG{i5yLJ;8qbt-n_0*Q371<^u@tdz|;>fPW zE=&q~;wVD_4IQ^^jyYX;2shIMiYdvIpIYRT>&I@^{kL9Ka2ECG>^l>Ae!GTn{r~o= z|I9=J#wNe)zYRqGZ7Q->L{dfewyC$ZYcLaoNormZ3*gfM=da*{heC)&46{yTS!t10 zn_o0qUbQOs$>YuY>YHi|NG^NQG<_@jD&WnZcW^NTC#mhVE7rXlZ=2>mZkx{bc=~+2 z{zVH=Xs0`*K9QAgq9cOtfQ^BHh-yr=qX8hmW*0~uCup89IJMvWy%#yt_nz@6dTS)L{O3vXye< zW4zUNb6d|Tx`XIVwMMgqnyk?c;Kv`#%F0m^<$9X!@}rI##T{iXFC?(ui{;>_9Din8 z7;(754q!Jx(~sb!6+6Lf*l{fqD7GW*v{>3wp+)@wq2abADBK!kI8To}7zooF%}g-z zJ1-1lp-lQI6w^bov9EfhpxRI}`$PTpJI3uo@ZAV729JJ2Hs68{r$C0U=!d$Bm+s(p z8Kgc(Ixf4KrN%_jjJjTx5`&`Ak*Il%!}D_V)GM1WF!k$rDJ-SudXd_Xhl#NWnET&e-P!rH~*nNZTzxj$?^oo3VWc-Ay^`Phze3(Ft!aNW-f_ zeMy&BfNCP^-FvFzR&rh!w(pP5;z1$MsY9Voozmpa&A}>|a{eu}>^2s)So>&kmi#7$ zJS_-DVT3Yi(z+ruKbffNu`c}s`Uo`ORtNpUHa6Q&@a%I%I;lm@ea+IbCLK)IQ~)JY zp`kdQ>R#J*i&Ljer3uz$m2&Un9?W=Ue|hHv?xlM`I&*-M;2{@so--0OAiraN1TLra z>EYQu#)Q@UszfJj&?kr%RraFyi*eG+HD_(!AWB;hPgB5Gd-#VDRxxv*VWMY0hI|t- zR=;TL%EKEg*oet7GtmkM zgH^y*1bfJ*af(_*S1^PWqBVVbejFU&#m`_69IwO!aRW>Rcp~+7w^ptyu>}WFYUf;) zZrgs;EIN9$Immu`$umY%$I)5INSb}aV-GDmPp!d_g_>Ar(^GcOY%2M)Vd7gY9llJR zLGm*MY+qLzQ+(Whs8-=ty2l)G9#82H*7!eo|B6B$q%ak6eCN%j?{SI9|K$u3)ORoz zw{bAGaWHrMb|X^!UL~_J{jO?l^}lI^|7jIn^p{n%JUq9{tC|{GM5Az3SrrPkuCt_W zq#u0JfDw{`wAq`tAJmq~sz`D_P-8qr>kmms>I|);7Tn zLl^n*Ga7l=U)bQmgnSo5r_&#Pc=eXm~W75X9Cyy0WDO|fbSn5 zLgpFAF4fa90T-KyR4%%iOq6$6BNs@3ZV<~B;7V=u zdlB8$lpe`w-LoS;0NXFFu@;^^bc?t@r3^XTe*+0;o2dt&>eMQeDit(SfDxYxuA$uS z**)HYK7j!vJVRNfrcokVc@&(ke5kJzvi};Lyl7@$!`~HM$T!`O`~MQ1k~ZH??fQr zNP)33uBWYnTntKRUT*5lu&8*{fv>syNgxVzEa=qcKQ86Vem%Lpae2LM=TvcJLs?`=o9%5Mh#k*_7zQD|U7;A%=xo^_4+nX{~b1NJ6@ z*=55;+!BIj1nI+)TA$fv-OvydVQB=KK zrGWLUS_Chm$&yoljugU=PLudtJ2+tM(xj|E>Nk?c{-RD$sGYNyE|i%yw>9gPItE{ zD|BS=M>V^#m8r?-3swQofD8j$h-xkg=F+KM%IvcnIvc)y zl?R%u48Jeq7E*26fqtLe_b=9NC_z|axW#$e0adI#r(Zsui)txQ&!}`;;Z%q?y2Kn! zXzFNe+g7+>>`9S0K1rmd)B_QVMD?syc3e0)X*y6(RYH#AEM9u?V^E0GHlAAR)E^4- zjKD+0K=JKtf5DxqXSQ!j?#2^ZcQoG5^^T+JaJa3GdFeqIkm&)dj76WaqGukR-*&`13ls8lU2ayVIR%;79HYAr5aEhtYa&0}l}eAw~qKjUyz4v*At z?})QplY`3cWB6rl7MI5mZx&#%I0^iJm3;+J9?RA(!JXjl?(XgmA-D#2cY-^?g1c*Q z3GVLh!8Jhe;QqecbMK#XIJxKMb=6dcs?1vbb?@ov-raj`hnYO92y8pv@>RVr=9Y-F zv`BK)9R6!m4Pfllu4uy0WBL+ZaUFFzbZZtI@J8{OoQ^wL-b$!FpGT)jYS-=vf~b-@ zIiWs7j~U2yI=G5;okQz%gh6}tckV5wN;QDbnu|5%%I(#)8Q#)wTq8YYt$#f9=id;D zJbC=CaLUyDIPNOiDcV9+=|$LE9v2;Qz;?L+lG{|g&iW9TI1k2_H;WmGH6L4tN1WL+ zYfSVWq(Z_~u~U=g!RkS|YYlWpKfZV!X%(^I3gpV%HZ_{QglPSy0q8V+WCC2opX&d@eG2BB#(5*H!JlUzl$DayI5_J-n zF@q*Fc-nlp%Yt;$A$i4CJ_N8vyM5fNN`N(CN53^f?rtya=p^MJem>JF2BEG|lW|E) zxf)|L|H3Oh7mo=9?P|Y~|6K`B3>T)Gw`0ESP9R`yKv}g|+qux(nPnU(kQ&&x_JcYg9+6`=; z-EI_wS~l{T3K~8}8K>%Ke`PY!kNt415_x?^3QOvX(QUpW&$LXKdeZM-pCI#%EZ@ta zv(q-(xXIwvV-6~(Jic?8<7ain4itN>7#AqKsR2y(MHMPeL)+f+v9o8Nu~p4ve*!d3 z{Lg*NRTZsi;!{QJknvtI&QtQM_9Cu%1QcD0f!Fz+UH4O#8=hvzS+^(e{iG|Kt7C#u zKYk7{LFc+9Il>d6)blAY-9nMd(Ff0;AKUo3B0_^J&ESV@4UP8PO0no7G6Gp_;Z;YnzW4T-mCE6ZfBy(Y zXOq^Of&?3#Ra?khzc7IJT3!%IKK8P(N$ST47Mr=Gv@4c!>?dQ-&uZihAL1R<_(#T8Y`Ih~soL6fi_hQmI%IJ5qN995<{<@_ z;^N8AGQE+?7#W~6X>p|t<4@aYC$-9R^}&&pLo+%Ykeo46-*Yc(%9>X>eZpb8(_p{6 zwZzYvbi%^F@)-}5%d_z^;sRDhjqIRVL3U3yK0{Q|6z!PxGp?|>!%i(!aQODnKUHsk^tpeB<0Qt7`ZBlzRIxZMWR+|+ z3A}zyRZ%0Ck~SNNov~mN{#niO**=qc(faGz`qM16H+s;Uf`OD1{?LlH!K!+&5xO%6 z5J80-41C{6)j8`nFvDaeSaCu_f`lB z_Y+|LdJX=YYhYP32M556^^Z9MU}ybL6NL15ZTV?kfCFfpt*Pw5FpHp#2|ccrz#zoO zhs=+jQI4fk*H0CpG?{fpaSCmXzU8bB`;kCLB8T{_3t>H&DWj0q0b9B+f$WG=e*89l zzUE)b9a#aWsEpgnJqjVQETpp~R7gn)CZd$1B8=F*tl+(iPH@s9jQtE33$dBDOOr=% ziOpR8R|1eLI?Rn*d+^;_U#d%bi$|#obe0(-HdB;K>=Y=mg{~jTA_WpChe8QquhF`N z>hJ}uV+pH`l_@d>%^KQNm*$QNJ(lufH>zv9M`f+C-y*;hAH(=h;kp@eL=qPBeXrAo zE7my75EYlFB30h9sdt*Poc9)2sNP9@K&4O7QVPQ^m$e>lqzz)IFJWpYrpJs)Fcq|P z5^(gnntu!+oujqGpqgY_o0V&HL72uOF#13i+ngg*YvPcqpk)Hoecl$dx>C4JE4DWp z-V%>N7P-}xWv%9Z73nn|6~^?w$5`V^xSQbZceV<_UMM&ijOoe{Y^<@3mLSq_alz8t zr>hXX;zTs&k*igKAen1t1{pj94zFB;AcqFwV)j#Q#Y8>hYF_&AZ?*ar1u%((E2EfZ zcRsy@s%C0({v=?8oP=DML`QsPgzw3|9|C22Y>;=|=LHSm7~+wQyI|;^WLG0_NSfrf zamq!5%EzdQ&6|aTP2>X=Z^Jl=w6VHEZ@=}n+@yeu^ke2Yurrkg9up3g$0SI8_O-WQu$bCsKc(juv|H;vz6}%7ONww zKF%!83W6zO%0X(1c#BM}2l^ddrAu^*`9g&1>P6m%x{gYRB)}U`40r>6YmWSH(|6Ic zH~QNgxlH*;4jHg;tJiKia;`$n_F9L~M{GiYW*sPmMq(s^OPOKm^sYbBK(BB9dOY`0 z{0!=03qe*Sf`rcp5Co=~pfQyqx|umPHj?a6;PUnO>EZGb!pE(YJgNr{j;s2+nNV(K zDi#@IJ|To~Zw)vqGnFwb2}7a2j%YNYxe2qxLk)VWJIux$BC^oII=xv-_}h@)Vkrg1kpKokCmX({u=lSR|u znu_fA0PhezjAW{#Gu0Mdhe8F4`!0K|lEy+<1v;$ijSP~A9w%q5-4Ft|(l7UqdtKao zs|6~~nmNYS>fc?Nc=yzcvWNp~B0sB5ForO5SsN(z=0uXxl&DQsg|Y?(zS)T|X``&8 z*|^p?~S!vk8 zg>$B{oW}%rYkgXepmz;iqCKY{R@%@1rcjuCt}%Mia@d8Vz5D@LOSCbM{%JU#cmIp! z^{4a<3m%-p@JZ~qg)Szb-S)k{jv92lqB(C&KL(jr?+#ES5=pUH$(;CO9#RvDdErmW z3(|f{_)dcmF-p*D%qUa^yYngNP&Dh2gq5hr4J!B5IrJ?ODsw@*!0p6Fm|(ebRT%l) z#)l22@;4b9RDHl1ys$M2qFc;4BCG-lp2CN?Ob~Be^2wQJ+#Yz}LP#8fmtR%o7DYzoo1%4g4D+=HonK7b!3nvL0f1=oQp93dPMTsrjZRI)HX-T}ApZ%B#B;`s? z9Kng{|G?yw7rxo(T<* z1+O`)GNRmXq3uc(4SLX?fPG{w*}xDCn=iYo2+;5~vhWUV#e5e=Yfn4BoS@3SrrvV9 zrM-dPU;%~+3&>(f3sr$Rcf4>@nUGG*vZ~qnxJznDz0irB(wcgtyATPd&gSuX^QK@+ z)7MGgxj!RZkRnMSS&ypR94FC$;_>?8*{Q110XDZ)L);&SA8n>72s1#?6gL>gydPs` zM4;ert4-PBGB@5E` zBaWT=CJUEYV^kV%@M#3(E8>g8Eg|PXg`D`;K8(u{?}W`23?JgtNcXkUxrH}@H_4qN zw_Pr@g%;CKkgP(`CG6VTIS4ZZ`C22{LO{tGi6+uPvvHkBFK|S6WO{zo1MeK$P zUBe}-)3d{55lM}mDVoU@oGtPQ+a<=wwDol}o=o1z*)-~N!6t09du$t~%MlhM9B5~r zy|zs^LmEF#yWpXZq!+Nt{M;bE%Q8z7L8QJDLie^5MKW|I1jo}p)YW(S#oLf(sWn~* zII>pocNM5#Z+-n2|495>?H?*oyr0!SJIl(}q-?r`Q;Jbqqr4*_G8I7agO298VUr9x z8ZcHdCMSK)ZO@Yr@c0P3{`#GVVdZ{zZ$WTO zuvO4ukug&& ze#AopTVY3$B>c3p8z^Yyo8eJ+(@FqyDWlR;uxy0JnSe`gevLF`+ZN6OltYr>oN(ZV z>76nIiVoll$rDNkck6_eh%po^u16tD)JXcii|#Nn(7=R9mA45jz>v}S%DeMc(%1h> zoT2BlF9OQ080gInWJ3)bO9j$ z`h6OqF0NL4D3Kz?PkE8nh;oxWqz?<3_!TlN_%qy*T7soZ>Pqik?hWWuya>T$55#G9 zxJv=G&=Tm4!|p1#!!hsf*uQe}zWTKJg`hkuj?ADST2MX6fl_HIDL7w`5Dw1Btays1 zz*aRwd&>4*H%Ji2bt-IQE$>sbCcI1Poble0wL`LAhedGRZp>%>X6J?>2F*j>`BX|P zMiO%!VFtr_OV!eodgp-WgcA-S=kMQ^zihVAZc!vdx*YikuDyZdHlpy@Y3i!r%JI85$-udM6|7*?VnJ!R)3Qfm4mMm~Z#cvNrGUy|i0u zb|(7WsYawjBK0u1>@lLhMn}@X>gyDlx|SMXQo|yzkg-!wIcqfGrA!|t<3NC2k` zq;po50dzvvHD>_mG~>W0iecTf@3-)<$PM5W@^yMcu@U;)(^eu@e4jAX7~6@XrSbIE zVG6v2miWY^g8bu5YH$c2QDdLkg2pU8xHnh`EUNT+g->Q8Tp4arax&1$?CH($1W&*} zW&)FQ>k5aCim$`Ph<9Zt?=%|pz&EX@_@$;3lQT~+;EoD(ho|^nSZDh*M0Z&&@9T+e zHYJ;xB*~UcF^*7a_T)9iV5}VTYKda8n*~PSy@>h7c(mH~2AH@qz{LMQCb+-enMhX} z2k0B1JQ+6`?Q3Lx&(*CBQOnLBcq;%&Nf<*$CX2<`8MS9c5zA!QEbUz1;|(Ua%CiuL zF2TZ>@t7NKQ->O#!;0s;`tf$veXYgq^SgG>2iU9tCm5&^&B_aXA{+fqKVQ*S9=58y zddWqy1lc$Y@VdB?E~_B5w#so`r552qhPR649;@bf63_V@wgb!>=ij=%ptnsq&zl8^ zQ|U^aWCRR3TnoKxj0m0QL2QHM%_LNJ(%x6aK?IGlO=TUoS%7YRcY{!j(oPcUq{HP=eR1>0o^(KFl-}WdxGRjsT);K8sGCkK0qVe{xI`# z@f+_kTYmLbOTxRv@wm2TNBKrl+&B>=VaZbc(H`WWLQhT=5rPtHf)#B$Q6m1f8We^)f6ylbO=t?6Y;{?&VL|j$VXyGV!v8eceRk zl>yOWPbk%^wv1t63Zd8X^Ck#12$*|yv`v{OA@2;-5Mj5sk#ptfzeX(PrCaFgn{3*hau`-a+nZhuJxO;Tis51VVeKAwFML#hF9g26NjfzLs8~RiM_MFl1mgDOU z=ywk!Qocatj1Q1yPNB|FW>!dwh=aJxgb~P%%7(Uydq&aSyi?&b@QCBiA8aP%!nY@c z&R|AF@8}p7o`&~>xq9C&X6%!FAsK8gGhnZ$TY06$7_s%r*o;3Y7?CenJUXo#V-Oag z)T$d-V-_O;H)VzTM&v8^Uk7hmR8v0)fMquWHs6?jXYl^pdM#dY?T5XpX z*J&pnyJ<^n-d<0@wm|)2SW9e73u8IvTbRx?Gqfy_$*LI_Ir9NZt#(2T+?^AorOv$j zcsk+t<#!Z!eC|>!x&#l%**sSAX~vFU0|S<;-ei}&j}BQ#ekRB-;c9~vPDIdL5r{~O zMiO3g0&m-O^gB}<$S#lCRxX@c3g}Yv*l)Hh+S^my28*fGImrl<-nbEpOw-BZ;WTHL zgHoq&ftG|~ouV<>grxRO6Z%{!O+j`Cw_4~BIzrjpkdA5jH40{1kDy|pEq#7`$^m*? zX@HxvW`e}$O$mJvm+65Oc4j7W@iVe)rF&-}R>KKz>rF&*Qi3%F0*tz!vNtl@m8L9= zyW3%|X}0KsW&!W<@tRNM-R>~~QHz?__kgnA(G`jWOMiEaFjLzCdRrqzKlP1vYLG`Y zh6_knD3=9$weMn4tBD|5=3a9{sOowXHu(z5y^RYrxJK z|L>TUvbDuO?3=YJ55N5}Kj0lC(PI*Te0>%eLNWLnawD54geX5>8AT(oT6dmAacj>o zC`Bgj-RV0m3Dl2N=w3e0>wWWG5!mcal`Xu<(1=2$b{k(;kC(2~+B}a(w;xaHPk^@V zGzDR|pt%?(1xwNxV!O6`JLCM!MnvpbLoHzKziegT_2LLWAi4}UHIo6uegj#WTQLet z9Dbjyr{8NAk+$(YCw~_@Az9N|iqsliRYtR7Q|#ONIV|BZ7VKcW$phH9`ZAlnMTW&9 zIBqXYuv*YY?g*cJRb(bXG}ts-t0*|HXId4fpnI>$9A?+BTy*FG8f8iRRKYRd*VF_$ zoo$qc+A(d#Lx0@`ck>tt5c$L1y7MWohMnZd$HX++I9sHoj5VXZRZkrq`v@t?dfvC} z>0h!c4HSb8%DyeF#zeU@rJL2uhZ^8dt(s+7FNHJeY!TZJtyViS>a$~XoPOhHsdRH* zwW+S*rIgW0qSPzE6w`P$Jv^5dsyT6zoby;@z=^yWLG^x;e557RnndY>ph!qCF;ov$ ztSW1h3@x{zm*IMRx|3lRWeI3znjpbS-0*IL4LwwkWyPF1CRpQK|s42dJ{ddA#BDDqio-Y+mF-XcP-z4bi zAhfXa2=>F0*b;F0ftEPm&O+exD~=W^qjtv&>|%(4q#H=wbA>7QorDK4X3~bqeeXv3 zV1Q<>_Fyo!$)fD`fd@(7(%6o-^x?&+s=)jjbQ2^XpgyYq6`}ISX#B?{I$a&cRcW?X zhx(i&HWq{=8pxlA2w~7521v-~lu1M>4wL~hDA-j(F2;9ICMg+6;Zx2G)ulp7j;^O_ zQJIRUWQam(*@?bYiRTKR<;l_Is^*frjr-Dj3(fuZtK{Sn8F;d*t*t{|_lnlJ#e=hx zT9?&_n?__2mN5CRQ}B1*w-2Ix_=CF@SdX-cPjdJN+u4d-N4ir*AJn&S(jCpTxiAms zzI5v(&#_#YrKR?B?d~ge1j*g<2yI1kp`Lx>8Qb;aq1$HOX4cpuN{2ti!2dXF#`AG{ zp<iD=Z#qN-yEwLwE7%8w8&LB<&6{WO$#MB-|?aEc@S1a zt%_p3OA|kE&Hs47Y8`bdbt_ua{-L??&}uW zmwE7X4Y%A2wp-WFYPP_F5uw^?&f zH%NCcbw_LKx!c!bMyOBrHDK1Wzzc5n7A7C)QrTj_Go#Kz7%+y^nONjnnM1o5Sw(0n zxU&@41(?-faq?qC^kO&H301%|F9U-Qm(EGd3}MYTFdO+SY8%fCMTPMU3}bY7ML1e8 zrdOF?E~1uT)v?UX(XUlEIUg3*UzuT^g@QAxEkMb#N#q0*;r zF6ACHP{ML*{Q{M;+^4I#5bh#c)xDGaIqWc#ka=0fh*_Hlu%wt1rBv$B z%80@8%MhIwa0Zw$1`D;Uj1Bq`lsdI^g_18yZ9XUz2-u6&{?Syd zHGEh-3~HH-vO<)_2^r|&$(q7wG{@Q~un=3)Nm``&2T99L(P+|aFtu1sTy+|gwL*{z z)WoC4rsxoWhz0H$rG|EwhDT z0zcOAod_k_Ql&Y`YV!#&Mjq{2ln|;LMuF$-G#jX_2~oNioTHb4GqFatn@?_KgsA7T z(ouy$cGKa!m}6$=C1Wmb;*O2p*@g?wi-}X`v|QA4bNDU*4(y8*jZy-Ku)S3iBN(0r ztfLyPLfEPqj6EV}xope=?b0Nyf*~vDz-H-Te@B`{ib?~F<*(MmG+8zoYS77$O*3vayg#1kkKN+Bu9J9;Soev<%2S&J zr8*_PKV4|?RVfb#SfNQ;TZC$8*9~@GR%xFl1 z3MD?%`1PxxupvVO>2w#8*zV<-!m&Lis&B>)pHahPQ@I_;rY~Z$1+!4V1jde&L8y0! zha7@F+rOENF{~0$+a~oId0R|_!PhO=8)$>LcO)ca6YeOQs?ZG;`4O`x=Pd??Bl?Qf zgkaNj7X5@3_==zlQ-u6?omteA!_e-6gfDtw6CBnP2o1wo-7U!Y@89rU1HFb|bIr!I z=qIz=AW(}L^m z=I9RiS{DRtTYS6jsnvt1zs)W;kSVFOK|WMyZ@dxs+8{*W9-aTmS79J4R{Cis>EIqS zw+~gJqwz)(!z>)KDyhS{lM*xQ-8mNvo$A=IwGu+iS564tgX`|MeEuis!aN-=7!L&e zhNs;g1MBqDyx{y@AI&{_)+-?EEg|5C*!=OgD#$>HklRVU+R``HYZZq5{F9C0KKo!d z$bE2XC(G=I^YUxYST+Hk>0T;JP_iAvCObcrPV1Eau865w6d^Wh&B?^#h2@J#!M2xp zLGAxB^i}4D2^?RayxFqBgnZ-t`j+~zVqr+9Cz9Rqe%1a)c*keP#r54AaR2*TH^}7j zmJ48DN);^{7+5|+GmbvY2v#qJy>?$B(lRlS#kyodlxA&Qj#9-y4s&|eq$5} zgI;4u$cZWKWj`VU%UY#SH2M$8?PjO-B-rNPMr=8d=-D(iLW#{RWJ}@5#Z#EK=2(&LvfW&{P4_jsDr^^rg9w#B7h`mBwdL9y)Ni;= zd$jFDxnW7n-&ptjnk#<0zmNNt{;_30vbQW!5CQ7SuEjR1be!vxvO53!30iOermrU1 zXhXaen8=4Q(574KO_h$e$^1khO&tQL59=)Dc^8iPxz8+tC3`G$w|yUzkGd%Wg4(3u zJ<&7r^HAaEfG?F8?2I64j4kPpsNQk7qBJa9_hFT;*j;A%H%;QI@QWqJaiOl=;u>G8 zG`5Ow4K5ifd=OS|7F;EFc1+GzLld0RCQxG>Fn?~5Wl5VHJ=$DeR-2zwBgzSrQsGG0 zBqrILuB+_SgLxh~S~^QNHWW(2P;Z?d!Rd1lnEM=z23xPzyrbO_L0k43zruDkrJO*D zlzN(peBMLji`xfgYUirul-7c#3t(*=x6A^KSU-L|$(0pp9A*43#=Q!cu%9ZHP!$J| zSk8k=Z8cl811Vvn(4p8xx+EdKQV(sjC4_mEvlWeuIfwEVcF2LiC{H!oW)LSW=0ul| zT?$5PCc(pf-zKzUH`p7I7coVvCK;Dv-3_c?%~bPz`#ehbfrSrFf{RAz0I5e*W1S)kTW{0gf5X2v2k=S=W{>pr44tQ?o` zih8gE29VGR_SL~YJtcA)lRLozPg!<3Mh(`Hp)5{bclb)reTScXzJ>7{?i^yR@{(^% z#=$BYXPIX%fhgsofP-T`3b<5#V(TTS)^$vlhV&Kn=(LXOTAADIR1v8UqmW5c`n`S% zC8SOW$e?>&0dwKD%Jt{+67PfCLnqX0{8K^(q_^^2#puPYPkJsyXWMa~?V?p5{flYi z-1!uqI2x%puPG)r7b8y+Pc0Z5C%aA6`Q1_?W9k!YbiVVJVJwGLL?)P0M&vo{^IgEE zrX3eTgrJl_AeXYmiciYX9OP?NPN%-7Ji%z3U`-iXX=T~OI0M=ek|5IvIsvXM$%S&v zKw{`Kj(JVc+Pp^?vLKEyoycfnk)Hd>et78P^Z*{#rBY~_>V7>{gtB$0G99nbNBt+r zyXvEg_2=#jjK+YX1A>cj5NsFz9rjB_LB%hhx4-2I73gr~CW_5pD=H|e`?#CQ2)p4& z^v?Dlxm-_j6bO5~eeYFZGjW3@AGkIxY=XB*{*ciH#mjQ`dgppNk4&AbaRYKKY-1CT z>)>?+ME)AcCM7RRZQsH5)db7y!&jY-qHp%Ex9N|wKbN$!86i>_LzaD=f4JFc6Dp(a z%z>%=q(sXlJ=w$y^|tcTy@j%AP`v1n0oAt&XC|1kA`|#jsW(gwI0vi3a_QtKcL+yh z1Y=`IRzhiUvKeZXH6>>TDej)?t_V8Z7;WrZ_7@?Z=HRhtXY+{hlY?x|;7=1L($?t3 z6R$8cmez~LXopZ^mH9=^tEeAhJV!rGGOK@sN_Zc-vmEr;=&?OBEN)8aI4G&g&gdOb zfRLZ~dVk3194pd;=W|Z*R|t{}Evk&jw?JzVERk%JNBXbMDX82q~|bv%!2%wFP9;~-H?={C1sZ( zuDvY5?M8gGX*DyN?nru)UvdL|Rr&mXzgZ;H<^KYvzIlet!aeFM@I?JduKj=!(+ zM7`37KYhd*^MrKID^Y1}*sZ#6akDBJyKna%xK%vLlBqzDxjQ3}jx8PBOmXkvf@B{@ zc#J;~wQ<6{B;``j+B!#7s$zONYdXunbuKvl@zvaWq;`v2&iCNF2=V9Kl|77-mpCp= z2$SxhcN=pZ?V{GW;t6s)?-cNPAyTi&8O0QMGo#DcdRl#+px!h3ayc*(VOGR95*Anj zL0YaiVN2mifzZ){X+fl`Z^P=_(W@=*cIe~BJd&n@HD@;lRmu8cx7K8}wPbIK)GjF> zQGQ2h#21o6b2FZI1sPl}9_(~R|2lE^h}UyM5A0bJQk2~Vj*O)l-4WC4$KZ>nVZS|d zZv?`~2{uPYkc?254B9**q6tS|>We?uJ&wK3KIww|zzSuj>ncI4D~K z1Y6irVFE{?D-|R{!rLhZxAhs+Ka9*-(ltIUgC;snNek4_5xhO}@+r9Sl*5=7ztnXO zAVZLm$Kdh&rqEtdxxrE9hw`aXW1&sTE%aJ%3VL3*<7oWyz|--A^qvV3!FHBu9B-Jj z4itF)3dufc&2%V_pZsjUnN=;s2B9<^Zc83>tzo)a_Q$!B9jTjS->%_h`ZtQPz@{@z z5xg~s*cz`Tj!ls3-hxgnX}LDGQp$t7#d3E}>HtLa12z&06$xEQfu#k=(4h{+p%aCg zzeudlLc$=MVT+|43#CXUtRR%h5nMchy}EJ;n7oHfTq6wN6PoalAy+S~2l}wK;qg9o zcf#dX>ke;z^13l%bwm4tZcU1RTXnDhf$K3q-cK576+TCwgHl&?9w>>_(1Gxt@jXln zt3-Qxo3ITr&sw1wP%}B>J$Jy>^-SpO#3e=7iZrXCa2!N69GDlD{97|S*og)3hG)Lk zuqxK|PkkhxV$FP45%z*1Z?(LVy+ruMkZx|(@1R(0CoS6`7FWfr4-diailmq&Q#ehn zc)b&*&Ub;7HRtFVjL%((d$)M=^6BV@Kiusmnr1_2&&aEGBpbK7OWs;+(`tRLF8x?n zfKJB3tB^F~N`_ak3^exe_3{=aP)3tuuK2a-IriHcWv&+u7p z_yXsd6kyLV@k=(QoSs=NRiKNYZ>%4wAF;2#iu1p^!6>MZUPd;=2LY~l2ydrx10b#OSAlltILY%OKTp{e{ zzNogSk~SJBqi<_wRa#JqBW8Ok=6vb%?#H(hG}Dv98{JST5^SSh>_GQ@UK-0J`6l#E za}X#ud0W?cp-NQE@jAx>NUv65U~%YYS%BC0Cr$5|2_A)0tW;(nqoGJUHG5R`!-{1M-4T{<^pOE!Dvyuu1x7?Wt#YIgq zA$Vwj`St+M#ZxJXXGkepIF6`xL&XPu^qiFlZcX+@fOAdQ9d(h{^xCiAWJ0Ixp~3&E z(WwdT$O$7ez?pw>Jf{`!T-205_zJv+y~$w@XmQ;CiL8d*-x_z~0@vo4|3xUermJ;Q z9KgxjkN8Vh)xZ2xhX0N@{~@^d@BLoYFW%Uys83=`15+YZ%KecmWXjVV2}YbjBonSh zVOwOfI7^gvlC~Pq$QDHMQ6_Pd10OV{q_Zai^Yg({5XysuT`3}~3K*8u>a2FLBQ%#_YT6$4&6(?ZGwDE*C-p8>bM?hj*XOIoj@C!L5) zH1y!~wZ^dX5N&xExrKV>rEJJjkJDq*$K>qMi`Lrq08l4bQW~!Fbxb>m4qMHu6weTiV6_9(a*mZ23kr9AM#gCGE zBXg8#m8{ad@214=#w0>ylE7qL$4`xm!**E@pw484-VddzN}DK2qg&W~?%hcv3lNHx zg(CE<2)N=p!7->aJ4=1*eB%fbAGJcY65f3=cKF4WOoCgVelH$qh0NpIka5J-6+sY* zBg<5!R=I*5hk*CR@$rY6a8M%yX%o@D%{q1Jn=8wAZ;;}ol>xFv5nXvjFggCQ_>N2} zXHiC~pCFG*oEy!h_sqF$^NJIpQzXhtRU`LR0yU;MqrYUG0#iFW4mbHe)zN&4*Wf)G zV6(WGOq~OpEoq##E{rC?!)8ygAaAaA0^`<8kXmf%uIFfNHAE|{AuZd!HW9C^4$xW; zmIcO#ti!~)YlIU4sH(h&s6}PH-wSGtDOZ+%H2gAO(%2Ppdec9IMViuwwWW)qnqblH9xe1cPQ@C zS4W|atjGDGKKQAQlPUVUi1OvGC*Gh2i&gkh0up%u-9ECa7(Iw}k~0>r*WciZyRC%l z7NX3)9WBXK{mS|=IK5mxc{M}IrjOxBMzFbK59VI9k8Yr$V4X_^wI#R^~RFcme2)l!%kvUa zJ{zpM;;=mz&>jLvON5j>*cOVt1$0LWiV>x)g)KKZnhn=%1|2E|TWNfRQ&n?vZxQh* zG+YEIf33h%!tyVBPj>|K!EB{JZU{+k`N9c@x_wxD7z~eFVw%AyU9htoH6hmo0`%kb z55c#c80D%0^*6y|9xdLG$n4Hn%62KIp`Md9Jhyp8)%wkB8<%RlPEwC&FL z;hrH(yRr(Ke$%TZ09J=gGMC3L?bR2F4ZU!}pu)*8@l(d9{v^^(j>y+GF*nGran5*M z{pl5ig0CVsG1etMB8qlF4MDFRkLAg4N=l{Sc*F>K_^AZQc{dSXkvonBI)qEN1*U&? zKqMr?Wu)q9c>U~CZUG+-ImNrU#c`bS?RpvVgWXqSsOJrCK#HNIJ+k_1Iq^QNr(j|~ z-rz67Lf?}jj^9Ik@VIMBU2tN{Ts>-O%5f?=T^LGl-?iC%vfx{}PaoP7#^EH{6HP!( zG%3S1oaiR;OmlKhLy@yLNns`9K?60Zg7~NyT0JF(!$jPrm^m_?rxt~|J2)*P6tdTU z25JT~k4RH9b_1H3-y?X4=;6mrBxu$6lsb@xddPGKA*6O`Cc^>Ul`f9c&$SHFhHN!* zjj=(Jb`P}R%5X@cC%+1ICCRh1^G&u548#+3NpYTVr54^SbFhjTuO-yf&s%r4VIU!lE!j(JzHSc9zRD_fw@CP0pkL(WX6 zn+}LarmQP9ZGF9So^+jr<(LGLlOxGiCsI^SnuC{xE$S;DA+|z+cUk=j^0ipB(WTZ} zR0osv{abBd)HOjc(SAV&pcP@37SLnsbtADj?bT#cPZq|?W1Ar;4Vg5m!l{@{TA~|g zXYOeU`#h-rT@(#msh%%kH>D=`aN}2Rysez?E@R6|@SB(_gS0}HC>83pE`obNA9vsH zSu^r>6W-FSxJA}?oTuH>-y9!pQg|*<7J$09tH=nq4GTx+5($$+IGlO^bptmxy#=)e zuz^beIPpUB_YK^?eb@gu(D%pJJwj3QUk6<3>S>RN^0iO|DbTZNheFX?-jskc5}Nho zf&1GCbE^maIL$?i=nXwi)^?NiK`Khb6A*kmen^*(BI%Kw&Uv4H;<3ib-2UwG{7M&* zn$qyi8wD9cKOuxWhRmFupwLuFn!G5Vj6PZ#GCNJLlTQuQ?bqAYd7Eva5YR~OBbIim zf(6yXS4pei1Bz4w4rrB6Ke~gKYErlC=l9sm*Zp_vwJe7<+N&PaZe|~kYVO%uChefr%G4-=0eSPS{HNf=vB;p~ z5b9O1R?WirAZqcdRn9wtct>$FU2T8p=fSp;E^P~zR!^C!)WHe=9N$5@DHk6(L|7s@ zcXQ6NM9Q~fan1q-u8{ez;RADoIqwkf4|6LfsMZK6h{ZUGYo>vD%JpY<@w;oIN-*sK zxp4@+d{zxe>Z-pH#_)%|d(AC`fa!@Jq)5K8hd71!;CEG|ZI{I2XI`X~n|ae;B!q{I zJDa#T+fRviR&wAN^Sl{z8Ar1LQOF&$rDs18h0{yMh^pZ#hG?c5OL8v07qRZ-Lj5(0 zjFY(S4La&`3IjOT%Jqx4z~08($iVS;M10d@q~*H=Py)xnKt(+G-*o33c7S3bJ8cmwgj45` zU|b7xCoozC!-7CPOR194J-m9N*g`30ToBo!Io?m>T)S{CusNZx0J^Hu6hOmvv;0~W zFHRYJgyRhP1sM_AQ%pkD!X-dPu_>)`8HunR4_v$4T78~R<})-@K2LBt03PBLnjHzuYY)AK?>0TJe9 zmmOjwSL%CTaLYvYlJ~|w?vc*R+$@vEAYghtgGhZ2LyF+UdOn+v^yvD9R%xbU$fUjK{{VQ4VL&&UqAFa>CZuX4kX zJ)njewLWfKXneB+r}Y$`ezzwDoRT3r{9(@=I3-z>8tT)n3whDyi(r*lAnxQJefj_x z-8lc=r!Vua{b}v;LT)oXW>~6Q03~RAp~R}TZq9sGbeUBMS)?ZrJqiu|E&ZE)uN1uL zXcAj3#aEz zzbcCF)+;Hia#OGBvOatkPQfE{*RtBlO1QFVhi+3q0HeuFa*p+Dj)#8Mq9yGtIx%0A znV5EmN(j!&b%kNz4`Vr-)mX_?$ng&M^a6loFO(G3SA!~eBUEY!{~>C|Ht1Q4cw)X5~dPiEYQJNg?B2&P>bU7N(#e5cr8qc7A{a7J9cdMcRx)N|?;$L~O|E)p~ zIC}oi3iLZKb>|@=ApsDAfa_<$0Nm<3nOPdr+8Y@dnb|u2S<7CUmTGKd{G57JR*JTo zb&?qrusnu}jb0oKHTzh42P00C{i^`v+g=n|Q6)iINjWk4mydBo zf0g=ikV*+~{rIUr%MXdz|9ebUP)<@zR8fgeR_rChk0<^^3^?rfr;-A=x3M?*8|RPz z@}DOF`aXXuZGih9PyAbp|DULSw8PJ`54io)ga6JG@Hgg@_Zo>OfJ)8+TIfgqu%877 z@aFykK*+|%@rSs-t*oAzH6Whyr=TpuQ}B0ptSsMg9p8@ZE5A6LfMk1qdsf8T^zkdC3rUhB$`s zBdanX%L3tF7*YZ4^A8MvOvhfr&B)QOWCLJ^02kw5;P%n~5e`sa6MG{E2N^*2ZX@ge zI2>ve##O?I}sWX)UqK^_bRz@;5HWp5{ziyg?QuEjXfMP!j zpr(McSAQz>ME?M-3NSoCn$91#_iNnULp6tD0NN7Z0s#G~-~xWZFWN-%KUVi^yz~-` zn;AeGvjLJ~{1p#^?$>zM4vu=3mjBI$(_tC~NC0o@6<{zS_*3nGfUsHr3Gdgn%XedF zQUP=j5Mb>9=#f7aPl;cm$=I0u*WP}aVE!lCYw2Ht{Z_j9mp1h>dHGKkEZP6f^6O@J zndJ2+rWjxp|3#<2oO=8v!oHMX{|Vb|^G~pU_A6=ckBQvt>o+dpgYy(D=VCj65GE&jJj{&-*iq?z)PHNee&-@Mie~#LD*={ex8h(-)<@|55 zUr(}L?mz#;d|mrD%zrh<-*=;5*7K$B`zPjJ%m2pwr*G6tf8tN%a

_x$+l{{cH8$W#CT literal 0 HcmV?d00001 diff --git a/device_calendar/android/gradle/wrapper/gradle-wrapper.properties b/device_calendar/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3c9d0852 --- /dev/null +++ b/device_calendar/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/device_calendar/android/gradlew b/device_calendar/android/gradlew new file mode 100644 index 00000000..cccdd3d5 --- /dev/null +++ b/device_calendar/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/device_calendar/android/gradlew.bat b/device_calendar/android/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/device_calendar/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/device_calendar/android/proguard-rules.pro b/device_calendar/android/proguard-rules.pro new file mode 100644 index 00000000..d7668e11 --- /dev/null +++ b/device_calendar/android/proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.builttoroam.devicecalendar.** { *; } diff --git a/device_calendar/android/settings.gradle b/device_calendar/android/settings.gradle new file mode 100644 index 00000000..ef870028 --- /dev/null +++ b/device_calendar/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'device_calendar' diff --git a/device_calendar/android/src/main/AndroidManifest.xml b/device_calendar/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1479c8d6 --- /dev/null +++ b/device_calendar/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt new file mode 100644 index 00000000..5a803a6b --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt @@ -0,0 +1,18 @@ +package com.builttoroam.devicecalendar + +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) { + return JsonPrimitive(src.name) + } + return JsonObject() + } +} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt new file mode 100644 index 00000000..491370e7 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -0,0 +1,1268 @@ +package com.builttoroam.devicecalendar + +import android.Manifest +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +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.ErrorMessages +import com.builttoroam.devicecalendar.models.* +import com.builttoroam.devicecalendar.models.Calendar +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +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.RecurrenceRule.WeekdayNum +import java.util.* +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()) + + init { + val gsonBuilder = GsonBuilder() + 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 + + if (!_cachedParametersMap.containsKey(requestCode)) { + // this plugin doesn't handle this request code + return false + } + + 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 + + try { + if (!permissionGranted) { + finishWithError( + EC.NOT_AUTHORIZED, + EM.NOT_AUTHORIZED_MESSAGE, + cachedValues.pendingChannelResult + ) + return false + } + + when (cachedValues.calendarDelegateMethodCode) { + RETRIEVE_CALENDARS_REQUEST_CODE -> { + retrieveCalendars(cachedValues.pendingChannelResult) + } + RETRIEVE_EVENTS_REQUEST_CODE -> { + 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 + ) + } + DELETE_EVENT_REQUEST_CODE -> { + deleteEvent( + cachedValues.calendarId, + cachedValues.eventId, + cachedValues.pendingChannelResult + ) + } + REQUEST_PERMISSIONS_REQUEST_CODE -> { + finishWithSuccess(permissionGranted, cachedValues.pendingChannelResult) + } + DELETE_CALENDAR_REQUEST_CODE -> { + deleteCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) + } + } + + return true + } finally { + _cachedParametersMap.remove(cachedValues.calendarDelegateMethodCode) + } + } + + fun requestPermissions(pendingChannelResult: MethodChannel.Result) { + if (arePermissionsGranted()) { + finishWithSuccess(true, pendingChannelResult) + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + REQUEST_PERMISSIONS_REQUEST_CODE + ) + requestPermissions(parameters) + } + } + + fun hasPermissions(pendingChannelResult: MethodChannel.Result) { + finishWithSuccess(arePermissionsGranted(), pendingChannelResult) + } + + @SuppressLint("MissingPermission") + fun retrieveCalendars(pendingChannelResult: MethodChannel.Result) { + if (arePermissionsGranted()) { + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = CalendarContract.Calendars.CONTENT_URI + val cursor: Cursor? = if (atLeastAPI(17)) { + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION, null, null, null) + } else { + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION_OLDER_API, null, null, null) + } + val calendars: MutableList = mutableListOf() + try { + while (cursor?.moveToNext() == true) { + val calendar = parseCalendarRow(cursor) ?: continue + calendars.add(calendar) + } + + finishWithSuccess(_gson?.toJson(calendars), pendingChannelResult) + } catch (e: Exception) { + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) + } finally { + cursor?.close() + } + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDARS_REQUEST_CODE + ) + requestPermissions(parameters) + } + } + + 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( + EC.INVALID_ARGUMENT, + EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, + pendingChannelResult + ) + } + return null + } + + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = CalendarContract.Calendars.CONTENT_URI + + val cursor: Cursor? = if (atLeastAPI(17)) { + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION, + null, + null, + null + ) + } else { + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION_OLDER_API, + null, + null, + null + ) + } + + try { + if (cursor?.moveToFirst() == true) { + val calendar = parseCalendarRow(cursor) + if (isInternalCall) { + return calendar + } else { + finishWithSuccess(_gson?.toJson(calendar), pendingChannelResult) + } + } else { + if (!isInternalCall) { + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) + } + } + } catch (e: Exception) { + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) + } finally { + cursor?.close() + } + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDAR_REQUEST_CODE, + calendarId + ) + requestPermissions(parameters) + } + + return null + } + + fun deleteCalendar( + calendarId: String, + pendingChannelResult: MethodChannel.Result, + isInternalCall: Boolean = false + ): Calendar? { + if (isInternalCall || arePermissionsGranted()) { + val calendarIdNumber = calendarId.toLongOrNull() + if (calendarIdNumber == null) { + if (!isInternalCall) { + 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 deleteSucceeded = contentResolver?.delete(calenderUriWithId, null, null) ?: 0 + finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) + } else { + if (!isInternalCall) { + 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 + ) + requestPermissions(parameters) + } + + return null + } + + 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() + 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.OWNER_ACCOUNT, localAccountName) + 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 + val calendarId = java.lang.Long.parseLong(result?.lastPathSegment!!) + + finishWithSuccess(calendarId.toString(), pendingChannelResult) + } + + fun retrieveEvents( + calendarId: String, + startDate: Long?, + endDate: Long?, + eventIds: List, + pendingChannelResult: MethodChannel.Result + ) { + if (startDate == null && endDate == null && eventIds.isEmpty()) { + 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( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) + return + } + + val contentResolver: ContentResolver? = _context?.contentResolver + val eventsUriBuilder = CalendarContract.Instances.CONTENT_URI.buildUpon() + ContentUris.appendId(eventsUriBuilder, startDate ?: Date(0).time) + ContentUris.appendId(eventsUriBuilder, endDate ?: Date(Long.MAX_VALUE).time) + + val eventsUri = eventsUriBuilder.build() + val eventsCalendarQuery = "(${Events.CALENDAR_ID} = $calendarId)" + val eventsNotDeletedQuery = "(${Events.DELETED} != 1)" + val eventsIdsQuery = + "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" + + var eventsSelectionQuery = "$eventsCalendarQuery AND $eventsNotDeletedQuery" + if (eventIds.isNotEmpty()) { + eventsSelectionQuery += " AND ($eventsIdsQuery)" + } + val eventsSortOrder = Events.DTSTART + " DESC" + + val eventsCursor = contentResolver?.query( + eventsUri, + Cst.EVENT_PROJECTION, + eventsSelectionQuery, + null, + eventsSortOrder + ) + + val events: MutableList = mutableListOf() + + val exceptionHandler = CoroutineExceptionHandler { _, exception -> + uiThreadHandler.post { + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) + } + } + + GlobalScope.launch(Dispatchers.IO + exceptionHandler) { + while (eventsCursor?.moveToNext() == true) { + val event = parseEvent(calendarId, eventsCursor) ?: continue + events.add(event) + } + for (event in events) { + val attendees = retrieveAttendees(calendar, event.eventId!!, contentResolver) + event.organizer = + attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } + event.attendees = attendees + event.reminders = retrieveReminders(event.eventId!!, contentResolver) + } + }.invokeOnCompletion { cause -> + eventsCursor?.close() + if (cause == null) { + uiThreadHandler.post { + finishWithSuccess(_gson?.toJson(events), pendingChannelResult) + } + } + } + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_EVENTS_REQUEST_CODE, + calendarId, + startDate, + endDate + ) + requestPermissions(parameters) + } + + return + } + + fun createOrUpdateEvent( + calendarId: String, + event: Event?, + pendingChannelResult: MethodChannel.Result + ) { + if (arePermissionsGranted()) { + if (event == null) { + finishWithError( + EC.GENERIC_ERROR, + EM.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, + pendingChannelResult + ) + return + } + + val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) + if (calendar == null) { + finishWithError( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) + return + } + + val contentResolver: ContentResolver? = _context?.contentResolver + val values = buildEventContentValues(event, calendarId) + + val exceptionHandler = CoroutineExceptionHandler { _, exception -> + uiThreadHandler.post { + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) + } + } + + val job: Job + var eventId: Long? = event.eventId?.toLongOrNull() + if (eventId == null) { + val uri = contentResolver?.insert(Events.CONTENT_URI, values) + // get the event ID that is the last element in the Uri + eventId = java.lang.Long.parseLong(uri?.lastPathSegment!!) + job = GlobalScope.launch(Dispatchers.IO + exceptionHandler) { + insertAttendees(event.attendees, eventId, contentResolver) + insertReminders(event.reminders, eventId, contentResolver) + } + } 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 + for (attendeeToDelete in attendeesToDelete) { + deleteAttendee(eventId, attendeeToDelete, contentResolver) + } + + val attendeesToInsert = + event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } + insertAttendees(attendeesToInsert, eventId, contentResolver) + deleteExistingReminders(contentResolver, eventId) + insertReminders(event.reminders, eventId, contentResolver!!) + + val existingSelfAttendee = existingAttendees.firstOrNull { + it.emailAddress == calendar.ownerAccount + } + val newSelfAttendee = event.attendees.firstOrNull { + it.emailAddress == calendar.ownerAccount + } + if (existingSelfAttendee != null && newSelfAttendee != null && + newSelfAttendee.attendanceStatus != null && + existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus + ) { + updateAttendeeStatus(eventId, newSelfAttendee, contentResolver) + } + } + } + job.invokeOnCompletion { cause -> + if (cause == null) { + uiThreadHandler.post { + finishWithSuccess(eventId.toString(), pendingChannelResult) + } + } + } + } else { + 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 + ) + ) + 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) + } + if (reminderUri != null) { + contentResolver?.delete(reminderUri, null, null) + } + } + cursor?.close() + } + + @SuppressLint("MissingPermission") + private fun insertReminders( + reminders: List, + eventId: Long?, + contentResolver: ContentResolver + ) { + if (reminders.isEmpty()) { + return + } + val remindersContentValues = reminders.map { + ContentValues().apply { + put(CalendarContract.Reminders.EVENT_ID, eventId) + put(CalendarContract.Reminders.MINUTES, it.minutes) + put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) + } + }.toTypedArray() + contentResolver.bulkInsert(CalendarContract.Reminders.CONTENT_URI, remindersContentValues) + } + + private fun buildEventContentValues(event: Event, calendarId: String): ContentValues { + val values = ContentValues() + + 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.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.AVAILABILITY, getAvailability(event.availability)) + 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 + } + + private fun getTimeZone(timeZoneString: String?): TimeZone { + val deviceTimeZone: TimeZone = java.util.Calendar.getInstance().timeZone + var timeZone = TimeZone.getTimeZone(timeZoneString ?: deviceTimeZone.id) + + // Invalid time zone names defaults to GMT so update that to be device's time zone + if (timeZone.id == "GMT" && timeZoneString != "GMT") { + timeZone = TimeZone.getTimeZone(deviceTimeZone.id) + } + + return timeZone + } + + private fun getAvailability(availability: Availability?): Int? = when (availability) { + Availability.BUSY -> Events.AVAILABILITY_BUSY + Availability.FREE -> Events.AVAILABILITY_FREE + Availability.TENTATIVE -> Events.AVAILABILITY_TENTATIVE + else -> null + } + + private fun getEventStatus(eventStatus: EventStatus?): Int? = when (eventStatus) { + EventStatus.CONFIRMED -> Events.STATUS_CONFIRMED + EventStatus.TENTATIVE -> Events.STATUS_TENTATIVE + EventStatus.CANCELED -> Events.STATUS_CANCELED + else -> null + } + + @SuppressLint("MissingPermission") + private fun insertAttendees( + attendees: List, + eventId: Long?, + contentResolver: ContentResolver? + ) { + if (attendees.isEmpty()) { + return + } + + val attendeesValues = attendees.map { + ContentValues().apply { + put(CalendarContract.Attendees.ATTENDEE_NAME, it.name) + put(CalendarContract.Attendees.ATTENDEE_EMAIL, it.emailAddress) + put( + CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.RELATIONSHIP_ATTENDEE + ) + put(CalendarContract.Attendees.ATTENDEE_TYPE, it.role) + put( + CalendarContract.Attendees.ATTENDEE_STATUS, + it.attendanceStatus + ) + put(CalendarContract.Attendees.EVENT_ID, eventId) + } + }.toTypedArray() + + contentResolver?.bulkInsert(CalendarContract.Attendees.CONTENT_URI, attendeesValues) + } + + @SuppressLint("MissingPermission") + 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 + " = ?)" + 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 + ) + } + + 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( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) + return + } + + if (existingCal.isReadOnly) { + finishWithError( + EC.NOT_ALLOWED, + "Calendar with ID $calendarId is read-only", + pendingChannelResult + ) + return + } + + val eventIdNumber = eventId.toLongOrNull() + if (eventIdNumber == null) { + finishWithError( + EC.INVALID_ARGUMENT, + EM.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, + pendingChannelResult + ) + return + } + + val contentResolver: ContentResolver? = _context?.contentResolver + if (startDate == null && endDate == null && followingInstances == null) { // Delete all instances + val eventsUriWithId = ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) + val deleteSucceeded = contentResolver?.delete(eventsUriWithId, null, null) ?: 0 + finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) + } else { + if (!followingInstances!!) { // Only this instance + val exceptionUriWithId = + ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) + val values = ContentValues() + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) + + while (instanceCursor.moveToNext()) { + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) + + if (eventIdNumber == foundEventID) { + values.put( + Events.ORIGINAL_INSTANCE_TIME, + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_BEGIN_INDEX) + ) + values.put(Events.STATUS, Events.STATUS_CANCELED) + } + } + + val deleteSucceeded = contentResolver?.insert(exceptionUriWithId, values) + instanceCursor.close() + finishWithSuccess(deleteSucceeded != null, pendingChannelResult) + } else { // This and following instances + val eventsUriWithId = + ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) + val values = ContentValues() + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) + + while (instanceCursor.moveToNext()) { + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) + + if (eventIdNumber == foundEventID) { + 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, + Cst.EVENT_INSTANCE_DELETION, + startDate, + lastDate + ) + while (cursor.moveToNext()) { + 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, + Cst.EVENT_INSTANCE_DELETION, + startDate - DateUtils.YEAR_IN_MILLIS, + startDate - 1 + ) + var lastRecurrenceDate: Long? = null + + while (cursor.moveToNext()) { + if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { + lastRecurrenceDate = + cursor.getLong(Cst.EVENT_INSTANCE_DELETION_END_INDEX) + } + } + + if (lastRecurrenceDate != null) { + newRule.until = DateTime(lastRecurrenceDate) + } else { + newRule.until = DateTime(startDate - 1) + } + cursor.close() + } + + values.put(Events.RRULE, newRule.toString()) + contentResolver?.update(eventsUriWithId, values, null, null) + finishWithSuccess(true, pendingChannelResult) + } + } + instanceCursor.close() + } + } + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + DELETE_EVENT_REQUEST_CODE, + calendarId + ) + parameters.eventId = eventId + requestPermissions(parameters) + } + } + + private fun arePermissionsGranted(): Boolean { + if (atLeastAPI(23) && _binding != null) { + val writeCalendarPermissionGranted = _binding!!.activity.checkSelfPermission(Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED + val readCalendarPermissionGranted = _binding!!.activity.checkSelfPermission(Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED + return writeCalendarPermissionGranted && readCalendarPermissionGranted + } + + return true + } + + private fun requestPermissions(parameters: CalendarMethodsParametersCacheModel) { + val requestCode: Int = generateUniqueRequestCodeAndCacheParameters(parameters) + requestPermissions(requestCode) + } + + private fun requestPermissions(requestCode: Int) { + if (atLeastAPI(23)) { + _binding!!.activity.requestPermissions( + arrayOf( + Manifest.permission.WRITE_CALENDAR, + Manifest.permission.READ_CALENDAR + ), requestCode + ) + } + } + + private fun parseCalendarRow(cursor: Cursor?): Calendar? { + if (cursor == null) { + return null + } + + 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(), + displayName, + calendarColor, + accountName, + accountType, + ownerAccount + ) + + calendar.isReadOnly = isCalendarReadOnly(accessLevel) + if (atLeastAPI(17)) { + val isPrimary = cursor.getString(Cst.CALENDAR_PROJECTION_IS_PRIMARY_INDEX) + calendar.isDefault = isPrimary == "1" + } else { + calendar.isDefault = false + } + return calendar + } + + private fun parseEvent(calendarId: String, cursor: Cursor?): Event? { + if (cursor == null) { + return null + } + 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 eventColor = cursor.getLong(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) + + val event = Event() + event.eventTitle = title ?: "New Event" + event.eventId = eventId.toString() + event.calendarId = calendarId + event.eventDescription = description + event.eventStartDate = begin + event.eventEndDate = end + event.eventAllDay = allDay + event.eventLocation = location + event.eventURL = url + event.recurrenceRule = parseRecurrenceRuleString(recurringRule) + event.eventStartTimeZone = startTimeZone + event.eventEndTimeZone = endTimeZone + event.availability = availability + event.eventStatus = eventStatus + event.eventColor = eventColor + + return event + } + + private fun parseRecurrenceRuleString(recurrenceRuleString: String?): RecurrenceRule? { + if (recurrenceRuleString == null) { + return null + } + val rfcRecurrenceRule = Rrule(recurrenceRuleString) + val frequency = when (rfcRecurrenceRule.freq) { + 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) + + recurrenceRule.count = rfcRecurrenceRule.count + recurrenceRule.interval = rfcRecurrenceRule.interval + + val until = rfcRecurrenceRule.until + if (until != null) { + recurrenceRule.until = formatDateTime(dateTime = until) + } + + 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) + } + + recurrenceRule.bysetpos = rfcRecurrenceRule.getByPart(Rrule.Part.BYSETPOS) + + return recurrenceRule + } + + private fun formatDateTime(dateTime: DateTime): String { + assert(dateTime.year in 0..9999) + + fun twoDigits(n: Int): String { + return if (n < 10) "0$n" else "$n" + } + + 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" + } + + 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? { + if (cursor == null) { + return null + } + + val emailAddress = cursor.getString(Cst.ATTENDEE_EMAIL_INDEX) + + return Attendee( + emailAddress, + 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 + ) + } + + private fun parseReminderRow(cursor: Cursor?): Reminder? { + if (cursor == null) { + return null + } + + return Reminder(cursor.getInt(Cst.REMINDER_MINUTES_INDEX)) + } + + private fun isCalendarReadOnly(accessLevel: Int): Boolean { + return when (accessLevel) { + Events.CAL_ACCESS_CONTRIBUTOR, + Events.CAL_ACCESS_ROOT, + Events.CAL_ACCESS_OWNER, + Events.CAL_ACCESS_EDITOR + -> false + else -> true + } + } + + @SuppressLint("MissingPermission") + 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, + Cst.ATTENDEE_PROJECTION, + attendeesQuery, + null, + null + ) + attendeesCursor.use { cursor -> + if (cursor?.moveToFirst() == true) { + do { + val attendee = parseAttendeeRow(calendar, attendeesCursor) ?: continue + attendees.add(attendee) + } while (cursor.moveToNext()) + } + } + + return attendees + } + + @SuppressLint("MissingPermission") + 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, + Cst.REMINDER_PROJECTION, + remindersQuery, + null, + null + ) + remindersCursor.use { cursor -> + if (cursor?.moveToFirst() == true) { + do { + val reminder = parseReminderRow(remindersCursor) ?: continue + reminders.add(reminder) + } while (cursor.moveToNext()) + } + } + + return reminders + } + + @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 + val uniqueRequestCode: Int = (_cachedParametersMap.keys.maxOrNull() ?: 0) + 1 + parameters.ownCacheKey = uniqueRequestCode + _cachedParametersMap[uniqueRequestCode] = parameters + + return uniqueRequestCode + } + + private fun finishWithSuccess(result: T, pendingChannelResult: MethodChannel.Result) { + pendingChannelResult.success(result) + clearCachedParameters(pendingChannelResult) + } + + 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() + for (cachedParameter in cachedParameters) { + if (_cachedParametersMap.containsKey(cachedParameter.ownCacheKey)) { + _cachedParametersMap.remove(cachedParameter.ownCacheKey) + } + } + } + + private fun atLeastAPI(api: Int): Boolean { + return api <= Build.VERSION.SDK_INT + } + + 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.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.wkst != null) { + rr.weekStart = Weekday.valueOf(recurrenceRule.wkst!!) + } + + if (recurrenceRule.byday != null) { + rr.byDayPart = recurrenceRule.byday?.mapNotNull { + WeekdayNum.valueOf(it) + }?.toMutableList() + } + + if (recurrenceRule.bymonthday != null) { + rr.setByPart(Rrule.Part.BYMONTHDAY, recurrenceRule.bymonthday!!) + } + + if (recurrenceRule.byyearday != null) { + rr.setByPart(Rrule.Part.BYYEARDAY, recurrenceRule.byyearday!!) + } + + 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) + } + rr.setByPart(Rrule.Part.BYMONTH, newMonth) + } + + if (recurrenceRule.bysetpos != null) { + rr.setByPart(Rrule.Part.BYSETPOS, recurrenceRule.bysetpos!!) + } + return rr.toString() + } + + 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) { + Events.AVAILABILITY_BUSY -> Availability.BUSY + Events.AVAILABILITY_FREE -> Availability.FREE + Events.AVAILABILITY_TENTATIVE -> Availability.TENTATIVE + else -> null + } + + private fun parseEventStatus(status: Int): EventStatus? = when(status) { + Events.STATUS_CONFIRMED -> EventStatus.CONFIRMED + Events.STATUS_CANCELED -> EventStatus.CANCELED + Events.STATUS_TENTATIVE -> EventStatus.TENTATIVE + else -> null + } +} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt new file mode 100644 index 00000000..c1f14533 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -0,0 +1,301 @@ +package com.builttoroam.devicecalendar + +import android.app.Activity +import android.content.Context +import androidx.annotation.NonNull +import com.builttoroam.devicecalendar.common.Constants +import com.builttoroam.devicecalendar.models.* +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +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 org.dmfs.rfc5545.recur.Freq + +const val CHANNEL_NAME = "plugins.builttoroam.com/device_calendar" + +// 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 + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + private var context: Context? = null + private var activity: Activity? = null + + private lateinit var _calendarDelegate: CalendarDelegate + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel.setMethodCallHandler(this) + _calendarDelegate = CalendarDelegate(null, context!!) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + _calendarDelegate = CalendarDelegate(binding, context!!) + binding.addRequestPermissionsResultListener(_calendarDelegate) + } + + override fun onDetachedFromActivityForConfigChanges() { + activity = null + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity + _calendarDelegate = CalendarDelegate(binding, context!!) + binding.addRequestPermissionsResultListener(_calendarDelegate) + } + + override fun onDetachedFromActivity() { + activity = null + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + REQUEST_PERMISSIONS_METHOD -> { + _calendarDelegate.requestPermissions(result) + } + HAS_PERMISSIONS_METHOD -> { + _calendarDelegate.hasPermissions(result) + } + RETRIEVE_CALENDARS_METHOD -> { + _calendarDelegate.retrieveCalendars(result) + } + RETRIEVE_EVENTS_METHOD -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT) + 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 -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT) + val eventId = call.argument(EVENT_ID_ARGUMENT) + + _calendarDelegate.deleteEvent(calendarId!!, eventId!!, result) + } + DELETE_EVENT_INSTANCE_METHOD -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT) + val eventId = call.argument(EVENT_ID_ARGUMENT) + val startDate = call.argument(EVENT_START_DATE_ARGUMENT) + val endDate = call.argument(EVENT_END_DATE_ARGUMENT) + val followingInstances = call.argument(FOLLOWING_INSTANCES) + + _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 + ) + } + DELETE_CALENDAR_METHOD -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT) + _calendarDelegate.deleteCalendar(calendarId!!, result) + } + else -> { + result.notImplemented() + } + } + } + + private fun parseEventArgs(call: MethodCall, calendarId: String?): Event { + val event = Event() + event.eventTitle = call.argument(EVENT_TITLE_ARGUMENT) + event.calendarId = calendarId + event.eventId = call.argument(EVENT_ID_ARGUMENT) + event.eventDescription = call.argument(EVENT_DESCRIPTION_ARGUMENT) + event.eventAllDay = call.argument(EVENT_ALL_DAY_ARGUMENT) ?: false + event.eventStartDate = call.argument(EVENT_START_DATE_ARGUMENT)!! + event.eventEndDate = call.argument(EVENT_END_DATE_ARGUMENT)!! + event.eventStartTimeZone = call.argument(EVENT_START_TIMEZONE_ARGUMENT) + event.eventEndTimeZone = call.argument(EVENT_END_TIMEZONE_ARGUMENT) + event.eventLocation = call.argument(EVENT_LOCATION_ARGUMENT) + event.eventURL = call.argument(EVENT_URL_ARGUMENT) + 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 + ) { + val recurrenceRule = parseRecurrenceRuleArgs(call) + event.recurrenceRule = recurrenceRule + } + + 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( + attendeeArgs[EMAIL_ADDRESS_ARGUMENT] as String, + attendeeArgs[NAME_ARGUMENT] as String?, + attendeeArgs[ROLE_ARGUMENT] as Int, + attendeeArgs[ATTENDANCE_STATUS_ARGUMENT] as Int?, + null, 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 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(UNTIL_ARGUMENT)) { + recurrenceRule.until = recurrenceRuleArgs[UNTIL_ARGUMENT] as String? + } + + if (recurrenceRuleArgs.containsKey(BY_WEEK_DAYS_ARGUMENT)) { + recurrenceRule.byday = + recurrenceRuleArgs[BY_WEEK_DAYS_ARGUMENT].toListOf()?.toMutableList() + } + + if (recurrenceRuleArgs.containsKey(BY_MONTH_DAYS_ARGUMENT)) { + recurrenceRule.bymonthday = + recurrenceRuleArgs[BY_MONTH_DAYS_ARGUMENT] as MutableList? + } + + if (recurrenceRuleArgs.containsKey(BY_YEAR_DAYS_ARGUMENT)) { + recurrenceRule.byyearday = + recurrenceRuleArgs[BY_YEAR_DAYS_ARGUMENT] as MutableList? + } + + 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 + } + + private inline fun Any?.toListOf(): List? { + return (this as List<*>?)?.filterIsInstance()?.toList() + } + + private fun parseAvailability(value: String?): Availability? = + if (value == null || value == Constants.AVAILABILITY_UNAVAILABLE) { + null + } else { + Availability.valueOf(value) + } + + private fun parseEventStatus(value: String?): EventStatus? = + if (value == null || value == Constants.EVENT_STATUS_NONE) { + null + } else { + EventStatus.valueOf(value) + } +} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt new file mode 100644 index 00000000..4883b7a7 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt @@ -0,0 +1,15 @@ +package com.builttoroam.devicecalendar + +import com.builttoroam.devicecalendar.models.EventStatus +import com.google.gson.* +import java.lang.reflect.Type + +class EventStatusSerializer: JsonSerializer { + override fun serialize(src: EventStatus?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { + if(src != null) { + return JsonPrimitive(src.name) + } + return JsonObject() + } + +} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt new file mode 100644 index 00000000..052c7278 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -0,0 +1,117 @@ +package com.builttoroam.devicecalendar.common + +import android.provider.CalendarContract + +class Constants { + companion object { + const val CALENDAR_PROJECTION_ID_INDEX: Int = 0 + const val CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX: Int = 1 + const val CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX: Int = 2 + const val CALENDAR_PROJECTION_DISPLAY_NAME_INDEX: Int = 3 + const val CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX: Int = 4 + const val CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX: Int = 5 + const val CALENDAR_PROJECTION_COLOR_INDEX: Int = 6 + const val CALENDAR_PROJECTION_IS_PRIMARY_INDEX: Int = 7 + + // 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 + + ) + + // 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 + ) + + const val EVENT_PROJECTION_ID_INDEX: Int = 0 + const val EVENT_PROJECTION_TITLE_INDEX: Int = 1 + const val EVENT_PROJECTION_DESCRIPTION_INDEX: Int = 2 + const val EVENT_PROJECTION_BEGIN_INDEX: Int = 3 + const val EVENT_PROJECTION_END_INDEX: Int = 4 + const val EVENT_PROJECTION_RECURRING_RULE_INDEX: Int = 7 + const val EVENT_PROJECTION_ALL_DAY_INDEX: Int = 8 + const val EVENT_PROJECTION_EVENT_LOCATION_INDEX: Int = 9 + const val EVENT_PROJECTION_CUSTOM_APP_URI_INDEX: Int = 10 + const val EVENT_PROJECTION_START_TIMEZONE_INDEX: Int = 11 + const val EVENT_PROJECTION_END_TIMEZONE_INDEX: Int = 12 + 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, + CalendarContract.Events.TITLE, + CalendarContract.Events.DESCRIPTION, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END, + CalendarContract.Instances.DURATION, + CalendarContract.Events.RDATE, + CalendarContract.Events.RRULE, + CalendarContract.Events.ALL_DAY, + CalendarContract.Events.EVENT_LOCATION, + CalendarContract.Events.CUSTOM_APP_URI, + CalendarContract.Events.EVENT_TIMEZONE, + CalendarContract.Events.EVENT_END_TIMEZONE, + CalendarContract.Events.AVAILABILITY, + CalendarContract.Events.STATUS, + CalendarContract.Events.EVENT_COLOR, + ) + + const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 + const val EVENT_INSTANCE_DELETION_RRULE_INDEX: Int = 1 + const val EVENT_INSTANCE_DELETION_LAST_DATE_INDEX: Int = 2 + const val EVENT_INSTANCE_DELETION_BEGIN_INDEX: Int = 3 + 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 + ) + + const val ATTENDEE_ID_INDEX: Int = 0 + const val ATTENDEE_EVENT_ID_INDEX: Int = 1 + const val ATTENDEE_NAME_INDEX: Int = 2 + const val ATTENDEE_EMAIL_INDEX: Int = 3 + const val ATTENDEE_TYPE_INDEX: Int = 4 + const val ATTENDEE_RELATIONSHIP_INDEX: Int = 5 + 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 + ) + + const val REMINDER_MINUTES_INDEX = 1 + val REMINDER_PROJECTION: Array = arrayOf( + CalendarContract.Reminders.EVENT_ID, + CalendarContract.Reminders.MINUTES + ) + + const val AVAILABILITY_UNAVAILABLE = "UNAVAILABLE" + + const val EVENT_STATUS_NONE = "NONE" + } +} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt new file mode 100644 index 00000000..3509ad11 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt @@ -0,0 +1,11 @@ +package com.builttoroam.devicecalendar.common + +class ErrorCodes { + companion object { + const val INVALID_ARGUMENT: String = "400" + const val NOT_FOUND: String = "404" + const val NOT_ALLOWED: String = "405" + const val NOT_AUTHORIZED: String = "401" + const val GENERIC_ERROR: String = "500" + } +} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt new file mode 100644 index 00000000..e8486baa --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt @@ -0,0 +1,16 @@ +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)" + } +} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt new file mode 100644 index 00000000..825ca964 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt @@ -0,0 +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 diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt new file mode 100644 index 00000000..0ac7faa1 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt @@ -0,0 +1,7 @@ +package com.builttoroam.devicecalendar.models + +enum class Availability { + BUSY, + FREE, + TENTATIVE +} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt new file mode 100644 index 00000000..6e10b7fe --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -0,0 +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? +) { + var isReadOnly: Boolean = false + var isDefault: Boolean = false +} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt new file mode 100644 index 00000000..22bb4c4b --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt @@ -0,0 +1,16 @@ +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 +) { + var ownCacheKey: Int? = null +} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt new file mode 100644 index 00000000..a3918d75 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt @@ -0,0 +1,23 @@ +package com.builttoroam.devicecalendar.models + +class Event { + var eventTitle: String? = null + var eventId: String? = null + var calendarId: String? = null + var eventDescription: String? = null + var eventStartDate: Long? = null + var eventEndDate: Long? = null + var eventStartTimeZone: String? = null + var eventEndTimeZone: String? = null + var eventAllDay: Boolean = false + var eventLocation: String? = null + var eventURL: String? = null + var attendees: MutableList = mutableListOf() + var recurrenceRule: RecurrenceRule? = null + var organizer: Attendee? = null + var reminders: MutableList = mutableListOf() + var availability: Availability? = null + var eventStatus: EventStatus? = null + var eventColor: Long? = null + +} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt new file mode 100644 index 00000000..c8422795 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt @@ -0,0 +1,7 @@ +package com.builttoroam.devicecalendar.models + +enum class EventStatus { + CONFIRMED, + CANCELED, + TENTATIVE +} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt new file mode 100644 index 00000000..1da83111 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt @@ -0,0 +1,17 @@ +package com.builttoroam.devicecalendar.models + +import org.dmfs.rfc5545.recur.Freq + +class RecurrenceRule(val freq: Freq) { + var count: Int? = null + var interval: 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/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt new file mode 100644 index 00000000..c9695796 --- /dev/null +++ b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt @@ -0,0 +1,3 @@ +package com.builttoroam.devicecalendar.models + +class Reminder(val minutes: Int) \ No newline at end of file diff --git a/device_calendar/device_calendar.iml b/device_calendar/device_calendar.iml new file mode 100644 index 00000000..73e7ebd0 --- /dev/null +++ b/device_calendar/device_calendar.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/device_calendar/device_calendar_android.iml b/device_calendar/device_calendar_android.iml new file mode 100644 index 00000000..ac5d744d --- /dev/null +++ b/device_calendar/device_calendar_android.iml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/device_calendar/example/.flutter-plugins-dependencies b/device_calendar/example/.flutter-plugins-dependencies new file mode 100644 index 00000000..4bf359d7 --- /dev/null +++ b/device_calendar/example/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"device_calendar","path":"/Users/naokreuzeder/Workspace_SAVE/AndroidStudio/FLUTTER/github/device_calendar/","native_build":true,"dependencies":[]},{"name":"flutter_native_timezone","path":"/Users/naokreuzeder/.pub-cache/hosted/pub.dev/flutter_native_timezone-2.0.0/","native_build":true,"dependencies":[]},{"name":"integration_test","path":"/Users/naokreuzeder/Development/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"android":[{"name":"device_calendar","path":"/Users/naokreuzeder/Workspace_SAVE/AndroidStudio/FLUTTER/github/device_calendar/","native_build":true,"dependencies":[]},{"name":"flutter_native_timezone","path":"/Users/naokreuzeder/.pub-cache/hosted/pub.dev/flutter_native_timezone-2.0.0/","native_build":true,"dependencies":[]},{"name":"integration_test","path":"/Users/naokreuzeder/Development/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_native_timezone","path":"/Users/naokreuzeder/.pub-cache/hosted/pub.dev/flutter_native_timezone-2.0.0/","native_build":true,"dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"flutter_native_timezone","path":"/Users/naokreuzeder/.pub-cache/hosted/pub.dev/flutter_native_timezone-2.0.0/","dependencies":[]}]},"dependencyGraph":[{"name":"device_calendar","dependencies":[]},{"name":"flutter_native_timezone","dependencies":[]},{"name":"integration_test","dependencies":[]}],"date_created":"2023-11-28 11:15:47.783591","version":"3.13.5"} \ No newline at end of file diff --git a/device_calendar/example/.gitignore b/device_calendar/example/.gitignore new file mode 100644 index 00000000..dee655cc --- /dev/null +++ b/device_calendar/example/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ + +.flutter-plugins diff --git a/device_calendar/example/.metadata b/device_calendar/example/.metadata new file mode 100644 index 00000000..8cab361b --- /dev/null +++ b/device_calendar/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 44b7e7d3f42f050a79712daab253af06e9daf530 + channel: beta diff --git a/device_calendar/example/README.md b/device_calendar/example/README.md new file mode 100644 index 00000000..a24543bc --- /dev/null +++ b/device_calendar/example/README.md @@ -0,0 +1,195 @@ +# Examples + +Most of the APIs are covered in [calendar_event.dart](https://github.com/builttoroam/device_calendar/blob/master/example/lib/presentation/pages/calendar_event.dart) or [calendar_events.dart](https://github.com/builttoroam/device_calendar/blob/master/example/lib/presentation/pages/calendar_events.dart) files in the example app. +You'll be able to get a reference of how the APIs are used. + +For a full API reference, the documentation can be found at [pub.dev](https://pub.dev/documentation/device_calendar/latest/device_calendar/device_calendar-library.html). + +## DayOfWeekGroup Enum + +`DayOfWeekGroup` enum allows to explicitly choose and return a list of `DayOfWeek` enum values by using an extension `getDays`: + +* `DayOfWeekGroup.Weekday.getDays` will return: + + ```dart + [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday]; + ``` + +* `DayOfWeekGroup.Weekend.getDays` will return: + + ```dart + [DayOfWeek.Saturday, DayOfWeek.Sunday]; + ``` + +* `DayOfWeekGroup.Alldays.getDays` will return: + + ```dart + [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday]; + ``` + +## Attendee Examples + +Examples below present on how to initialise an `Attendee` model in Dart: + +* A required attendee: + + ```dart + Attendee( + name: 'Test User 1', + emailAddress: 'test1@example.com', + role: AttendeeRole.Required); + ``` + +* An optional attendee: + + ```dart + Attendee( + name: 'Test User 2', + emailAddress: 'test2@example.com', + role: AttendeeRole.Optional); + ``` + +## Reminder Examples + +Examples below present on how to initialise a `Reminder` model in Dart: + +* 30 minutes + + ```dart + Reminder(minutes: 30); + ``` + +* 1 day + + ```dart + Reminder(minutes: 1440); + ``` + +## Recurrence Rule Examples + +Examples below present sample parameters of recurrence rules received by each platform and required properties for the `RecurrenceRule` model in Dart.\ +**Please note**: Receiving monthly and yearly recurrence parameters are slightly different for the two platforms. + +You can find more standard examples at [iCalendar.org](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html). + +### **Daily Rule** + +Daily every 5 days and end after 3 occurrences + +* Recurrence parameter example (Android and iOS):\ +`FREQ=DAILY;INTERVAL=5;COUNT=3` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Daily, + interval: 5, + totalOccurrences: 3); + ``` + +### **Weekly Rule** + +Weekly on Monday, Tuesday and Saturday every 2 weeks and end on 31 Jan 2020 + +* Recurrence parameter example (Android and iOS):\ +`FREQ=WEEKLY;BYDAY=MO,TU,SA;INTERVAL=2;UNTIL=20200130T130000Z` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Weekly, + interval: 2, + endDate: DateTime(2020, 1, 31), + daysOfWeek: [ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Saturday ]); + ``` + +### **Monthly/Yearly SetPosition (Week Number) Rule** + +Monthly on third Thursday + +* Recurrence parameter example (Android):\ +`FREQ=MONTHLY;INTERVAL=1;BYDAY=3TH` +* Recurrence parameter example (iOS):\ +`FREQ=MONTHLY;INTERVAL=1;BYDAY=TH;BYSETPOS=3` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Monthly, + interval: 1, + daysOfWeek: [ DayOfWeek.Thursday ], + weekOfMonth: WeekNumber.Third); + ``` + +Monthly on last Thursday + +* Recurrence parameter example (Android and iOS):\ +`FREQ=MONTHLY;INTERVAL=1;BYDAY=-1TH` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Monthly, + interval: 1, + daysOfWeek: [ DayOfWeek.Thursday ], + weekOfMonth: WeekNumber.Last); + ``` + +Yearly on third Thursday of January + +* Recurrence parameter example (Android and iOS):\ +`FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYDAY=3TH` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Yearly, + interval: 1, + monthOfYear: MonthOfYear.January, + weekOfMonth: WeekNumber.Third); + ``` + +Yearly on last Thursday of January + +* Recurrence parameter example (Android and iOS):\ +`FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYDAY=-1TH` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Yearly, + interval: 1, + monthOfYear: MonthOfYear.January, + weekOfMonth: WeekNumber.Last); + ``` + +### **Monthly/Yearly By Day of a Month Rule** + +Monthly on 8th + +* Recurrence parameter example (Android and iOS):\ +`FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=8` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Monthly, + interval: 1, + dayOfMonth: 8); + ``` + +Yearly on 8th of February + +* Recurrence parameter example (Android):\ +`FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=8;BYMONTH=2` +* Recurrence parameter example (iOS):\ +`FREQ=YEARLY;INTERVAL=1` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Yearly, + interval: 1, + monthOfYear: MonthOfYear.February, + dayOfMonth: 8); + ``` diff --git a/device_calendar/example/analysis_options.yaml b/device_calendar/example/analysis_options.yaml new file mode 100644 index 00000000..3e1200f9 --- /dev/null +++ b/device_calendar/example/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 diff --git a/device_calendar/example/android/.gitignore b/device_calendar/example/android/.gitignore new file mode 100644 index 00000000..65b7315a --- /dev/null +++ b/device_calendar/example/android/.gitignore @@ -0,0 +1,10 @@ +*.iml +*.class +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +GeneratedPluginRegistrant.java diff --git a/device_calendar/example/android/app/build.gradle b/device_calendar/example/android/app/build.gradle new file mode 100644 index 00000000..776dc817 --- /dev/null +++ b/device_calendar/example/android/app/build.gradle @@ -0,0 +1,60 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 32 + ndkVersion '22.1.7171670' + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.builttoroam.devicecalendarexample" + minSdkVersion 19 + targetSdkVersion 31 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} diff --git a/device_calendar/example/android/app/proguard-rules.pro b/device_calendar/example/android/app/proguard-rules.pro new file mode 100644 index 00000000..77023c9c --- /dev/null +++ b/device_calendar/example/android/app/proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.builttoroam.devicecalendar.** { *; } \ No newline at end of file diff --git a/device_calendar/example/android/app/src/main/AndroidManifest.xml b/device_calendar/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..630265e9 --- /dev/null +++ b/device_calendar/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/device_calendar/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt b/device_calendar/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt new file mode 100644 index 00000000..2d71b1d4 --- /dev/null +++ b/device_calendar/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt @@ -0,0 +1,6 @@ +package com.builttoroam.devicecalendarexample + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() { +} diff --git a/device_calendar/example/android/app/src/main/res/drawable/launch_background.xml b/device_calendar/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/device_calendar/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/device_calendar/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/device_calendar/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/device_calendar/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/device_calendar/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/device_calendar/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/device_calendar/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/device_calendar/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/device_calendar/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/device_calendar/example/android/app/src/main/res/values/styles.xml b/device_calendar/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..00fa4417 --- /dev/null +++ b/device_calendar/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/device_calendar/example/android/build.gradle b/device_calendar/example/android/build.gradle new file mode 100644 index 00000000..d3f65307 --- /dev/null +++ b/device_calendar/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + ext.kotlin_version = '1.6.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/device_calendar/example/android/gradle.properties b/device_calendar/example/android/gradle.properties new file mode 100644 index 00000000..946d709d --- /dev/null +++ b/device_calendar/example/android/gradle.properties @@ -0,0 +1,3 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1536M \ No newline at end of file diff --git a/device_calendar/example/android/gradle/wrapper/gradle-wrapper.jar b/device_calendar/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/device_calendar/example/android/gradle/wrapper/gradle-wrapper.properties b/device_calendar/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..6f5b3ec5 --- /dev/null +++ b/device_calendar/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jun 16 16:20:15 AEST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip diff --git a/device_calendar/example/android/gradlew b/device_calendar/example/android/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/device_calendar/example/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/device_calendar/example/android/gradlew.bat b/device_calendar/example/android/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/device_calendar/example/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/device_calendar/example/android/settings.gradle b/device_calendar/example/android/settings.gradle new file mode 100644 index 00000000..5a2f14fb --- /dev/null +++ b/device_calendar/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/device_calendar/example/device_calendar_example.iml b/device_calendar/example/device_calendar_example.iml new file mode 100644 index 00000000..c92516a5 --- /dev/null +++ b/device_calendar/example/device_calendar_example.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/device_calendar/example/device_calendar_example_android.iml b/device_calendar/example/device_calendar_example_android.iml new file mode 100644 index 00000000..b050030a --- /dev/null +++ b/device_calendar/example/device_calendar_example_android.iml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/device_calendar/example/integration_test/app_test.dart b/device_calendar/example/integration_test/app_test.dart new file mode 100644 index 00000000..6e4a1908 --- /dev/null +++ b/device_calendar/example/integration_test/app_test.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:uuid/uuid.dart'; + +import 'package:device_calendar_example/main.dart' as app; + +/// NOTE: These integration tests are currently made to be run on a physical device where there is at least a calendar that can be written to. +/// Calendar permissions are needed. See example/test_driver/integration_test.dart for how to run this on Android +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('Calendar plugin example', () { + final eventTitle = const Uuid().v1(); + final saveEventButtonFinder = find.byKey(const Key('saveEventButton')); + final eventTitleFinder = find.text(eventTitle); + final firstWritableCalendarFinder = + find.byKey(const Key('writableCalendar0')); + final addEventButtonFinder = find.byKey(const Key('addEventButton')); + final titleFieldFinder = find.byKey(const Key('titleField')); + final deleteButtonFinder = find.byKey(const Key('deleteEventButton')); +//TODO: remove redundant restarts. Currently needed because the first screen is always "test starting..." + testWidgets('starts on calendars page', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('calendarsPage')), findsOneWidget); + }); + testWidgets('select first writable calendar', (WidgetTester tester) async { + app.main(); + + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + expect(firstWritableCalendarFinder, findsOneWidget); + }); + testWidgets('go to add event page', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(firstWritableCalendarFinder); + + await tester.pumpAndSettle(); + expect(addEventButtonFinder, findsOneWidget); + print('found add event button'); + await tester.tap(addEventButtonFinder); + await tester.pumpAndSettle(); + expect(saveEventButtonFinder, findsOneWidget); + }); + testWidgets('try to save event without entering mandatory fields', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(firstWritableCalendarFinder); + await tester.pumpAndSettle(); + await tester.tap(addEventButtonFinder); + + await tester.pumpAndSettle(); + await tester.tap(saveEventButtonFinder); + await tester.pumpAndSettle(); + expect(find.text('Please fix the errors in red before submitting.'), + findsOneWidget); + }); + testWidgets('save event with title $eventTitle', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(firstWritableCalendarFinder); + await tester.pumpAndSettle(); + await tester.tap(addEventButtonFinder); + + await tester.pumpAndSettle(); + await tester.tap(titleFieldFinder); + + await tester.enterText(titleFieldFinder, eventTitle); + await tester.tap(saveEventButtonFinder); + await tester.pumpAndSettle(); + expect(eventTitleFinder, findsOneWidget); + }); + testWidgets('delete event with title $eventTitle', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(firstWritableCalendarFinder); + await tester.pumpAndSettle(); + await tester.tap(eventTitleFinder); + + await tester.scrollUntilVisible(deleteButtonFinder, -5); + await tester.tap(deleteButtonFinder); + await tester.pumpAndSettle(); + expect(eventTitleFinder, findsNothing); + }); + }); +} diff --git a/device_calendar/example/integration_test/integration_test.dart b/device_calendar/example/integration_test/integration_test.dart new file mode 100644 index 00000000..ca6e9ef3 --- /dev/null +++ b/device_calendar/example/integration_test/integration_test.dart @@ -0,0 +1,8 @@ +import 'package:integration_test/integration_test_driver.dart'; + +/// Instruction for iOS: +/// See `ios.sh` +/// Instruction for android: +/// See `integration_test_android.dart` + +Future main() => integrationDriver(); diff --git a/device_calendar/example/integration_test/integration_test_android.dart b/device_calendar/example/integration_test/integration_test_android.dart new file mode 100644 index 00000000..2c840da5 --- /dev/null +++ b/device_calendar/example/integration_test/integration_test_android.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +// make sure 'adb devices' works on your local machine, then from the root of the plugin, run the following: +/* +1. +cd example +2. +flutter drive --driver=integration_test/integration_test.dart --target=integration_test/app_test.dart + */ + +Future main() async { + await Process.run('adb', [ + 'shell', + 'pm', + 'grant', + 'com.builttoroam.devicecalendarexample', + 'android.permission.READ_CALENDAR' + ]); + await Process.run('adb', [ + 'shell', + 'pm', + 'grant', + 'com.builttoroam.devicecalendarexample', + 'android.permission.WRITE_CALENDAR' + ]); + await integrationDriver(); +} diff --git a/device_calendar/example/integration_test/ios.sh b/device_calendar/example/integration_test/ios.sh new file mode 100755 index 00000000..8f8eede4 --- /dev/null +++ b/device_calendar/example/integration_test/ios.sh @@ -0,0 +1,24 @@ +# Use: integration_test/ios.sh +# +# Executes the device_calendar integration test for iOS +# This script creates and starts a new iOS simulator, grants calendar permission +# to the app then runs the integration tests and finally deletes the simulator. +# +# Prerequisites: Xcode, Xcode Command Line Tools, Xcode iOS Simulator +# +# To run an integration test, make sure the script has execute permission +# example: `chmod +x example/integration_test/ios.sh` then: +# 1. cd example +# 2. integration_test/ios.sh +# 3. You should see `All tests passed` +# +# Success - "All tests passed." is printed to the console +# +deviceId=$(xcrun simctl create builtToRoamCalendarTest "iPhone 13" 2> /dev/null | tail -1) +echo "Created device: $deviceId, booting..." +xcrun simctl boot $deviceId +xcrun simctl privacy $deviceId grant calendar com.builttoroam.deviceCalendarExample00 +echo "Running tests..." +flutter drive --driver=integration_test/integration_test.dart --target=integration_test/app_test.dart -d $deviceId +echo "Removing device: $deviceId" +xcrun simctl delete $deviceId diff --git a/device_calendar/example/ios/.gitignore b/device_calendar/example/ios/.gitignore new file mode 100755 index 00000000..1c202be0 --- /dev/null +++ b/device_calendar/example/ios/.gitignore @@ -0,0 +1,45 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/app.flx +/Flutter/app.zip +/Flutter/flutter_assets/ +/Flutter/App.framework +/Flutter/Flutter.framework +/Flutter/Generated.xcconfig +/ServiceDefinitions.json + +**/.symlinks/ +Pods/ diff --git a/device_calendar/example/ios/Flutter/AppFrameworkInfo.plist b/device_calendar/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100755 index 00000000..9b41e7d8 --- /dev/null +++ b/device_calendar/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 11.0 + + diff --git a/device_calendar/example/ios/Flutter/Debug.xcconfig b/device_calendar/example/ios/Flutter/Debug.xcconfig new file mode 100755 index 00000000..e8efba11 --- /dev/null +++ b/device_calendar/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/device_calendar/example/ios/Flutter/Release.xcconfig b/device_calendar/example/ios/Flutter/Release.xcconfig new file mode 100755 index 00000000..399e9340 --- /dev/null +++ b/device_calendar/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/device_calendar/example/ios/Flutter/flutter_export_environment.sh b/device_calendar/example/ios/Flutter/flutter_export_environment.sh new file mode 100755 index 00000000..bc51da7b --- /dev/null +++ b/device_calendar/example/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/naokreuzeder/Development/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/naokreuzeder/Workspace_SAVE/AndroidStudio/FLUTTER/github/example" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=3.2.0" +export "FLUTTER_BUILD_NUMBER=3.2.0" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/device_calendar/example/ios/Podfile b/device_calendar/example/ios/Podfile new file mode 100644 index 00000000..997d1cb3 --- /dev/null +++ b/device_calendar/example/ios/Podfile @@ -0,0 +1,45 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + flutter_additional_ios_build_settings(target) + config.build_settings['SWIFT_VERSION'] = '5.0' + # Or whatever Swift version your app is using that works with your plugins + end + end +end diff --git a/device_calendar/example/ios/Podfile.lock b/device_calendar/example/ios/Podfile.lock new file mode 100755 index 00000000..cb8f159b --- /dev/null +++ b/device_calendar/example/ios/Podfile.lock @@ -0,0 +1,34 @@ +PODS: + - device_calendar (0.0.1): + - Flutter + - Flutter (1.0.0) + - flutter_native_timezone (0.0.1): + - Flutter + - integration_test (0.0.1): + - Flutter + +DEPENDENCIES: + - device_calendar (from `.symlinks/plugins/device_calendar/ios`) + - Flutter (from `Flutter`) + - flutter_native_timezone (from `.symlinks/plugins/flutter_native_timezone/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + +EXTERNAL SOURCES: + device_calendar: + :path: ".symlinks/plugins/device_calendar/ios" + Flutter: + :path: Flutter + flutter_native_timezone: + :path: ".symlinks/plugins/flutter_native_timezone/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + +SPEC CHECKSUMS: + device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 + integration_test: 13825b8a9334a850581300559b8839134b124670 + +PODFILE CHECKSUM: 10625bdc9b9ef8574174815aabd5b048e6e29bff + +COCOAPODS: 1.11.3 diff --git a/device_calendar/example/ios/Runner.xcodeproj/project.pbxproj b/device_calendar/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..160e1d14 --- /dev/null +++ b/device_calendar/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,507 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 763C5C3662C48FEF7F2B0120 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E098C60D243A71853922C094 /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2AD784C0989B63BAA46EFDFD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E098C60D243A71853922C094 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F504DA6DD95FB326B0725EEE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 763C5C3662C48FEF7F2B0120 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0C6DE7144DB7716571BF5210 /* Pods */ = { + isa = PBXGroup; + children = ( + F504DA6DD95FB326B0725EEE /* Pods-Runner.debug.xcconfig */, + 2AD784C0989B63BAA46EFDFD /* Pods-Runner.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 6400E78A7B626A4B08303DA0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E098C60D243A71853922C094 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 0C6DE7144DB7716571BF5210 /* Pods */, + 6400E78A7B626A4B08303DA0 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + A6EB42BBC2C9005F5277AB4F /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 122A90E1AB6EBE6FCB241A06 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = PG8Q9ZR89L; + LastSwiftMigration = 1130; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 122A90E1AB6EBE6FCB241A06 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/device_calendar/device_calendar.framework", + "${BUILT_PRODUCTS_DIR}/flutter_native_timezone/flutter_native_timezone.framework", + "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_calendar.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_timezone.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + A6EB42BBC2C9005F5277AB4F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PG8Q9ZR89L; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PG8Q9ZR89L; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100755 index 00000000..919434a6 --- /dev/null +++ b/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/device_calendar/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/device_calendar/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100755 index 00000000..14d255fd --- /dev/null +++ b/device_calendar/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/device_calendar/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/device_calendar/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100755 index 00000000..21a3cc14 --- /dev/null +++ b/device_calendar/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/device_calendar/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/device_calendar/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 00000000..18d98100 --- /dev/null +++ b/device_calendar/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/device_calendar/example/ios/Runner/AppDelegate.swift b/device_calendar/example/ios/Runner/AppDelegate.swift new file mode 100755 index 00000000..70693e4a --- /dev/null +++ b/device_calendar/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 00000000..d36b1fab --- /dev/null +++ b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100755 index 0000000000000000000000000000000000000000..3d43d11e66f4de3da27ed045ca4fe38ad8b48094 GIT binary patch literal 11112 zcmeHN3sh5A)((b(k1DoWZSj%R+R=^`Y(b;ElB$1^R>iT7q6h&WAVr806i~>Gqn6rM z>3}bMG&oq%DIriqR35=rtEdos5L6z)YC*Xq0U-$_+Il@RaU zXYX%+``hR28`(B*uJ6G9&iz>|)PS%!)9N`7=LcmcxH}k69HPyT-%S zH7+jBCC<%76cg_H-n41cTqnKn`u_V9p~XaTLUe3s{KRPSTeK6apP4Jg%VQ$e#72ms zxyWzmGSRwN?=fRgpx!?W&ZsrLfuhAsRxm%;_|P@3@3~BJwY4ZVBJ3f&$5x>`^fD?d zI+z!v#$!gz%FtL*%mR^Uwa*8LJFZ_;X!y$cD??W#c)31l@ervOa_Qk86R{HJiZb$f z&&&0xYmB{@D@yl~^l5IXtB_ou{xFiYP(Jr<9Ce{jCN z<3Rf2TD%}_N?y>bgWq|{`RKd}n>P4e8Z-D+(fn^4)+|pv$DcR&i+RHNhv$71F*McT zl`phYBlb;wO`b7)*10XF6UXhY9`@UR*6-#(Zp`vyU(__*te6xYtV&N0(zjMtev{tZ zapmGin===teMXjsS0>CYxUy<2izOKOPai0}!B9+6q$s3CF8W{xUwz?A0ADO5&BsiB z{SFt|KehNd-S#eiDq!y&+mW9N_!wH-i~q|oNm=mEzkx}B?Ehe%q$tK8f=QY#*6rH9 zNHHaG(9WBqzP!!TMEktSVuh$i$4A^b25LK}&1*4W?ul*5pZYjL1OZ@X9?3W7Y|T6} z1SXx0Wn-|!A;fZGGlYn9a1Jz5^8)~v#mXhmm>um{QiGG459N}L<&qyD+sy_ixD@AP zW0XV6w#3(JW>TEV}MD=O0O>k5H>p#&|O zD2mGf0Cz7+>l7`NuzGobt;(o@vb9YiOpHN8QJ9Uva|i7R?7nnq;L_iq+ZqPv*oGu! zN@GuJ9fm;yrEFga63m?1qy|5&fd32<%$yP$llh}Udrp>~fb>M>R55I@BsGYhCj8m1 zC=ziFh4@hoytpfrJlr}FsV|C(aV4PZ^8^`G29(+!Bk8APa#PemJqkF zE{IzwPaE)I&r`OxGk*vPErm6sGKaQJ&6FODW$;gAl_4b_j!oH4yE@ zP~Cl4?kp>Ccc~Nm+0kjIb`U0N7}zrQEN5!Ju|}t}LeXi!baZOyhlWha5lq{Ld2rdo zGz7hAJQt<6^cxXTe0xZjmADL85cC&H+~Lt2siIIh{$~+U#&#^{Ub22IA|ea6 z5j12XLc`~dh$$1>3o0Cgvo*ybi$c*z>n=5L&X|>Wy1~eagk;lcEnf^2^2xB=e58Z` z@Rw{1ssK)NRV+2O6c<8qFl%efHE;uy!mq(Xi1P*H2}LMi z3EqWN2U?eW{J$lSFxDJg-=&RH!=6P9!y|S~gmjg)gPKGMxq6r9cNIhW` zS})-obO}Ao_`;=>@fAwU&=|5$J;?~!s4LN2&XiMXEl>zk9M}tVEg#kkIkbKp%Ig2QJ2aCILCM1E=aN*iuz>;q#T_I7aVM=E4$m_#OWLnXQnFUnu?~(X>$@NP zBJ@Zw>@bmErSuW7SR2=6535wh-R`WZ+5dLqwTvw}Ks8~4F#hh0$Qn^l-z=;>D~St( z-1yEjCCgd*z5qXa*bJ7H2Tk54KiX&=Vd}z?%dcc z`N8oeYUKe17&|B5A-++RHh8WQ%;gN{vf%05@jZF%wn1Z_yk#M~Cn(i@MB_mpcbLj5 zR#QAtC`k=tZ*h|){Mjz`7bNL zGWOW=bjQhX@`Vw^xn#cVwn28c2D9vOb0TLLy~-?-%gOyHSeJ9a>P}5OF5$n}k-pvUa*pvLw)KvG~>QjNWS3LY1f*OkFwPZ5qC@+3^Bt=HZbf`alKY#{pn zdY}NEIgo1sd)^TPxVzO{uvU$|Z-jkK0p1x##LexgQ$zx1^bNPOG*u2RmZkIM!zFVz zz|IsP3I?qrlmjGS2w_(azCvGTnf~flqogV@Q%mH{76uLU(>UB zQZ?*ys3BO&TV{Pj_qEa-hkH7mOMe_Bnu3%CXCgu90XNKf$N)PUc3Ei-&~@tT zI^49Lm^+=TrI=h4h=W@jW{GjWd{_kVuSzAL6Pi@HKYYnnNbtcYdIRww+jY$(30=#p8*if(mzbvau z00#}4Qf+gH&ce_&8y3Z@CZV>b%&Zr7xuPSSqOmoaP@arwPrMx^jQBQQi>YvBUdpBn zI``MZ3I3HLqp)@vk^E|~)zw$0$VI_RPsL9u(kqulmS`tnb%4U)hm{)h@bG*jw@Y*#MX;Th1wu3TrO}Srn_+YWYesEgkO1 zv?P8uWB)is;#&=xBBLf+y5e4?%y>_8$1KwkAJ8UcW|0CIz89{LydfJKr^RF=JFPi}MAv|ecbuZ!YcTSxsD$(Pr#W*oytl?@+2 zXBFb32Kf_G3~EgOS7C`8w!tx}DcCT%+#qa76VSbnHo;4(oJ7)}mm?b5V65ir`7Z}s zR2)m15b#E}z_2@rf34wo!M^CnVoi# ze+S(IK({C6u=Sm{1>F~?)8t&fZpOOPcby;I3jO;7^xmLKM(<%i-nyj9mgw9F1Lq4|DZUHZ4)V9&6fQM(ZxbG{h+}(koiTu`SQw6#6q2Yg z-d+1+MRp$zYT2neIR2cKij2!R;C~ooQ3<;^8)_Gch&ZyEtiQwmF0Mb_)6)4lVEBF< zklXS7hvtu30uJR`3OzcqUNOdYsfrKSGkIQAk|4=&#ggxdU4^Y(;)$8}fQ>lTgQdJ{ zzie8+1$3@E;|a`kzuFh9Se}%RHTmBg)h$eH;gttjL_)pO^10?!bNev6{mLMaQpY<< z7M^ZXrg>tw;vU@9H=khbff?@nu)Yw4G% zGxobPTUR2p_ed7Lvx?dkrN^>Cv$Axuwk;Wj{5Z@#$sK@f4{7SHg%2bpcS{(~s;L(mz@9r$cK@m~ef&vf%1@ z@8&@LLO2lQso|bJD6}+_L1*D^}>oqg~$NipL>QlP3 zM#ATSy@ycMkKs5-0X8nFAtMhO_=$DlWR+@EaZ}`YduRD4A2@!at3NYRHmlENea9IF zN*s>mi?zy*Vv+F+&4-o`Wj}P3mLGM*&M(z|;?d82>hQkkY?e-hJ47mWOLCPL*MO04 z3lE(n2RM=IIo;Z?I=sKJ_h=iJHbQ2<}WW0b@I6Qf-{T=Qn#@N0yG5xH&ofEy^mZMPzd22nR`t!Q)VkNgf*VOxE z$XhOunG3ZN#`Ks$Hp~}`OX5vmHP={GYUJ+-g0%PS$*Qi5+-40M47zJ24vK1#? zb$s^%r?+>#lw$mpZaMa1aO%wlPm3~cno_(S%U&-R;6eK(@`CjswAW2)HfZ>ptItaZ|XqQ z&sHVVL>WCe|E4iPb2~gS5ITs6xfg(kmt&3$YcI=zTuqj37t|+9ojCr(G^ul#p{>k) zM94pI>~5VZ$!*Qurq<@RIXgP3sx-2kL$1Q~da%rnNIh?)&+c~*&e~CYPDhPYjb+Xu zKg5w^XB3(_9{Waa4E(-J-Kq_u6t_k?a8kEHqai-N-4#`SRerO!h}!cS%SMC<)tGix zOzVP^_t!HN&HIPL-ZpcgWitHM&yFRC7!k4zSI+-<_uQ}|tX)n{Ib;X>Xx>i_d*KkH zCzogKQFpP1408_2!ofU|iBq2R8hW6G zuqJs9Tyw{u%-uWczPLkM!MfKfflt+NK9Vk8E!C>AsJwNDRoe2~cL+UvqNP|5J8t)( z0$iMa!jhudJ+fqFn+um&@Oj6qXJd_3-l`S^I1#0fnt!z3?D*hAHr*u(*wR@`4O z#avrtg%s`Fh{?$FtBFM^$@@hW!8ZfF4;=n0<8In&X}-Rp=cd0TqT_ne46$j^r}FzE z26vX^!PzScuQfFfl1HEZ{zL?G88mcc76zHGizWiykBf4m83Z${So-+dZ~YGhm*RO7 zB1gdIdqnFi?qw+lPRFW5?}CQ3Me3G^muvll&4iN+*5#_mmIu;loULMwb4lu9U*dFM z-Sr**(0Ei~u=$3<6>C-G6z4_LNCx||6YtjS)<;hf)YJTPKXW+w%hhCTUAInIse9>r zl2YU6nRb$u-FJlWN*{{%sm_gi_UP5{=?5}5^D2vPzM=oPfNw~azZQ#P zl5z8RtSSiTIpEohC15i-Q1Bk{3&ElsD0uGAOxvbk29VUDmmA0w;^v`W#0`};O3DVE z&+-ca*`YcN%z*#VXWK9Qa-OEME#fykF%|7o=1Y+eF;Rtv0W4~kKRDx9YBHOWhC%^I z$Jec0cC7o37}Xt}cu)NH5R}NT+=2Nap*`^%O)vz?+{PV<2~qX%TzdJOGeKj5_QjqR&a3*K@= P-1+_A+?hGkL;m(J7kc&K literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100755 index 0000000000000000000000000000000000000000..28c6bf03016f6c994b70f38d1b7346e5831b531f GIT binary patch literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 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 literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 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 literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100755 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100755 index 00000000..89c2725b --- /dev/null +++ b/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/device_calendar/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/device_calendar/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100755 index 00000000..f2e259c7 --- /dev/null +++ b/device_calendar/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/device_calendar/example/ios/Runner/Base.lproj/Main.storyboard b/device_calendar/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100755 index 00000000..f3c28516 --- /dev/null +++ b/device_calendar/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/device_calendar/example/ios/Runner/Info.plist b/device_calendar/example/ios/Runner/Info.plist new file mode 100755 index 00000000..ad25cf2b --- /dev/null +++ b/device_calendar/example/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + NSContactsUsageDescription + Contacts are used for event attendee editing. + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + device_calendar_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCalendarsUsageDescription + We need access to your calendar to help you track events + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/device_calendar/example/ios/Runner/Runner-Bridging-Header.h b/device_calendar/example/ios/Runner/Runner-Bridging-Header.h new file mode 100755 index 00000000..7335fdf9 --- /dev/null +++ b/device_calendar/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/device_calendar/example/lib/common/app_routes.dart b/device_calendar/example/lib/common/app_routes.dart new file mode 100644 index 00000000..991a9d70 --- /dev/null +++ b/device_calendar/example/lib/common/app_routes.dart @@ -0,0 +1,3 @@ +class AppRoutes { + static const calendars = '/'; +} diff --git a/device_calendar/example/lib/main.dart b/device_calendar/example/lib/main.dart new file mode 100644 index 00000000..3b5d61ee --- /dev/null +++ b/device_calendar/example/lib/main.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'common/app_routes.dart'; +import 'presentation/pages/calendars.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData(), + themeMode: ThemeMode.system, + darkTheme: ThemeData.dark(), + routes: { + AppRoutes.calendars: (context) { + return const CalendarsPage(key: Key('calendarsPage')); + } + }, + ); + } +} diff --git a/device_calendar/example/lib/presentation/date_time_picker.dart b/device_calendar/example/lib/presentation/date_time_picker.dart new file mode 100644 index 00000000..dc11e8d9 --- /dev/null +++ b/device_calendar/example/lib/presentation/date_time_picker.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import 'input_dropdown.dart'; + +class DateTimePicker extends StatelessWidget { + const DateTimePicker( + {Key? key, + this.labelText, + this.selectedDate, + this.selectedTime, + this.selectDate, + this.selectTime, + this.enableTime = true}) + : super(key: key); + + final String? labelText; + final DateTime? selectedDate; + final TimeOfDay? selectedTime; + final ValueChanged? selectDate; + final ValueChanged? selectTime; + final bool enableTime; + + Future _selectDate(BuildContext context) async { + final picked = await showDatePicker( + context: context, + initialDate: selectedDate != null + ? DateTime.parse(selectedDate.toString()) + : DateTime.now(), + firstDate: DateTime(2015, 8), + lastDate: DateTime(2101)); + if (picked != null && picked != selectedDate && selectDate != null) { + selectDate!(picked); + } + } + + Future _selectTime(BuildContext context) async { + if (selectedTime == null) return; + final picked = + await showTimePicker(context: context, initialTime: selectedTime!); + if (picked != null && picked != selectedTime) selectTime!(picked); + } + + @override + Widget build(BuildContext context) { + final valueStyle = Theme.of(context).textTheme.headline6; + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 4, + child: InputDropdown( + labelText: labelText, + valueText: selectedDate == null + ? '' + : DateFormat.yMMMd().format(selectedDate as DateTime), + valueStyle: valueStyle, + onPressed: () { + _selectDate(context); + }, + ), + ), + if (enableTime) ...[ + const SizedBox(width: 12.0), + Expanded( + flex: 3, + child: InputDropdown( + valueText: selectedTime?.format(context) ?? '', + valueStyle: valueStyle, + onPressed: () { + _selectTime(context); + }, + ), + ), + ] + ], + ); + } +} diff --git a/device_calendar/example/lib/presentation/event_item.dart b/device_calendar/example/lib/presentation/event_item.dart new file mode 100644 index 00000000..f91bb7de --- /dev/null +++ b/device_calendar/example/lib/presentation/event_item.dart @@ -0,0 +1,342 @@ +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 'recurring_event_dialog.dart'; + +class EventItem extends StatefulWidget { + final Event? _calendarEvent; + final DeviceCalendarPlugin _deviceCalendarPlugin; + final bool _isReadOnly; + + final Function(Event) _onTapped; + final VoidCallback _onLoadingStarted; + final Function(bool) _onDeleteFinished; + + const EventItem( + this._calendarEvent, + this._deviceCalendarPlugin, + this._onLoadingStarted, + this._onDeleteFinished, + this._onTapped, + this._isReadOnly, + {Key? key}) + : super(key: key); + + @override + State createState() { + return _EventItemState(); + } +} + +class _EventItemState extends State { + final double _eventFieldNameWidth = 75.0; + Location? _currentLocation; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => setCurentLocation()); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (widget._calendarEvent != null) { + widget._onTapped(widget._calendarEvent as Event); + } + }, + child: Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + child: FlutterLogo(), + ), + ListTile( + title: Text(widget._calendarEvent?.title ?? ''), + subtitle: Text(widget._calendarEvent?.description ?? '')), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + if (_currentLocation != null) + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Starts'), + ), + Text( + widget._calendarEvent == null + ? '' + : _formatDateTime( + dateTime: widget._calendarEvent!.start!, + ), + ) + ], + ), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 5.0), + ), + if (_currentLocation != null) + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Ends'), + ), + Text( + widget._calendarEvent?.end == null + ? '' + : _formatDateTime( + dateTime: widget._calendarEvent!.end!, + ), + ), + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('All day?'), + ), + Text(widget._calendarEvent?.allDay != null && + widget._calendarEvent?.allDay == true + ? 'Yes' + : 'No') + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Location'), + ), + Expanded( + child: Text( + widget._calendarEvent?.location ?? '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('URL'), + ), + Expanded( + child: Text( + widget._calendarEvent?.url?.data?.contentText ?? '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Attendees'), + ), + Expanded( + child: Text( + widget._calendarEvent?.attendees + ?.where((a) => a?.name?.isNotEmpty ?? false) + .map((a) => a?.name) + .join(', ') ?? + '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Availability'), + ), + Expanded( + child: Text( + widget._calendarEvent?.availability.enumToString ?? + '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Status'), + ), + Expanded( + child: Text( + widget._calendarEvent?.status?.enumToString ?? '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + ], + ), + ), + ButtonBar( + children: [ + if (!widget._isReadOnly) ...[ + IconButton( + onPressed: () { + if (widget._calendarEvent != null) { + widget._onTapped(widget._calendarEvent as Event); + } + }, + icon: const Icon(Icons.edit), + ), + IconButton( + onPressed: () async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + if (widget._calendarEvent?.recurrenceRule == null) { + return AlertDialog( + title: const Text( + 'Are you sure you want to delete this event?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + widget._onLoadingStarted(); + final deleteResult = await widget + ._deviceCalendarPlugin + .deleteEvent( + widget._calendarEvent?.calendarId, + widget._calendarEvent?.eventId); + widget._onDeleteFinished( + deleteResult.isSuccess && + deleteResult.data != null); + }, + child: const Text('Delete'), + ), + ], + ); + } else { + if (widget._calendarEvent == null) { + return const SizedBox(); + } + return RecurringEventDialog( + widget._deviceCalendarPlugin, + widget._calendarEvent!, + widget._onLoadingStarted, + widget._onDeleteFinished); + } + }, + ); + }, + icon: const Icon(Icons.delete), + ), + ] else ...[ + IconButton( + onPressed: () { + if (widget._calendarEvent != null) { + widget._onTapped(widget._calendarEvent!); + } + }, + icon: const Icon(Icons.remove_red_eye), + ), + ] + ], + ) + ], + ), + ), + ); + } + + void setCurentLocation() async { + String? timezone; + try { + timezone = await FlutterNativeTimezone.getLocalTimezone(); + } catch (e) { + debugPrint('Could not get the local timezone'); + } + timezone ??= 'Etc/UTC'; + _currentLocation = timeZoneDatabase.locations[timezone]; + setState(() {}); + } + + /// Formats [dateTime] into a human-readable string. + /// If [_calendarEvent] is an Android allDay event, then the output will + /// omit the time. + String _formatDateTime({DateTime? dateTime}) { + if (dateTime == null) { + return 'Error'; + } + var output = ''; + if (Platform.isAndroid && widget._calendarEvent?.allDay == true) { + // just the dates, no times + output = DateFormat.yMd().format(dateTime); + } else { + output = DateFormat('yyyy-MM-dd HH:mm:ss') + .format(TZDateTime.from(dateTime, _currentLocation!)); + } + return output; + } +} diff --git a/device_calendar/example/lib/presentation/input_dropdown.dart b/device_calendar/example/lib/presentation/input_dropdown.dart new file mode 100644 index 00000000..a6c19820 --- /dev/null +++ b/device_calendar/example/lib/presentation/input_dropdown.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class InputDropdown extends StatelessWidget { + const InputDropdown( + {Key? key, + this.child, + this.labelText, + this.valueText, + this.valueStyle, + this.onPressed}) + : super(key: key); + + final String? labelText; + final String? valueText; + final TextStyle? valueStyle; + final VoidCallback? onPressed; + final Widget? child; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + child: InputDecorator( + decoration: InputDecoration( + labelText: labelText, + ), + baseStyle: valueStyle, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + if (valueText != null) Text(valueText!, style: valueStyle), + Icon(Icons.arrow_drop_down, + color: Theme.of(context).brightness == Brightness.light + ? Colors.grey.shade700 + : Colors.white70), + ], + ), + ), + ); + } +} diff --git a/device_calendar/example/lib/presentation/pages/calendar_add.dart b/device_calendar/example/lib/presentation/pages/calendar_add.dart new file mode 100644 index 00000000..7d6d8820 --- /dev/null +++ b/device_calendar/example/lib/presentation/pages/calendar_add.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +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(); + } +} + +class _CalendarAddPageState extends State { + final GlobalKey _formKey = GlobalKey(); + final GlobalKey _scaffoldKey = GlobalKey(); + late DeviceCalendarPlugin _deviceCalendarPlugin; + + AutovalidateMode _autovalidate = AutovalidateMode.disabled; + String _calendarName = ''; + ColorChoice? _colorChoice; + String _localAccountName = ''; + + _CalendarAddPageState() { + _deviceCalendarPlugin = DeviceCalendarPlugin(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Create Calendar'), + ), + body: Form( + autovalidateMode: _autovalidate, + key: _formKey, + child: Container( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + TextFormField( + decoration: const InputDecoration( + labelText: 'Calendar Name', + hintText: 'My New Calendar', + ), + validator: _validateCalendarName, + onSaved: (String? value) => _calendarName = value ?? '', + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Calendar Color'), + DropdownButton( + onChanged: (selectedColor) { + setState(() => _colorChoice = selectedColor); + }, + value: _colorChoice, + items: ColorChoice.values + .map((color) => DropdownMenuItem( + value: color, + child: Text(color.toString().split('.').last), + )) + .toList(), + ), + ], + ), + if (Platform.isAndroid) + TextFormField( + decoration: const InputDecoration( + labelText: 'Local Account Name', + hintText: 'Device Calendar', + ), + onSaved: (String? value) => _localAccountName = value ?? '', + ), + ], + ), + ), + ), + floatingActionButton: FloatingActionButton( + 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.'); + } else { + form?.save(); + var result = await _deviceCalendarPlugin.createCalendar( + _calendarName, + calendarColor: _colorChoice?.value, + localAccountName: _localAccountName, + ); + + if (result.isSuccess) { + Navigator.pop(context, true); + } else { + showInSnackBar(result.errors + .map((err) => '[${err.errorCode}] ${err.errorMessage}') + .join(' | ')); + } + } + }, + child: const Icon(Icons.check), + ), + ); + } + + String? _validateCalendarName(String? value) { + if (value == null) return null; + if (value.isEmpty) { + return 'Calendar name is required.'; + } + + return null; + } + + void showInSnackBar(String value) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); + } +} + +enum ColorChoice { + Red, + Orange, + Yellow, + Green, + Blue, + Purple, + Brown, + Black, + White +} + +extension ColorChoiceExtension on ColorChoice { + static Color _value(ColorChoice val) { + switch (val) { + case ColorChoice.Red: + return Colors.red; + case ColorChoice.Orange: + return Colors.orange; + case ColorChoice.Yellow: + return Colors.yellow; + case ColorChoice.Green: + return Colors.green; + case ColorChoice.Blue: + return Colors.blue; + case ColorChoice.Purple: + return Colors.purple; + case ColorChoice.Brown: + return Colors.brown; + case ColorChoice.Black: + return Colors.black; + case ColorChoice.White: + return Colors.white; + default: + return Colors.red; + } + } + + Color get value => _value(this); +} diff --git a/device_calendar/example/lib/presentation/pages/calendar_event.dart b/device_calendar/example/lib/presentation/pages/calendar_event.dart new file mode 100644 index 00000000..72c4cb5b --- /dev/null +++ b/device_calendar/example/lib/presentation/pages/calendar_event.dart @@ -0,0 +1,1261 @@ +import 'dart:io'; + +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 '../date_time_picker.dart'; +import '../recurring_event_dialog.dart'; +import 'event_attendee.dart'; +import 'event_reminders.dart'; + +enum RecurrenceRuleEndType { Indefinite, MaxOccurrences, SpecifiedEndDate } + +class CalendarEventPage extends StatefulWidget { + final Calendar _calendar; + final Event? _event; + final RecurringEventDialog? _recurringEventDialog; + + const CalendarEventPage(this._calendar, + [this._event, this._recurringEventDialog, Key? key]) + : super(key: key); + + @override + _CalendarEventPageState createState() { + return _CalendarEventPageState(_calendar, _event, _recurringEventDialog); + } +} + +class _CalendarEventPageState extends State { + final GlobalKey _formKey = GlobalKey(); + final GlobalKey _scaffoldKey = GlobalKey(); + final Calendar _calendar; + + Event? _event; + 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; + + TZDateTime? _endDate; + TimeOfDay? _endTime; + + AutovalidateMode _autovalidate = AutovalidateMode.disabled; + DayOfWeekGroup _dayOfWeekGroup = DayOfWeekGroup.None; + + RecurrenceRuleEndType _recurrenceRuleEndType = + RecurrenceRuleEndType.Indefinite; + RecurrenceRule? _rrule; + + final List _validDaysOfMonth = []; + + Availability _availability = Availability.Busy; + EventStatus? _eventStatus; + List? _attendees; + List? _reminders; + String _timezone = 'Etc/UTC'; + + _CalendarEventPageState( + this._calendar, this._event, this._recurringEventDialog) { + getCurentLocation(); + } + + void getCurentLocation() async { + try { + _timezone = await FlutterNativeTimezone.getLocalTimezone(); + } catch (e) { + debugPrint('Could not get the local timezone'); + } + + _deviceCalendarPlugin = DeviceCalendarPlugin(); + + final event = _event; + if (event == null) { + debugPrint( + 'calendar_event _timezone ------------------------- $_timezone'); + final currentLocation = timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + 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']; + 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, availability: _availability); + + debugPrint('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); + + _eventStatus = EventStatus.None; + } else { + 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); + } + + final attendees = event.attendees; + if (attendees != null && attendees.isNotEmpty) { + _attendees = []; + _attendees?.addAll(attendees as Iterable); + } + + final reminders = event.reminders; + if (reminders != null && reminders.isNotEmpty) { + _reminders = []; + _reminders?.addAll(reminders); + } + + final rrule = event.recurrenceRule; + if (rrule != null) { + // debugPrint('OLD_RRULE: ${rrule.toString()}'); + _rrule = rrule; + if (rrule.count != null) { + _recurrenceRuleEndType = RecurrenceRuleEndType.MaxOccurrences; + } + if (rrule.until != null) { + _recurrenceRuleEndType = RecurrenceRuleEndType.SpecifiedEndDate; + } + } + + _availability = event.availability; + _eventStatus = event.status; + } + + // Getting days of the current month (or a selected month for the yearly recurrence) as a default + _getValidDaysOfMonth(_rrule?.frequency); + setState(() {}); + } + + void printAttendeeDetails(Attendee attendee) { + debugPrint( + 'attendee name: ${attendee.name}, email address: ${attendee.emailAddress}, type: ${attendee.role?.enumToString}'); + debugPrint( + 'ios specifics - status: ${attendee.iosAttendeeDetails?.attendanceStatus}, type: ${attendee.iosAttendeeDetails?.attendanceStatus?.enumToString}'); + debugPrint( + 'android specifics - status ${attendee.androidAttendeeDetails?.attendanceStatus}, type: ${attendee.androidAttendeeDetails?.attendanceStatus?.enumToString}'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: Text(_event?.eventId?.isEmpty ?? true + ? 'Create event' + : _calendar.isReadOnly == true + ? 'View event ${_event?.title}' + : 'Edit event ${_event?.title}'), + ), + 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?.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; + }, + ), + ), + 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; + } + }, + ), + ), + ListTile( + leading: const Text( + 'Availability', + style: TextStyle(fontSize: 16), + ), + trailing: DropdownButton( + value: _availability, + onChanged: (Availability? newValue) { + setState(() { + if (newValue != null) { + _availability = newValue; + _event?.availability = newValue; + } + }); + }, + items: Availability.values + .map>( + (Availability value) { + return DropdownMenuItem( + value: value, + child: Text(value.enumToString), + ); + }).toList(), + ), + ), + if (Platform.isAndroid) + ListTile( + leading: const 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: const Text('All Day'), + ), + if (_startDate != null) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'From', + enableTime: _event?.allDay == false, + selectedDate: _startDate, + selectedTime: _startTime, + selectDate: (DateTime date) { + setState(() { + var currentLocation = + timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + _startDate = + TZDateTime.from(date, currentLocation); + _event?.start = _combineDateWithTime( + _startDate, _startTime); + } + }); + }, + selectTime: (TimeOfDay time) { + setState( + () { + _startTime = time; + _event?.start = _combineDateWithTime( + _startDate, _startTime); + }, + ); + }, + ), + ), + 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); + _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: const 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) => + const EventAttendeePage())); + if (result != null) { + _attendees ??= []; + setState(() { + _attendees?.add(result); + }); + } + } + : null, + leading: const Icon(Icons.people), + title: Text(_calendar.isReadOnly == false + ? 'Add Attendees' + : 'Attendees'), + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: _attendees?.length ?? 0, + itemBuilder: (context, index) { + return Container( + color: (_attendees?[index].isOrganiser ?? false) + ? MediaQuery.of(context).platformBrightness == + Brightness.dark + ? Colors.black26 + : 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), + 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 ?? + 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: const 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: const 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: const 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 ?? true) + Text(_calendar.isReadOnly == false + ? 'Add reminders' + : 'Reminders'), + for (var reminder in _reminders ?? []) + Text('${reminder.minutes} minutes before; ') + ], + ), + ), + ), + ), + CheckboxListTile( + value: _rrule != null, + title: const Text('Is recurring'), + onChanged: (isChecked) { + if (isChecked != null) { + setState(() { + if (isChecked) { + _rrule = + RecurrenceRule(frequency: Frequency.daily); + } else { + _rrule = null; + } + }); + } + }, + ), + if (_rrule != null) ...[ + ListTile( + leading: const Text('Select a Recurrence Type'), + trailing: DropdownButton( + onChanged: (selectedFrequency) { + setState(() { + _onFrequencyChange( + selectedFrequency ?? Frequency.daily); + _getValidDaysOfMonth(selectedFrequency); + }); + }, + 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(), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), + child: Row( + children: [ + const Text('Repeat Every '), + Flexible( + child: TextFormField( + initialValue: '${_rrule?.interval ?? 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) { + _rrule = _rrule?.copyWith( + interval: int.tryParse(value)); + } + }, + ), + ), + _recurrenceFrequencyToIntervalText( + _rrule?.frequency), + ], + ), + ), + 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(() { + 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: 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: const Text('Delete'), + ), + ], + ], + ), + ), + ), + ), + floatingActionButton: Visibility( + visible: _calendar.isReadOnly == false, + child: FloatingActionButton( + key: const Key('saveEventButton'), + onPressed: () async { + final form = _formKey.currentState; + if (form?.validate() == false) { + _autovalidate = + AutovalidateMode.always; // Start validating on every change. + showInSnackBar( + context, 'Please fix the errors in red before submitting.'); + return; + } else { + form?.save(); + _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: const Icon(Icons.check), + ), + ), + ); + } + + 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(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 const Text('Indefinitely'); + case RecurrenceRuleEndType.MaxOccurrences: + return const Text('After a set number of times'); + case RecurrenceRuleEndType.SpecifiedEndDate: + return const Text('Continues until a specified date'); + default: + return const Text(''); + } + } + + // Get total days of a month + void _getValidDaysOfMonth(Frequency? frequency) { + _validDaysOfMonth.clear(); + var totalDays = 0; + + // Year frequency: Get total days of the selected month + if (frequency == Frequency.yearly) { + totalDays = DateTime(DateTime.now().year, + (_rrule?.hasByMonths ?? false) ? _rrule!.byMonths.first : 1, 0) + .day; + } else { + // Otherwise, get total days of the current month + var now = DateTime.now(); + totalDays = DateTime(now.year, now.month + 1, 0).day; + } + + for (var i = 1; i <= totalDays; i++) { + _validDaysOfMonth.add(i); + } + } + + void _updateDaysOfWeek() { + 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: + _rrule = _rrule?.copyWith(byWeekDays: { + ByWeekDayEntry(1), + ByWeekDayEntry(2), + ByWeekDayEntry(3), + ByWeekDayEntry(4), + ByWeekDayEntry(5), + ByWeekDayEntry(6), + ByWeekDayEntry(7), + }); + break; + case DayOfWeekGroup.None: + default: + _rrule?.byWeekDays.clear(); + break; + } + // () => setState(() => {}); + } + + void _updateDaysOfWeekGroup({DayOfWeek? selectedDay}) { + 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; + } + } + } + + int _weekNumFromWeekDayOccurence(Set weekdays) { + final weekNum = weekdays.first.occurrence; + if (weekNum != null) { + return weekNum - 1; + } else { + return 0; + } + } + + 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); + } + } + } + } + + /// 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; + } + } + } + + String? _validateTotalOccurrences(String? value) { + if (value == null) return null; + if (value.isNotEmpty && int.tryParse(value) == null) { + return 'Total occurrences needs to be a valid number'; + } + return null; + } + + String? _validateInterval(String? value) { + if (value == null) return null; + if (value.isNotEmpty && int.tryParse(value) == null) { + return 'Interval needs to be a valid number'; + } + return null; + } + + String? _validateTitle(String? value) { + if (value == null) return null; + if (value.isEmpty) { + return 'Name is required.'; + } + return null; + } + + TZDateTime? _combineDateWithTime(TZDateTime? date, TimeOfDay? time) { + if (date == null) return null; + var currentLocation = timeZoneDatabase.locations[_timezone]; + + final dateWithoutTime = TZDateTime.from( + DateTime.parse(DateFormat('y-MM-dd 00:00:00').format(date)), + currentLocation!); + + if (time == null) return dateWithoutTime; + if (Platform.isAndroid && _event?.allDay == true) return dateWithoutTime; + + return dateWithoutTime + .add(Duration(hours: time.hour, minutes: time.minute)); + } + + void showInSnackBar(BuildContext context, String value) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); + } +} diff --git a/device_calendar/example/lib/presentation/pages/calendar_events.dart b/device_calendar/example/lib/presentation/pages/calendar_events.dart new file mode 100644 index 00000000..a8d4b2b2 --- /dev/null +++ b/device_calendar/example/lib/presentation/pages/calendar_events.dart @@ -0,0 +1,190 @@ +import 'dart:async'; + +import 'package:device_calendar/device_calendar.dart'; +import 'package:flutter/material.dart'; + +import '../event_item.dart'; +import '../recurring_event_dialog.dart'; +import 'calendar_event.dart'; + +class CalendarEventsPage extends StatefulWidget { + final Calendar _calendar; + + const CalendarEventsPage(this._calendar, {Key? key}) : super(key: key); + + @override + _CalendarEventsPageState createState() { + return _CalendarEventsPageState(_calendar); + } +} + +class _CalendarEventsPageState extends State { + final Calendar _calendar; + final GlobalKey _scaffoldstate = GlobalKey(); + + late DeviceCalendarPlugin _deviceCalendarPlugin; + List _calendarEvents = []; + bool _isLoading = true; + + _CalendarEventsPageState(this._calendar) { + _deviceCalendarPlugin = DeviceCalendarPlugin(); + } + + @override + void initState() { + super.initState(); + _retrieveCalendarEvents(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldstate, + appBar: AppBar( + title: Text('${_calendar.name} events'), + actions: [_getDeleteButton()], + ), + body: (_calendarEvents.isNotEmpty || _isLoading) + ? Stack( + children: [ + ListView.builder( + itemCount: _calendarEvents.length, + itemBuilder: (BuildContext context, int index) { + return EventItem( + _calendarEvents[index], + _deviceCalendarPlugin, + _onLoading, + _onDeletedFinished, + _onTapped, + _calendar.isReadOnly != null && + _calendar.isReadOnly as bool); + }, + ), + if (_isLoading) + const Center( + child: CircularProgressIndicator(), + ) + ], + ) + : const Center(child: Text('No events found')), + floatingActionButton: _getAddEventButton(context)); + } + + Widget? _getAddEventButton(BuildContext context) { + if (_calendar.isReadOnly == false || _calendar.isReadOnly == null) { + return FloatingActionButton( + key: const Key('addEventButton'), + onPressed: () async { + final refreshEvents = await Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) { + return CalendarEventPage(_calendar); + })); + if (refreshEvents == true) { + await _retrieveCalendarEvents(); + } + }, + child: const Icon(Icons.add), + ); + } else { + return null; + } + } + + void _onLoading() { + setState(() { + _isLoading = true; + }); + } + + Future _onDeletedFinished(bool deleteSucceeded) async { + if (deleteSucceeded) { + await _retrieveCalendarEvents(); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Oops, we ran into an issue deleting the event'), + backgroundColor: Colors.red, + duration: Duration(seconds: 5), + )); + setState(() { + _isLoading = false; + }); + } + } + + Future _onTapped(Event event) async { + final refreshEvents = await Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) { + return CalendarEventPage( + _calendar, + event, + RecurringEventDialog( + _deviceCalendarPlugin, + event, + _onLoading, + _onDeletedFinished, + ), + ); + })); + if (refreshEvents != null && refreshEvents) { + await _retrieveCalendarEvents(); + } + } + + Future _retrieveCalendarEvents() async { + final startDate = DateTime.now().add(const Duration(days: -30)); + final endDate = DateTime.now().add(const Duration(days: 365 * 10)); + var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( + _calendar.id, + RetrieveEventsParams(startDate: startDate, endDate: endDate)); + setState(() { + _calendarEvents = calendarEventsResult.data ?? []; + _isLoading = false; + }); + } + + Widget _getDeleteButton() { + return IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + await _showDeleteDialog(); + }); + } + + Future _showDeleteDialog() async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Warning'), + content: SingleChildScrollView( + child: ListBody( + children: const [ + Text('This will delete this calendar'), + Text('Are you sure?'), + ], + ), + ), + actions: [ + TextButton( + onPressed: () async { + var returnValue = + await _deviceCalendarPlugin.deleteCalendar(_calendar.id!); + debugPrint( + 'returnValue: ${returnValue.data}, ${returnValue.errors}'); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + child: const Text('Delete!'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + ], + ); + }, + ); + } +} diff --git a/device_calendar/example/lib/presentation/pages/calendars.dart b/device_calendar/example/lib/presentation/pages/calendars.dart new file mode 100644 index 00000000..71c47ea5 --- /dev/null +++ b/device_calendar/example/lib/presentation/pages/calendars.dart @@ -0,0 +1,161 @@ +import 'package:device_calendar/device_calendar.dart'; +import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'calendar_events.dart'; + +class CalendarsPage extends StatefulWidget { + const CalendarsPage({Key? key}) : super(key: key); + + @override + _CalendarsPageState createState() { + return _CalendarsPageState(); + } +} + +class _CalendarsPageState extends State { + late DeviceCalendarPlugin _deviceCalendarPlugin; + List _calendars = []; + List get _writableCalendars => + _calendars.where((c) => c.isReadOnly == false).toList(); + + List get _readOnlyCalendars => + _calendars.where((c) => c.isReadOnly == true).toList(); + + _CalendarsPageState() { + _deviceCalendarPlugin = DeviceCalendarPlugin(); + } + + @override + void initState() { + super.initState(); + _retrieveCalendars(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Calendars'), + actions: [_getRefreshButton()], + ), + body: Column( + children: [ + Padding( + 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, + ), + ), + Expanded( + flex: 1, + child: ListView.builder( + 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)}'), + onTap: () async { + await Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) { + return CalendarEventsPage(_calendars[index], + key: const Key('calendarEventsPage')); + })); + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + children: [ + Expanded( + 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, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(_calendars[index].color!)), + ), + 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: const Text('Default'), + ), + Icon(_calendars[index].isReadOnly == true + ? Icons.lock + : Icons.lock_open) + ], + ), + ), + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + final createCalendar = await Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) { + return const CalendarAddPage(); + })); + + if (createCalendar == true) { + _retrieveCalendars(); + } + }, + child: const Icon(Icons.add), + ), + ); + } + + void _retrieveCalendars() async { + try { + var permissionsGranted = await _deviceCalendarPlugin.hasPermissions(); + if (permissionsGranted.isSuccess && + (permissionsGranted.data == null || + permissionsGranted.data == false)) { + permissionsGranted = await _deviceCalendarPlugin.requestPermissions(); + if (!permissionsGranted.isSuccess || + permissionsGranted.data == null || + permissionsGranted.data == false) { + return; + } + } + + final calendarsResult = await _deviceCalendarPlugin.retrieveCalendars(); + setState(() { + _calendars = calendarsResult.data as List; + }); + } on PlatformException catch (e, s) { + debugPrint('RETRIEVE_CALENDARS: $e, $s'); + } + } + + Widget _getRefreshButton() { + return IconButton( + icon: const Icon(Icons.refresh), + onPressed: () async { + _retrieveCalendars(); + }); + } +} diff --git a/device_calendar/example/lib/presentation/pages/event_attendee.dart b/device_calendar/example/lib/presentation/pages/event_attendee.dart new file mode 100644 index 00000000..2fff734e --- /dev/null +++ b/device_calendar/example/lib/presentation/pages/event_attendee.dart @@ -0,0 +1,174 @@ +import 'dart:io'; + +import 'package:device_calendar/device_calendar.dart'; +import 'package:device_calendar_example/common/app_routes.dart'; +import 'package:flutter/material.dart'; + +late DeviceCalendarPlugin _deviceCalendarPlugin; + +class EventAttendeePage extends StatefulWidget { + final Attendee? attendee; + final String? eventId; + const EventAttendeePage({Key? key, this.attendee, this.eventId}) + : super(key: key); + + @override + _EventAttendeePageState createState() => + _EventAttendeePageState(attendee, eventId ?? ''); +} + +class _EventAttendeePageState extends State { + Attendee? _attendee; + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _emailAddressController = TextEditingController(); + var _role = AttendeeRole.None; + var _status = AndroidAttendanceStatus.None; + String _eventId = ''; + + _EventAttendeePageState(Attendee? attendee, eventId) { + if (attendee != null) { + _attendee = attendee; + _nameController.text = _attendee!.name!; + _emailAddressController.text = _attendee!.emailAddress!; + _role = _attendee!.role!; + _status = _attendee!.androidAttendeeDetails?.attendanceStatus ?? + AndroidAttendanceStatus.None; + } + _eventId = eventId; + } + + @override + void dispose() { + _nameController.dispose(); + _emailAddressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_attendee != null + ? 'Edit attendee ${_attendee!.name}' + : 'Add an Attendee'), + ), + body: Column( + children: [ + Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + controller: _nameController, + validator: (value) { + if (_attendee?.isCurrentUser == false && + (value == null || value.isEmpty)) { + return 'Please enter a name'; + } + return null; + }, + decoration: const InputDecoration(labelText: 'Name'), + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + controller: _emailAddressController, + validator: (value) { + if (value == null || + value.isEmpty || + !value.contains('@')) { + return 'Please enter a valid email address'; + } + return null; + }, + decoration: + const InputDecoration(labelText: 'Email Address'), + ), + ), + ListTile( + leading: const Text('Role'), + trailing: DropdownButton( + onChanged: (value) { + setState(() { + _role = value as AttendeeRole; + }); + }, + value: _role, + items: AttendeeRole.values + .map((role) => DropdownMenuItem( + value: role, + child: Text(role.enumToString), + )) + .toList(), + ), + ), + Visibility( + visible: Platform.isIOS, + child: ListTile( + onTap: () async { + _deviceCalendarPlugin = DeviceCalendarPlugin(); + + await _deviceCalendarPlugin + .showiOSEventModal(_eventId); + Navigator.popUntil( + context, ModalRoute.withName(AppRoutes.calendars)); + //TODO: finish calling and getting attendee details from iOS + }, + leading: const Icon(Icons.edit), + title: const Text('View / edit iOS attendance details'), + ), + ), + Visibility( + visible: Platform.isAndroid, + child: ListTile( + leading: const Text('Android attendee status'), + trailing: DropdownButton( + onChanged: (value) { + setState(() { + _status = value as AndroidAttendanceStatus; + }); + }, + value: _status, + items: AndroidAttendanceStatus.values + .map((status) => DropdownMenuItem( + value: status, + child: Text(status.enumToString), + )) + .toList(), + ), + ), + ) + ], + ), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + setState(() { + _attendee = Attendee( + name: _nameController.text, + emailAddress: _emailAddressController.text, + role: _role, + isOrganiser: _attendee?.isOrganiser ?? false, + isCurrentUser: _attendee?.isCurrentUser ?? false, + iosAttendeeDetails: _attendee?.iosAttendeeDetails, + androidAttendeeDetails: AndroidAttendeeDetails.fromJson( + {'attendanceStatus': _status.index})); + + _emailAddressController.clear(); + }); + + Navigator.pop(context, _attendee); + } + }, + child: Text(_attendee != null ? 'Update' : 'Add'), + ) + ], + ), + ); + } +} diff --git a/device_calendar/example/lib/presentation/pages/event_reminders.dart b/device_calendar/example/lib/presentation/pages/event_reminders.dart new file mode 100644 index 00000000..4b0a11f3 --- /dev/null +++ b/device_calendar/example/lib/presentation/pages/event_reminders.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:device_calendar/device_calendar.dart'; + +class EventRemindersPage extends StatefulWidget { + final List _reminders; + const EventRemindersPage(this._reminders, {Key? key}) : super(key: key); + + @override + _EventRemindersPageState createState() => + _EventRemindersPageState(_reminders); +} + +class _EventRemindersPageState extends State { + List _reminders = []; + final _formKey = GlobalKey(); + final _minutesController = TextEditingController(); + + _EventRemindersPageState(List reminders) { + _reminders = [...reminders]; + } + + @override + void dispose() { + _minutesController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Reminders'), + ), + body: Column( + children: [ + Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + children: [ + Expanded( + child: TextFormField( + controller: _minutesController, + validator: (value) { + if (value == null || + value.isEmpty || + int.tryParse(value) == null) { + return 'Please enter a reminder time in minutes'; + } + return null; + }, + decoration: const InputDecoration( + labelText: 'Minutes before start'), + ), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + setState(() { + _reminders.add(Reminder( + minutes: int.parse(_minutesController.text))); + _minutesController.clear(); + }); + } + }, + child: const Text('Add'), + ), + ], + ), + ), + ), + Expanded( + child: ListView.builder( + itemCount: _reminders.length, + itemBuilder: (context, index) { + return ListTile( + title: Text('${_reminders[index].minutes} minutes'), + trailing: ElevatedButton( + onPressed: () { + setState(() { + _reminders.removeWhere( + (a) => a.minutes == _reminders[index].minutes); + }); + }, + child: const Text('Delete'), + ), + ); + }, + ), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context, _reminders); + }, + child: const Text('Done'), + ) + ], + ), + ); + } +} diff --git a/device_calendar/example/lib/presentation/recurring_event_dialog.dart b/device_calendar/example/lib/presentation/recurring_event_dialog.dart new file mode 100644 index 00000000..c8b8ff35 --- /dev/null +++ b/device_calendar/example/lib/presentation/recurring_event_dialog.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:device_calendar/device_calendar.dart'; + +class RecurringEventDialog extends StatefulWidget { + final DeviceCalendarPlugin _deviceCalendarPlugin; + final Event _calendarEvent; + + final VoidCallback _onLoadingStarted; + final Function(bool) _onDeleteFinished; + + const RecurringEventDialog(this._deviceCalendarPlugin, this._calendarEvent, + this._onLoadingStarted, this._onDeleteFinished, + {Key? key}) + : super(key: key); + + @override + _RecurringEventDialogState createState() => + _RecurringEventDialogState(_deviceCalendarPlugin, _calendarEvent, + onLoadingStarted: _onLoadingStarted, + onDeleteFinished: _onDeleteFinished); +} + +class _RecurringEventDialogState extends State { + late DeviceCalendarPlugin _deviceCalendarPlugin; + late Event _calendarEvent; + VoidCallback? _onLoadingStarted; + Function(bool)? _onDeleteFinished; + + _RecurringEventDialogState( + DeviceCalendarPlugin deviceCalendarPlugin, Event calendarEvent, + {VoidCallback? onLoadingStarted, Function(bool)? onDeleteFinished}) { + _deviceCalendarPlugin = deviceCalendarPlugin; + _calendarEvent = calendarEvent; + _onLoadingStarted = onLoadingStarted; + _onDeleteFinished = onDeleteFinished; + } + + @override + Widget build(BuildContext context) { + return SimpleDialog( + title: const Text('Are you sure you want to delete this event?'), + children: [ + SimpleDialogOption( + onPressed: () async { + Navigator.of(context).pop(true); + if (_onLoadingStarted != null) _onLoadingStarted!(); + final deleteResult = + await _deviceCalendarPlugin.deleteEventInstance( + _calendarEvent.calendarId, + _calendarEvent.eventId, + _calendarEvent.start?.millisecondsSinceEpoch, + _calendarEvent.end?.millisecondsSinceEpoch, + false); + if (_onDeleteFinished != null) { + _onDeleteFinished!( + deleteResult.isSuccess && deleteResult.data != null); + } + }, + child: const Text('This instance only'), + ), + SimpleDialogOption( + onPressed: () async { + Navigator.of(context).pop(true); + if (_onLoadingStarted != null) _onLoadingStarted!(); + final deleteResult = + await _deviceCalendarPlugin.deleteEventInstance( + _calendarEvent.calendarId, + _calendarEvent.eventId, + _calendarEvent.start?.millisecondsSinceEpoch, + _calendarEvent.end?.millisecondsSinceEpoch, + true); + if (_onDeleteFinished != null) { + _onDeleteFinished!( + deleteResult.isSuccess && deleteResult.data != null); + } + }, + child: const Text('This and following instances'), + ), + SimpleDialogOption( + onPressed: () async { + Navigator.of(context).pop(true); + if (_onLoadingStarted != null) _onLoadingStarted!(); + final deleteResult = await _deviceCalendarPlugin.deleteEvent( + _calendarEvent.calendarId, _calendarEvent.eventId); + if (_onDeleteFinished != null) { + _onDeleteFinished!( + deleteResult.isSuccess && deleteResult.data != null); + } + }, + child: const Text('All instances'), + ), + SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ) + ], + ); + } +} diff --git a/device_calendar/example/pubspec.yaml b/device_calendar/example/pubspec.yaml new file mode 100644 index 00000000..b32d2fb0 --- /dev/null +++ b/device_calendar/example/pubspec.yaml @@ -0,0 +1,27 @@ +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" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + intl: ^0.17.0 + uuid: ^3.0.6 + flutter_native_timezone: ^2.0.0 + device_calendar: + path: ../ + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/device_calendar/ios/.gitignore b/device_calendar/ios/.gitignore new file mode 100644 index 00000000..710ec6cf --- /dev/null +++ b/device_calendar/ios/.gitignore @@ -0,0 +1,36 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig diff --git a/device_calendar/ios/Assets/.gitkeep b/device_calendar/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/device_calendar/ios/Classes/DeviceCalendarPlugin.h b/device_calendar/ios/Classes/DeviceCalendarPlugin.h new file mode 100644 index 00000000..0d5ad0b9 --- /dev/null +++ b/device_calendar/ios/Classes/DeviceCalendarPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface DeviceCalendarPlugin : NSObject +@end diff --git a/device_calendar/ios/Classes/DeviceCalendarPlugin.m b/device_calendar/ios/Classes/DeviceCalendarPlugin.m new file mode 100644 index 00000000..774b46d3 --- /dev/null +++ b/device_calendar/ios/Classes/DeviceCalendarPlugin.m @@ -0,0 +1,8 @@ +#import "DeviceCalendarPlugin.h" +#import + +@implementation DeviceCalendarPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftDeviceCalendarPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift b/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift new file mode 100644 index 00000000..f37d1a5a --- /dev/null +++ b/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -0,0 +1,1136 @@ +import EventKit +import EventKitUI +import Flutter +import Foundation +import UIKit + +extension Date { + var millisecondsSinceEpoch: Double { return self.timeIntervalSince1970 * 1000.0 } +} + +extension EKParticipant { + var emailAddress: String? { + return self.value(forKey: "emailAddress") as? String + } +} + +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" } + + if (!iCloudSources.isEmpty) { + return iCloudSources.first + } + + return nil + } + + 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 + } + + 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)) + } + } + + 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 { + 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"; + } + } + + 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 { + 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(fourYearsTimeInterval) + while currentEndDate <= endDate { + 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 [fourYearsTimeInterval] + currentStartDate = currentEndDate + currentEndDate = currentStartDate.addingTimeInterval(fourYearsTimeInterval) + } + + // If the cycle doesn't end exactly on the end date + if currentStartDate <= endDate { + 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) + } + } + } + + 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 { + for ekParticipant in ekEvent.attendees! { + let attendee = convertEkParticipantToAttendee(ekParticipant: ekParticipant) + 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, + calendarId: calendarId, + eventTitle: ekEvent.title ?? "New Event", + eventDescription: ekEvent.notes, + eventStartDate: Int64(ekEvent.startDate.millisecondsSinceEpoch), + eventEndDate: Int64(ekEvent.endDate.millisecondsSinceEpoch), + eventStartTimeZone: ekEvent.timeZone?.identifier, + eventAllDay: ekEvent.isAllDay, + attendees: attendees, + eventLocation: ekEvent.location, + eventURL: ekEvent.url?.absoluteString, + recurrenceRule: recurrenceRule, + organizer: convertEkParticipantToAttendee(ekParticipant: ekEvent.organizer), + reminders: reminders, + availability: convertEkEventAvailability(ekEventAvailability: ekEvent.availability), + eventStatus: convertEkEventStatus(ekEventStatus: ekEvent.status) + ) + + return event + } + + 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 + } + + private func convertEkEventAvailability(ekEventAvailability: EKEventAvailability?) -> Availability? { + switch ekEventAvailability { + case .busy: + return Availability.BUSY + case .free: + return Availability.FREE + case .tentative: + return Availability.TENTATIVE + case .unavailable: + return Availability.UNAVAILABLE + default: + return nil + } + } + + private func convertEkEventStatus(ekEventStatus: EKEventStatus?) -> EventStatus? { + switch ekEventStatus { + case .confirmed: + return EventStatus.CONFIRMED + case .tentative: + return EventStatus.TENTATIVE + case .canceled: + return EventStatus.CANCELED + case .none?: + return EventStatus.NONE + default: + return nil + } + } + + private func parseEKRecurrenceRules(_ ekEvent: EKEvent) -> RecurrenceRule? { + var recurrenceRule: RecurrenceRule? + if ekEvent.hasRecurrenceRules { + let ekRecurrenceRule = ekEvent.recurrenceRules![0] + var frequency: String + switch ekRecurrenceRule.frequency { + case EKRecurrenceFrequency.daily: + frequency = "DAILY" + case EKRecurrenceFrequency.weekly: + frequency = "WEEKLY" + case EKRecurrenceFrequency.monthly: + frequency = "MONTHLY" + case EKRecurrenceFrequency.yearly: + frequency = "YEARLY" + default: + frequency = "DAILY" + } + + var count: Int? + var endDate: String? + if(ekRecurrenceRule.recurrenceEnd?.occurrenceCount != nil && ekRecurrenceRule.recurrenceEnd?.occurrenceCount != 0) { + count = ekRecurrenceRule.recurrenceEnd?.occurrenceCount + } + + let endDateRaw = ekRecurrenceRule.recurrenceEnd?.endDate + if(endDateRaw != nil) { + endDate = formateDateTime(dateTime: endDateRaw!) + } + + 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( + freq: frequency, + count: count, + interval: ekRecurrenceRule.interval, + 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 recurrenceFrequency = recurrenceRuleArguments![recurrenceFrequencyArgument] as? String + let totalOccurrences = recurrenceRuleArguments![countArgument] as? NSInteger + let interval = recurrenceRuleArguments![intervalArgument] as? NSInteger + var recurrenceInterval = 1 + 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 { + 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 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!)} + } + } + + 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: 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 + } + if existingAttendee != nil && ekEvent!.organizer?.emailAddress != existingAttendee?.emailAddress{ + attendees.append(existingAttendee!) + 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 + } + + switch availabilityValue.uppercased() { + case Availability.BUSY.rawValue: + return .busy + case Availability.FREE.rawValue: + return .free + 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 + let calendarId = arguments[calendarIdArgument] as! String + let eventId = arguments[eventIdArgument] as? String + let isAllDay = arguments[eventAllDayArgument] as! Bool + let startDateMillisecondsSinceEpoch = arguments[eventStartDateArgument] as! NSNumber + let endDateDateMillisecondsSinceEpoch = arguments[eventEndDateArgument] as! NSNumber + let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch.doubleValue / 1000.0) + let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch.doubleValue / 1000.0) + let startTimeZoneString = arguments[eventStartTimeZoneArgument] as? String + let title = arguments[self.eventTitleArgument] as? String + let description = arguments[self.eventDescriptionArgument] as? String + let location = arguments[self.eventLocationArgument] as? String + let url = arguments[self.eventURLArgument] 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 + } + + var ekEvent: EKEvent? + if eventId == nil { + ekEvent = EKEvent.init(eventStore: self.eventStore) + } else { + ekEvent = self.eventStore.event(withIdentifier: eventId!) + if(ekEvent == nil) { + self.finishWithEventNotFoundError(result: result, eventId: eventId!) + return + } + } + + ekEvent!.title = title ?? "" + ekEvent!.notes = description + ekEvent!.isAllDay = isAllDay + ekEvent!.startDate = startDate + ekEvent!.endDate = endDate + + if (!isAllDay) { + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current + ekEvent!.timeZone = timeZone + } + + ekEvent!.calendar = ekCalendar! + ekEvent!.location = location + + // Create and add URL object only when if the input string is not empty or nil + if let urlCheck = url, !urlCheck.isEmpty { + let iosUrl = URL(string: url ?? "") + ekEvent!.url = iosUrl + } + 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) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + }, result: result) + } + + private func createParticipant(name: String, emailAddress: String, role: Int) -> EKParticipant? { + 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") + return participant as? EKParticipant + } + return nil + } + + private func deleteEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + let eventId = arguments[eventIdArgument] as! String + 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) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } + 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) + } + else { + try self.eventStore.remove(ekEvent!, span: .futureEvents, commit: true) + } + + result(true) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } + }, 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 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() + let jsonData = try jsonEncoder.encode(codable) + let jsonString = String(data: jsonData, encoding: .utf8) + result(jsonString) + } catch { + result(FlutterError(code: genericError, message: error.localizedDescription, details: nil)) + } + } + + private func checkPermissionsThenExecute(permissionsGrantedAction: () -> Void, result: @escaping FlutterResult) { + if hasEventPermissions() { + permissionsGrantedAction() + return + } + self.finishWithUnauthorizedError(result: result) + } + + private func requestPermissions(_ completion: @escaping (Bool) -> Void) { + if hasEventPermissions() { + completion(true) + return + } + 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) + if #available(iOS 17, *) { + return status == EKAuthorizationStatus.fullAccess + } else { + return status == EKAuthorizationStatus.authorized + } + } +} + +extension Date { + func convert(from initTimeZone: TimeZone, to targetTimeZone: TimeZone) -> Date { + let delta = TimeInterval(initTimeZone.secondsFromGMT() - targetTimeZone.secondsFromGMT()) + return addingTimeInterval(delta) + } +} + +extension UIColor { + func rgb() -> Int? { + var fRed : CGFloat = 0 + var fGreen : CGFloat = 0 + var fBlue : CGFloat = 0 + var fAlpha: CGFloat = 0 + if self.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha) { + let iRed = Int(fRed * 255.0) + let iGreen = Int(fGreen * 255.0) + let iBlue = Int(fBlue * 255.0) + let iAlpha = Int(fAlpha * 255.0) + + // (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue). + let rgb = (iAlpha << 24) + (iRed << 16) + (iGreen << 8) + iBlue + return rgb + } else { + // Could not extract RGBA components: + return nil + } + } + + public convenience init?(hex: String) { + let r, g, b, a: CGFloat + + if hex.hasPrefix("0x") { + let start = hex.index(hex.startIndex, offsetBy: 2) + let hexColor = String(hex[start...]) + + if hexColor.count == 8 { + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + + if scanner.scanHexInt64(&hexNumber) { + a = CGFloat((hexNumber & 0xff000000) >> 24) / 255 + 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 + } + } + } + + return nil + } + +} diff --git a/device_calendar/ios/device_calendar.podspec b/device_calendar/ios/device_calendar.podspec new file mode 100644 index 00000000..3230bcf3 --- /dev/null +++ b/device_calendar/ios/device_calendar.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'device_calendar' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.swift_version = '5.0' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.ios.deployment_target = '8.0' +end + diff --git a/device_calendar/lib/device_calendar.dart b/device_calendar/lib/device_calendar.dart new file mode 100644 index 00000000..3566d5df --- /dev/null +++ b/device_calendar/lib/device_calendar.dart @@ -0,0 +1,17 @@ +library device_calendar; + +export 'src/common/calendar_enums.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 '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'; +export 'src/models/platform_specifics/android/attendance_status.dart'; +export 'src/device_calendar.dart'; +export 'package:timezone/timezone.dart'; diff --git a/device_calendar/lib/src/common/calendar_enums.dart b/device_calendar/lib/src/common/calendar_enums.dart new file mode 100644 index 00000000..aa77ec02 --- /dev/null +++ b/device_calendar/lib/src/common/calendar_enums.dart @@ -0,0 +1,315 @@ +enum DayOfWeek { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, +} + +enum DayOfWeekGroup { + None, + Weekday, + Weekend, + AllDays, +} + +enum MonthOfYear { + January, + Feburary, + March, + April, + May, + June, + July, + August, + September, + October, + November, + December, +} + +enum WeekNumber { + First, + Second, + Third, + Fourth, + Last, +} + +enum AttendeeRole { + None, + Required, + Optional, + Resource, +} + +enum Availability { + Free, + Busy, + Tentative, + Unavailable, +} + +enum EventStatus { + None, + Confirmed, + Canceled, + Tentative, +} + +extension DayOfWeekExtension on DayOfWeek { + static int _value(DayOfWeek val) { + switch (val) { + case DayOfWeek.Monday: + return 1; + case DayOfWeek.Tuesday: + return 2; + case DayOfWeek.Wednesday: + return 3; + case DayOfWeek.Thursday: + return 4; + case DayOfWeek.Friday: + return 5; + case DayOfWeek.Saturday: + return 6; + case DayOfWeek.Sunday: + return 0; + default: + return 1; + } + } + + String _enumToString(DayOfWeek enumValue) { + return enumValue.toString().split('.').last; + } + + int get value => _value(this); + + String get enumToString => _enumToString(this); +} + +extension DaysOfWeekGroupExtension on DayOfWeekGroup { + static List _getDays(DayOfWeekGroup val) { + switch (val) { + case DayOfWeekGroup.Weekday: + return [ + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday + ]; + case DayOfWeekGroup.Weekend: + return [DayOfWeek.Saturday, DayOfWeek.Sunday]; + case DayOfWeekGroup.AllDays: + return [ + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, + DayOfWeek.Saturday, + DayOfWeek.Sunday + ]; + default: + return []; + } + } + + String _enumToString(DayOfWeekGroup enumValue) { + return enumValue.toString().split('.').last; + } + + List get getDays => _getDays(this); + + String get enumToString => _enumToString(this); +} + +extension MonthOfYearExtension on MonthOfYear { + static int _value(MonthOfYear val) { + switch (val) { + case MonthOfYear.January: + return 1; + case MonthOfYear.Feburary: + return 2; + case MonthOfYear.March: + return 3; + case MonthOfYear.April: + return 4; + case MonthOfYear.May: + return 5; + case MonthOfYear.June: + return 6; + case MonthOfYear.July: + return 7; + case MonthOfYear.August: + return 8; + case MonthOfYear.September: + return 9; + case MonthOfYear.October: + return 10; + case MonthOfYear.November: + return 11; + case MonthOfYear.December: + return 12; + default: + return 1; + } + } + + String _enumToString(MonthOfYear enumValue) { + return enumValue.toString().split('.').last; + } + + int get value => _value(this); + + String get enumToString => _enumToString(this); +} + +extension WeekNumberExtension on WeekNumber { + static int _value(WeekNumber val) { + switch (val) { + case WeekNumber.First: + return 1; + case WeekNumber.Second: + return 2; + case WeekNumber.Third: + return 3; + case WeekNumber.Fourth: + return 4; + case WeekNumber.Last: + return -1; + default: + return 1; + } + } + + String _enumToString(WeekNumber enumValue) { + return enumValue.toString().split('.').last; + } + + int get value => _value(this); + + String get enumToString => _enumToString(this); +} + +extension IntExtensions on int { + static DayOfWeek _getDayOfWeekEnumValue(int val) { + switch (val) { + case 1: + return DayOfWeek.Monday; + case 2: + return DayOfWeek.Tuesday; + case 3: + return DayOfWeek.Wednesday; + case 4: + return DayOfWeek.Thursday; + case 5: + return DayOfWeek.Friday; + case 6: + return DayOfWeek.Saturday; + case 0: + return DayOfWeek.Sunday; + default: + return DayOfWeek.Monday; + } + } + + static MonthOfYear _getMonthOfYearEnumValue(int val) { + switch (val) { + case 1: + return MonthOfYear.January; + case 2: + return MonthOfYear.Feburary; + case 3: + return MonthOfYear.March; + case 4: + return MonthOfYear.April; + case 5: + return MonthOfYear.May; + case 6: + return MonthOfYear.June; + case 7: + return MonthOfYear.July; + case 8: + return MonthOfYear.August; + case 9: + return MonthOfYear.September; + case 10: + return MonthOfYear.October; + case 11: + return MonthOfYear.November; + case 12: + return MonthOfYear.December; + default: + return MonthOfYear.January; + } + } + + static WeekNumber _getWeekNumberEnumValue(int val) { + switch (val) { + case 1: + return WeekNumber.First; + case 2: + return WeekNumber.Second; + case 3: + return WeekNumber.Third; + case 4: + return WeekNumber.Fourth; + case -1: + return WeekNumber.Last; + default: + return WeekNumber.First; + } + } + + DayOfWeek get getDayOfWeekEnumValue => _getDayOfWeekEnumValue(this); + + MonthOfYear get getMonthOfYearEnumValue => _getMonthOfYearEnumValue(this); + + WeekNumber get getWeekNumberEnumValue => _getWeekNumberEnumValue(this); +} + +extension RoleExtensions on AttendeeRole { + String _enumToString(AttendeeRole enumValue) { + return enumValue.toString().split('.').last; + } + + String get enumToString => _enumToString(this); +} + +extension AvailabilityExtensions on Availability { + String _enumToString(Availability enumValue) { + switch (enumValue) { + case Availability.Busy: + return 'BUSY'; + case Availability.Free: + return 'FREE'; + case Availability.Tentative: + return 'TENTATIVE'; + case Availability.Unavailable: + return 'UNAVAILABLE'; + } + } + + String get enumToString => _enumToString(this); +} + +extension EventStatusExtensions on EventStatus { + String _enumToString(EventStatus enumValue) { + switch (enumValue) { + case EventStatus.Confirmed: + return 'CONFIRMED'; + case EventStatus.Tentative: + return 'TENTATIVE'; + case EventStatus.Canceled: + return 'CANCELED'; + case EventStatus.None: + return 'NONE'; + } + } + + String get enumToString => _enumToString(this); +} diff --git a/device_calendar/lib/src/common/channel_constants.dart b/device_calendar/lib/src/common/channel_constants.dart new file mode 100644 index 00000000..2eef3d2d --- /dev/null +++ b/device_calendar/lib/src/common/channel_constants.dart @@ -0,0 +1,26 @@ +class ChannelConstants { + static const String channelName = 'plugins.builttoroam.com/device_calendar'; + + static const String methodNameRequestPermissions = 'requestPermissions'; + static const String methodNameHasPermissions = 'hasPermissions'; + static const String methodNameRetrieveCalendars = 'retrieveCalendars'; + static const String methodNameRetrieveEvents = 'retrieveEvents'; + static const String methodNameDeleteEvent = 'deleteEvent'; + static const String methodNameDeleteEventInstance = 'deleteEventInstance'; + static const String methodNameCreateOrUpdateEvent = 'createOrUpdateEvent'; + static const String methodNameCreateCalendar = 'createCalendar'; + static const String methodNameDeleteCalendar = 'deleteCalendar'; + static const String methodNameShowiOSEventModal = 'showiOSEventModal'; + + static const String parameterNameCalendarId = 'calendarId'; + static const String parameterNameStartDate = 'startDate'; + static const String parameterNameEndDate = 'endDate'; + static const String parameterNameEventId = 'eventId'; + static const String parameterNameEventIds = 'eventIds'; + static const String parameterNameEventStartDate = 'eventStartDate'; + static const String parameterNameEventEndDate = 'eventEndDate'; + static const String parameterNameFollowingInstances = 'followingInstances'; + static const String parameterNameCalendarName = 'calendarName'; + static const String parameterNameCalendarColor = 'calendarColor'; + static const String parameterNameLocalAccountName = 'localAccountName'; +} diff --git a/device_calendar/lib/src/common/error_codes.dart b/device_calendar/lib/src/common/error_codes.dart new file mode 100644 index 00000000..e75b29de --- /dev/null +++ b/device_calendar/lib/src/common/error_codes.dart @@ -0,0 +1,6 @@ +class ErrorCodes { + static const int invalidArguments = 400; + static const int platformSpecific = 599; + static const int generic = 500; + static const int unknown = 502; +} diff --git a/device_calendar/lib/src/common/error_messages.dart b/device_calendar/lib/src/common/error_messages.dart new file mode 100644 index 00000000..f2126006 --- /dev/null +++ b/device_calendar/lib/src/common/error_messages.dart @@ -0,0 +1,27 @@ +class ErrorMessages { + static const String fromJsonMapIsNull = 'The json object is null'; + + static const String invalidMissingCalendarId = + 'Calendar ID is missing or invalid'; + + static const String invalidRetrieveEventsParams = + 'A valid instance of the RetrieveEventsParams class is required. Must the event ids to filter by or the start and end date to filter by or a combination of these'; + static const String deleteEventInvalidArgumentsMessage = + 'Calendar ID and/or Event ID argument(s) have not been specified or are invalid'; + static const String createOrUpdateEventInvalidArgumentsMessageAllDay = + "To create or update an all day event you must provide calendar ID, event with a title and event's start date"; + static const String createOrUpdateEventInvalidArgumentsMessage = + "To create or update an event you must provide calendar ID, event with a title and event's start date and end date (where start date must be before end date)"; + static const String createCalendarInvalidCalendarNameMessage = + 'Calendar name must not be null or empty'; + + static const String invalidRecurrencyFrequency = + 'Invalid recurrency frequency'; + + 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.'; + static const String unknownDeviceGenericExceptionTemplate = + 'Device calendar plugin ran into an issue, with message "%s"'; +} diff --git a/device_calendar/lib/src/device_calendar.dart b/device_calendar/lib/src/device_calendar.dart new file mode 100644 index 00000000..4c1d12f1 --- /dev/null +++ b/device_calendar/lib/src/device_calendar.dart @@ -0,0 +1,443 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +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 { + static const MethodChannel channel = + MethodChannel(ChannelConstants.channelName); + + static final DeviceCalendarPlugin _instance = DeviceCalendarPlugin.private(); + + factory DeviceCalendarPlugin({bool shouldInitTimezone = true}) { + if (shouldInitTimezone) { + tz.initializeTimeZones(); + } + return _instance; + } + + @visibleForTesting + DeviceCalendarPlugin.private(); + + /// Requests permissions to modify the calendars on the device + /// + /// Returns a [Result] indicating if calendar READ and WRITE permissions + /// have (true) or have not (false) been granted + Future> requestPermissions() async { + return _invokeChannelMethod( + ChannelConstants.methodNameRequestPermissions, + ); + } + + /// Checks if permissions for modifying the device calendars have been granted + /// + /// Returns a [Result] indicating if calendar READ and WRITE permissions + /// have (true) or have not (false) been granted + Future> hasPermissions() async { + return _invokeChannelMethod( + ChannelConstants.methodNameHasPermissions, + ); + } + + /// Retrieves all of the device defined calendars + /// + /// Returns a [Result] containing a list of device [Calendar] + Future>> retrieveCalendars() async { + return _invokeChannelMethod( + ChannelConstants.methodNameRetrieveCalendars, + evaluateResponse: (rawData) => UnmodifiableListView( + json.decode(rawData).map( + (decodedCalendar) => Calendar.fromJson(decodedCalendar), + ), + ), + ); + } + + /// Retrieves the events from the specified calendar + /// + /// The `calendarId` paramter is the id of the calendar that plugin will return events for + /// The `retrieveEventsParams` parameter combines multiple properties that + /// specifies conditions of the events retrieval. For instance, defining [RetrieveEventsParams.startDate] + /// and [RetrieveEventsParams.endDate] will return events only happening in that time range + /// + /// Returns a [Result] containing a list [Event], that fall + /// into the specified parameters + Future>> retrieveEvents( + 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( + 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.\ + /// To delete individual instance of a recurring event, please use [deleteEventInstance()] + /// + /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\ + /// The `eventId` parameter is the id of the event that plugin will try to delete + /// + /// Returns a [Result] indicating if the event has (true) or has not (false) been deleted from the calendar + Future> deleteEvent( + String? calendarId, + String? eventId, + ) async { + return _invokeChannelMethod( + ChannelConstants.methodNameDeleteEvent, + assertParameters: (result) { + _validateCalendarIdParameter( + result, + calendarId, + ); + + _assertParameter( + result, + eventId?.isNotEmpty ?? false, + ErrorCodes.invalidArguments, + ErrorMessages.deleteEventInvalidArgumentsMessage, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarId: calendarId, + ChannelConstants.parameterNameEventId: eventId, + }, + ); + } + + /// Deletes an instance of a recurring event from a calendar. This should be used for a recurring event only.\ + /// If `startDate`, `endDate` or `deleteFollowingInstances` is not valid or null, then all instances of the event will be deleted. + /// + /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\ + /// The `eventId` parameter is the id of the event that plugin will try to delete\ + /// The `startDate` parameter is the start date of the instance to delete\ + /// The `endDate` parameter is the end date of the instance to delete\ + /// The `deleteFollowingInstances` parameter will also delete the following instances if set to true + /// + /// Returns a [Result] indicating if the instance of the event has (true) or has not (false) been deleted from the calendar + Future> deleteEventInstance( + String? calendarId, + String? eventId, + int? startDate, + int? endDate, + bool deleteFollowingInstances, + ) async { + return _invokeChannelMethod( + ChannelConstants.methodNameDeleteEventInstance, + assertParameters: (result) { + _validateCalendarIdParameter( + result, + calendarId, + ); + + _assertParameter( + result, + eventId?.isNotEmpty ?? false, + ErrorCodes.invalidArguments, + ErrorMessages.deleteEventInvalidArgumentsMessage, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarId: calendarId, + ChannelConstants.parameterNameEventId: eventId, + ChannelConstants.parameterNameEventStartDate: startDate, + ChannelConstants.parameterNameEventEndDate: endDate, + ChannelConstants.parameterNameFollowingInstances: + deleteFollowingInstances, + }, + ); + } + + /// Creates or updates an event + /// + /// The `event` paramter specifies how event data should be saved into the calendar + /// Always specify the [Event.calendarId], to inform the plugin in which calendar + /// it should create or update the event. + /// + /// Returns a [Result] with the newly created or updated [Event.eventId] + Future?> createOrUpdateEvent(Event? event) async { + if (event == null) return null; + return _invokeChannelMethod( + ChannelConstants.methodNameCreateOrUpdateEvent, + assertParameters: (result) { + // Setting time to 0 for all day events + if (event.allDay == true) { + if (event.start != null) { + var dateStart = DateTime(event.start!.year, event.start!.month, + event.start!.day, 0, 0, 0); + // 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) + : TZDateTime.from(dateStart, + timeZoneDatabase.locations[event.start!.location.name]!); + } + if (event.end != null) { + var dateEnd = DateTime( + event.end!.year, event.end!.month, event.end!.day, 0, 0, 0); + // allDay events on Android need to be at midnight UTC on the + // day after the last day. For example, a 2-day allDay event on + // 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(const Duration(days: 1)) + : TZDateTime.from(dateEnd, + timeZoneDatabase.locations[event.end!.location.name]!); + } + } + + _assertParameter( + result, + !(event.allDay == true && (event.calendarId?.isEmpty ?? true) || + event.start == null || + event.end == null), + ErrorCodes.invalidArguments, + ErrorMessages.createOrUpdateEventInvalidArgumentsMessageAllDay, + ); + + _assertParameter( + result, + !(event.allDay != true && + ((event.calendarId?.isEmpty ?? true) || + event.start == null || + event.end == null || + (event.start != null && + event.end != null && + event.start!.isAfter(event.end!)))), + ErrorCodes.invalidArguments, + ErrorMessages.createOrUpdateEventInvalidArgumentsMessage, + ); + }, + arguments: () => event.toJson(), + ); + } + + /// Creates a new local calendar for the current device. + /// + /// The `calendarName` parameter is the name of the new calendar\ + /// The `calendarColor` parameter is the color of the calendar. If null, + /// a default color (red) will be used\ + /// The `localAccountName` parameter is the name of the local account: + /// - [Android] Required. If `localAccountName` parameter is null or empty, it will default to 'Device Calendar'. + /// If the account name already exists in the device, it will add another calendar under the account, + /// otherwise a new local account and a new calendar will be created. + /// - [iOS] Not used. A local account will be picked up automatically, if not found, an error will be thrown. + /// + /// Returns a [Result] with the newly created [Calendar.id] + Future> createCalendar( + String? calendarName, { + Color? calendarColor, + String? localAccountName, + }) async { + return _invokeChannelMethod( + ChannelConstants.methodNameCreateCalendar, + assertParameters: (result) { + calendarColor ??= Colors.red; + + _assertParameter( + result, + calendarName?.isNotEmpty == true, + ErrorCodes.invalidArguments, + ErrorMessages.createCalendarInvalidCalendarNameMessage, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarName: calendarName, + ChannelConstants.parameterNameCalendarColor: + '0x${calendarColor?.value.toRadixString(16)}', + ChannelConstants.parameterNameLocalAccountName: + localAccountName?.isEmpty ?? true + ? 'Device Calendar' + : localAccountName + }, + ); + } + + /// Deletes a calendar. + /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\/// + /// Returns a [Result] indicating if the instance of the calendar has (true) or has not (false) been deleted + Future> deleteCalendar( + String calendarId, + ) async { + return _invokeChannelMethod( + ChannelConstants.methodNameDeleteCalendar, + assertParameters: (result) { + _validateCalendarIdParameter( + result, + calendarId, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarId: calendarId, + }, + ); + } + + /// 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 + Future> showiOSEventModal( + String eventId, + ) { + return _invokeChannelMethod( + ChannelConstants.methodNameShowiOSEventModal, + arguments: () => { + ChannelConstants.parameterNameEventId: eventId, + }, + ); + } + + Future> _invokeChannelMethod( + String channelMethodName, { + Function(Result)? assertParameters, + Map Function()? arguments, + T Function(dynamic)? evaluateResponse, + }) async { + final result = Result(); + + try { + if (assertParameters != null) { + assertParameters(result); + if (result.hasErrors) { + return result; + } + } + + var rawData = await channel.invokeMethod( + channelMethodName, + arguments != null ? arguments() : null, + ); + + if (evaluateResponse != null) { + result.data = evaluateResponse(rawData); + } else { + result.data = rawData; + } + } 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; + } + + void _parsePlatformExceptionAndUpdateResult( + Exception? exception, Result result) { + if (exception == null) { + result.errors.add( + const ResultError( + ErrorCodes.unknown, + ErrorMessages.unknownDeviceIssue, + ), + ); + return; + } + + debugPrint('$exception'); + + if (exception is PlatformException) { + result.errors.add( + ResultError( + ErrorCodes.platformSpecific, + '${ErrorMessages.unknownDeviceExceptionTemplate}, Code: ${exception.code}, Exception: ${exception.message}', + ), + ); + } else { + result.errors.add( + ResultError( + ErrorCodes.generic, + '${ErrorMessages.unknownDeviceGenericExceptionTemplate} ${exception.toString}', + ), + ); + } + } + + void _assertParameter( + Result result, + bool predicate, + int errorCode, + String errorMessage, + ) { + if (result.data != null) { + debugPrint("RESULT of _assertParameter: ${result.data}"); + } + if (!predicate) { + result.errors.add( + ResultError(errorCode, errorMessage), + ); + } + } + + void _validateCalendarIdParameter( + Result result, + String? calendarId, + ) { + _assertParameter( + result, + calendarId?.isNotEmpty ?? false, + ErrorCodes.invalidArguments, + ErrorMessages.invalidMissingCalendarId, + ); + } +} diff --git a/device_calendar/lib/src/models/attendee.dart b/device_calendar/lib/src/models/attendee.dart new file mode 100644 index 00000000..2e8e4cb9 --- /dev/null +++ b/device_calendar/lib/src/models/attendee.dart @@ -0,0 +1,81 @@ +import 'dart:io' show Platform; + +import '../common/calendar_enums.dart'; +import '../common/error_messages.dart'; +import 'platform_specifics/android/attendee_details.dart'; +import 'platform_specifics/ios/attendee_details.dart'; + +/// A person attending an event +class Attendee { + /// The name of the attendee + String? name; + + /// The email address of the attendee + String? emailAddress; + + /// An attendee role: None, Optional, Required or Resource + AttendeeRole? role; + + /// Read-only. Returns true if the attendee is an organiser, else false + bool isOrganiser = false; + + /// Read-only. Returns true if the attendee is the current user, else false + bool isCurrentUser = false; + + /// Details about the attendee that are specific to iOS. + /// When reading details for an existing event, this will only be populated on iOS devices. + IosAttendeeDetails? iosAttendeeDetails; + + /// Details about the attendee that are specific to Android. + /// When reading details for an existing event, this will only be populated on Android devices. + AndroidAttendeeDetails? androidAttendeeDetails; + + Attendee({ + this.name, + this.emailAddress, + this.role, + this.isOrganiser = false, + this.isCurrentUser = false, + this.iosAttendeeDetails, + this.androidAttendeeDetails, + }); + + Attendee.fromJson(Map? json) { + if (json == null) { + throw ArgumentError(ErrorMessages.fromJsonMapIsNull); + } + + name = json['name']; + emailAddress = json['emailAddress']; + role = AttendeeRole.values[json['role'] ?? 0]; + isOrganiser = json['isOrganizer'] ?? + false; // Getting and setting an organiser for Android + isCurrentUser = json['isCurrentUser'] ?? false; + + if (Platform.isAndroid) { + androidAttendeeDetails = AndroidAttendeeDetails.fromJson(json); + } + + if (Platform.isIOS) { + iosAttendeeDetails = IosAttendeeDetails.fromJson(json); + } + } + + Map toJson() { + final data = { + 'name': name, + 'emailAddress': emailAddress, + 'role': role?.index, + 'isOrganizer': isOrganiser, + }; + + if (iosAttendeeDetails != null) { + data.addEntries(iosAttendeeDetails!.toJson().entries); + } + if (androidAttendeeDetails != null) { + data.addEntries(androidAttendeeDetails!.toJson().entries); + } + + return data; + } +} diff --git a/device_calendar/lib/src/models/calendar.dart b/device_calendar/lib/src/models/calendar.dart new file mode 100644 index 00000000..c04ab405 --- /dev/null +++ b/device_calendar/lib/src/models/calendar.dart @@ -0,0 +1,56 @@ +/// A calendar on the user's device +class Calendar { + /// Read-only. The unique identifier for this calendar + String? id; + + /// The name of this calendar + String? name; + + /// Read-only. If the calendar is read-only + bool? isReadOnly; + + /// Read-only. If the calendar is the default + bool? isDefault; + + /// Read-only. Color of the calendar + int? color; + + // Read-only. Account name associated with the calendar + String? accountName; + + // Read-only. Account type associated with the calendar + String? accountType; + + Calendar( + {this.id, + this.name, + this.isReadOnly, + this.isDefault, + this.color, + this.accountName, + this.accountType}); + + Calendar.fromJson(Map json) { + id = json['id']; + name = json['name']; + isReadOnly = json['isReadOnly']; + isDefault = json['isDefault']; + color = json['color']; + accountName = json['accountName']; + accountType = json['accountType']; + } + + Map toJson() { + final data = { + 'id': id, + 'name': name, + 'isReadOnly': isReadOnly, + 'isDefault': isDefault, + 'color': color, + 'accountName': accountName, + 'accountType': accountType + }; + + return data; + } +} diff --git a/device_calendar/lib/src/models/event.dart b/device_calendar/lib/src/models/event.dart new file mode 100644 index 00000000..00ebfa52 --- /dev/null +++ b/device_calendar/lib/src/models/event.dart @@ -0,0 +1,306 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; + +import '../../device_calendar.dart'; +import '../common/error_messages.dart'; + +/// An event associated with a calendar +class Event { + /// Read-only. The unique identifier for this event. This is auto-generated when a new event is created + String? eventId; + + /// Read-only. The identifier of the calendar that this event is associated with + String? calendarId; + + /// The title of this event + String? title; + + /// The description for this event + String? description; + + /// Indicates when the event starts + TZDateTime? start; + + /// Indicates when the event ends + TZDateTime? end; + + /// Indicates if this is an all-day event + bool? allDay; + + /// The location of this event + String? location; + + /// An URL for this event + Uri? url; + + /// A list of attendees for this event + List? attendees; + + /// The recurrence rule for this event + RecurrenceRule? recurrenceRule; + + /// A list of reminders (by minutes) for this event + List? reminders; + + /// Indicates if this event counts as busy time, tentative, unavaiable or is still free time + late Availability availability; + + /// Indicates if this event is of confirmed, canceled, tentative or none status + EventStatus? status; + + //##### + /// Read-only. Color of the event + int? color; + + ///Note for development: + /// + ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. + ///Make sure all locations are updated if changes needed to be made. + ///Swift: + ///`ios/Classes/SwiftDeviceCalendarPlugin.swift` + ///Kotlin: + ///`android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt` + ///`android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt` + ///`android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt` + Event( + this.calendarId, { + this.eventId, + this.title, + this.start, + this.end, + this.description, + this.attendees, + this.recurrenceRule, + this.reminders, + this.availability = Availability.Busy, + this.location, + this.url, + this.allDay = false, + this.status, + this.color, + }); + + ///Get Event from JSON. + /// + ///Sample JSON: + ///{calendarId: 00, eventId: 0000, eventTitle: Sample Event, eventDescription: This is a sample event, eventStartDate: 1563719400000, eventStartTimeZone: Asia/Hong_Kong, eventEndDate: 1640532600000, eventEndTimeZone: Asia/Hong_Kong, eventAllDay: false, eventLocation: Yuenlong Station, eventURL: null, availability: BUSY, attendees: [{name: commonfolk, emailAddress: total.loss@hong.com, role: 1, isOrganizer: false, attendanceStatus: 3}], reminders: [{minutes: 39}]} + Event.fromJson(Map? json) { + if (json == null) { + throw ArgumentError(ErrorMessages.fromJsonMapIsNull); + } + String? foundUrl; + String? startLocationName; + String? endLocationName; + int? startTimestamp; + int? endTimestamp; + bool legacyJSON = false; + var legacyName = { + title: 'title', + description: 'description', + startTimestamp: 'start', + endTimestamp: 'end', + startLocationName: 'startTimeZone', + endLocationName: 'endTimeZone', + allDay: 'allDay', + location: 'location', + foundUrl: 'url', + }; + legacyName.forEach((key, value) { + if (json[value] != null) { + key = json[value]; + legacyJSON = true; + } + }); + + eventId = json['eventId']; + calendarId = json['calendarId']; + title = json['eventTitle']; + description = json['eventDescription']; + color = json['eventColor']; //##### + + startTimestamp = json['eventStartDate']; + startLocationName = json['eventStartTimeZone']; + var startTimeZone = timeZoneDatabase.locations[startLocationName]; + startTimeZone ??= local; + start = startTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(startTimeZone, startTimestamp) : TZDateTime.now(local); + + endTimestamp = json['eventEndDate']; + endLocationName = json['eventEndTimeZone']; + var endLocation = timeZoneDatabase.locations[endLocationName]; + endLocation ??= startTimeZone; + end = endTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(endLocation, endTimestamp) : TZDateTime.now(local); + allDay = json['eventAllDay'] ?? false; + if (Platform.isAndroid && (allDay ?? false)) { + // On Android, the datetime in an allDay event is adjusted to local + // timezone, which can result in the wrong day, so we need to bring the + // date back to midnight UTC to get the correct date + var startOffset = start?.timeZoneOffset.inMilliseconds ?? 0; + var endOffset = end?.timeZoneOffset.inMilliseconds ?? 0; + // subtract the offset to get back to midnight on the correct date + start = start?.subtract(Duration(milliseconds: startOffset)); + 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(const Duration(days: 1)); + } + location = json['eventLocation']; + availability = parseStringToAvailability(json['availability']); + status = parseStringToEventStatus(json['eventStatus']); + + foundUrl = json['eventURL']?.toString(); + if (foundUrl?.isEmpty ?? true) { + url = null; + } else { + url = Uri.dataFromString(foundUrl as String); + } + + if (json['attendees'] != null) { + attendees = json['attendees'].map((decodedAttendee) { + return Attendee.fromJson(decodedAttendee); + }).toList(); + } + + if (json['organizer'] != null) { + // Getting and setting an organiser for iOS + var organiser = Attendee.fromJson(json['organizer']); + + var attendee = attendees?.firstWhereOrNull((at) => at?.name == organiser.name && at?.emailAddress == organiser.emailAddress); + if (attendee != null) { + attendee.isOrganiser = true; + } + } + + 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) { + reminders = json['reminders'].map((decodedReminder) { + return Reminder.fromJson(decodedReminder); + }).toList(); + } + if (legacyJSON) { + throw const FormatException('legacy JSON detected. Please update your current JSONs as they may not be supported later on.'); + } + } + + Map toJson() { + final data = {}; + + data['calendarId'] = calendarId; + data['eventId'] = eventId; + data['eventTitle'] = title; + data['eventDescription'] = description; + data['eventStartDate'] = start?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; + data['eventStartTimeZone'] = start?.location.name; + data['eventEndDate'] = end?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; + data['eventEndTimeZone'] = end?.location.name; + data['eventAllDay'] = allDay; + data['eventLocation'] = location; + data['eventURL'] = url?.data?.contentText; + data['availability'] = availability.enumToString; + data['eventStatus'] = status?.enumToString; + data['eventColor'] = color; //##### + + if (attendees != null) { + data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); + } + + if (attendees != null) { + data['organizer'] = attendees?.firstWhereOrNull((a) => a!.isOrganiser)?.toJson(); + } + + 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; + } + + Availability parseStringToAvailability(String? value) { + var testValue = value?.toUpperCase(); + switch (testValue) { + case 'BUSY': + return Availability.Busy; + case 'FREE': + return Availability.Free; + case 'TENTATIVE': + return Availability.Tentative; + case 'UNAVAILABLE': + return Availability.Unavailable; + } + return Availability.Busy; + } + + EventStatus? parseStringToEventStatus(String? value) { + var testValue = value?.toUpperCase(); + switch (testValue) { + case 'CONFIRMED': + return EventStatus.Confirmed; + case 'TENTATIVE': + return EventStatus.Tentative; + case 'CANCELED': + return EventStatus.Canceled; + case 'NONE': + return EventStatus.None; + } + return null; + } + + bool updateStartLocation(String? newStartLocation) { + if (newStartLocation == null) return false; + try { + var location = timeZoneDatabase.get(newStartLocation); + start = TZDateTime.from(start as TZDateTime, location); + return true; + } on LocationNotFoundException { + return false; + } + } + + bool updateEndLocation(String? newEndLocation) { + if (newEndLocation == null) return false; + try { + var location = timeZoneDatabase.get(newEndLocation); + end = TZDateTime.from(end as TZDateTime, location); + return true; + } on LocationNotFoundException { + return false; + } + } +} diff --git a/device_calendar/lib/src/models/platform_specifics/android/attendance_status.dart b/device_calendar/lib/src/models/platform_specifics/android/attendance_status.dart new file mode 100644 index 00000000..d895877f --- /dev/null +++ b/device_calendar/lib/src/models/platform_specifics/android/attendance_status.dart @@ -0,0 +1,15 @@ +enum AndroidAttendanceStatus { + None, + Accepted, + Declined, + Invited, + Tentative, +} + +extension AndroidAttendanceStatusExtensions on AndroidAttendanceStatus { + String _enumToString(AndroidAttendanceStatus enumValue) { + return enumValue.toString().split('.').last; + } + + String get enumToString => _enumToString(this); +} diff --git a/device_calendar/lib/src/models/platform_specifics/android/attendee_details.dart b/device_calendar/lib/src/models/platform_specifics/android/attendee_details.dart new file mode 100644 index 00000000..ba6f3b7a --- /dev/null +++ b/device_calendar/lib/src/models/platform_specifics/android/attendee_details.dart @@ -0,0 +1,23 @@ +import '../../../common/error_messages.dart'; +import 'attendance_status.dart'; + +class AndroidAttendeeDetails { + AndroidAttendanceStatus? attendanceStatus; + + AndroidAttendeeDetails({this.attendanceStatus}); + + AndroidAttendeeDetails.fromJson(Map? json) { + if (json == null) { + throw ArgumentError(ErrorMessages.fromJsonMapIsNull); + } + + if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { + attendanceStatus = + AndroidAttendanceStatus.values[json['attendanceStatus']]; + } + } + + Map toJson() { + return {'attendanceStatus': attendanceStatus?.index}; + } +} diff --git a/device_calendar/lib/src/models/platform_specifics/ios/attendance_status.dart b/device_calendar/lib/src/models/platform_specifics/ios/attendance_status.dart new file mode 100644 index 00000000..bd958391 --- /dev/null +++ b/device_calendar/lib/src/models/platform_specifics/ios/attendance_status.dart @@ -0,0 +1,18 @@ +enum IosAttendanceStatus { + Unknown, + Pending, + Accepted, + Declined, + Tentative, + Delegated, + Completed, + InProcess, +} + +extension IosAttendanceStatusExtensions on IosAttendanceStatus { + String _enumToString(IosAttendanceStatus enumValue) { + return enumValue.toString().split('.').last; + } + + String get enumToString => _enumToString(this); +} diff --git a/device_calendar/lib/src/models/platform_specifics/ios/attendee_details.dart b/device_calendar/lib/src/models/platform_specifics/ios/attendee_details.dart new file mode 100644 index 00000000..305a1b42 --- /dev/null +++ b/device_calendar/lib/src/models/platform_specifics/ios/attendee_details.dart @@ -0,0 +1,21 @@ +import '../../../common/error_messages.dart'; +import 'attendance_status.dart'; + +class IosAttendeeDetails { + IosAttendanceStatus? attendanceStatus; + IosAttendeeDetails({this.attendanceStatus}); + + IosAttendeeDetails.fromJson(Map? json) { + if (json == null) { + throw ArgumentError(ErrorMessages.fromJsonMapIsNull); + } + + if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { + attendanceStatus = IosAttendanceStatus.values[json['attendanceStatus']]; + } + } + + Map toJson() { + return {'attendanceStatus': attendanceStatus?.index}; + } +} diff --git a/device_calendar/lib/src/models/reminder.dart b/device_calendar/lib/src/models/reminder.dart new file mode 100644 index 00000000..761ab676 --- /dev/null +++ b/device_calendar/lib/src/models/reminder.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; + +class Reminder { + /// The time when the reminder should be triggered expressed in terms of minutes before the start of the event + int? minutes; + + Reminder({@required this.minutes}) + : assert(minutes != null && minutes >= 0, + 'Minutes must be greater than or equal than zero'); + + Reminder.fromJson(Map json) { + minutes = json['minutes'] as int; + } + + Map toJson() { + return {'minutes': minutes}; + } +} diff --git a/device_calendar/lib/src/models/result.dart b/device_calendar/lib/src/models/result.dart new file mode 100644 index 00000000..fff23c71 --- /dev/null +++ b/device_calendar/lib/src/models/result.dart @@ -0,0 +1,33 @@ +class Result { + /// Indicates if the request was successfull or not + /// + /// Returns true if data is not null and there're no error messages, otherwise returns false + bool get isSuccess { + var res = data != null && errors.isEmpty; + if (res) { + if (data is String) { + res = (data as String).isNotEmpty; + } + } + + return res; + } + + /// Indicates if there are errors. This isn't exactly the same as !isSuccess since + /// it doesn't look at the state of the data. + /// + /// Returns true if there are error messages, otherwise false + bool get hasErrors { + return errors.isNotEmpty; + } + + T? data; + List errors = []; +} + +class ResultError { + final int errorCode; + final String errorMessage; + + const ResultError(this.errorCode, this.errorMessage); +} diff --git a/device_calendar/lib/src/models/retrieve_events_params.dart b/device_calendar/lib/src/models/retrieve_events_params.dart new file mode 100644 index 00000000..7965b0e1 --- /dev/null +++ b/device_calendar/lib/src/models/retrieve_events_params.dart @@ -0,0 +1,7 @@ +class RetrieveEventsParams { + final List? eventIds; + final DateTime? startDate; + final DateTime? endDate; + + const RetrieveEventsParams({this.eventIds, this.startDate, this.endDate}); +} diff --git a/device_calendar/pubspec.yaml b/device_calendar/pubspec.yaml new file mode 100644 index 00000000..7c99a9c5 --- /dev/null +++ b/device_calendar/pubspec.yaml @@ -0,0 +1,29 @@ +name: device_calendar +description: A cross platform plugin for modifying calendars on the user's device. +version: 4.3.1 +homepage: https://github.com/builttoroam/device_calendar/tree/master + +dependencies: + flutter: + sdk: flutter + collection: ^1.16.0 + timezone: ^0.9.0 + rrule: ^0.2.10 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +flutter: + plugin: + platforms: + android: + package: com.builttoroam.devicecalendar + pluginClass: DeviceCalendarPlugin + ios: + pluginClass: DeviceCalendarPlugin + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" diff --git a/device_calendar/test/device_calendar_test.dart b/device_calendar/test/device_calendar_test.dart new file mode 100644 index 00000000..132aad61 --- /dev/null +++ b/device_calendar/test/device_calendar_test.dart @@ -0,0 +1,245 @@ +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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channel = MethodChannel('plugins.builttoroam.com/device_calendar'); + var deviceCalendarPlugin = DeviceCalendarPlugin(); + + final log = []; + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + print('Calling channel method ${methodCall.method}'); + log.add(methodCall); + + return null; + }); + + log.clear(); + }); + + test('HasPermissions_Returns_Successfully', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return true; + }); + + final result = await deviceCalendarPlugin.hasPermissions(); + expect(result.isSuccess, true); + expect(result.errors, isEmpty); + expect(result.data, true); + }); + + test('RequestPermissions_Returns_Successfully', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return true; + }); + + final result = await deviceCalendarPlugin.requestPermissions(); + expect(result.isSuccess, true); + expect(result.errors, isEmpty); + expect(result.data, true); + }); + + test('RetrieveCalendars_Returns_Successfully', () async { + const fakeCalendarName = 'fakeCalendarName'; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return '[{"id":"1","isReadOnly":false,"name":"$fakeCalendarName"}]'; + }); + + final result = await deviceCalendarPlugin.retrieveCalendars(); + expect(result.isSuccess, true); + expect(result.errors, isEmpty); + expect(result.data, isNotNull); + expect(result.data, isNotEmpty); + expect(result.data?[0].name, fakeCalendarName); + }); + + test('RetrieveEvents_CalendarId_IsRequired', () async { + const String? calendarId = null; + const params = RetrieveEventsParams(); + + final result = + await deviceCalendarPlugin.retrieveEvents(calendarId, params); + expect(result.isSuccess, false); + expect(result.errors.length, greaterThan(0)); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); + }); + + test('DeleteEvent_CalendarId_IsRequired', () async { + const String? calendarId = null; + const eventId = 'fakeEventId'; + + final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); + expect(result.isSuccess, false); + expect(result.errors.length, greaterThan(0)); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); + }); + + test('DeleteEvent_EventId_IsRequired', () async { + const calendarId = 'fakeCalendarId'; + const String? eventId = null; + + final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); + expect(result.isSuccess, false); + expect(result.errors.length, greaterThan(0)); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); + }); + + test('DeleteEvent_PassesArguments_Correctly', () async { + const calendarId = 'fakeCalendarId'; + const eventId = 'fakeEventId'; + + await deviceCalendarPlugin.deleteEvent(calendarId, eventId); + expect(log, [ + isMethodCall('deleteEvent', arguments: { + 'calendarId': calendarId, + 'eventId': eventId + }) + ]); + }); + + test('CreateEvent_Arguments_Invalid', () async { + const String? fakeCalendarId = null; + final event = Event(fakeCalendarId); + + final result = await deviceCalendarPlugin.createOrUpdateEvent(event); + expect(result!.isSuccess, false); + expect(result.errors, isNotEmpty); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); + }); + + test('CreateEvent_Returns_Successfully', () async { + const fakeNewEventId = 'fakeNewEventId'; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return fakeNewEventId; + }); + + const fakeCalendarId = 'fakeCalendarId'; + final event = Event(fakeCalendarId); + event.title = 'fakeEventTitle'; + event.start = TZDateTime.now(local); + event.end = event.start!.add(const Duration(hours: 1)); + + final result = await deviceCalendarPlugin.createOrUpdateEvent(event); + expect(result?.isSuccess, true); + expect(result?.errors, isEmpty); + expect(result?.data, isNotEmpty); + expect(result?.data, fakeNewEventId); + }); + + test('UpdateEvent_Returns_Successfully', () async { + const fakeNewEventId = 'fakeNewEventId'; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + final arguments = methodCall.arguments as Map; + if (!arguments.containsKey('eventId') || arguments['eventId'] == null) { + return null; + } + + return fakeNewEventId; + }); + + const fakeCalendarId = 'fakeCalendarId'; + final event = Event(fakeCalendarId); + event.eventId = 'fakeEventId'; + event.title = 'fakeEventTitle'; + event.start = TZDateTime.now(local); + event.end = event.start!.add(const Duration(hours: 1)); + + final result = await deviceCalendarPlugin.createOrUpdateEvent(event); + expect(result?.isSuccess, true); + expect(result?.errors, isEmpty); + expect(result?.data, isNotEmpty); + expect(result?.data, fakeNewEventId); + }); + + test('Attendee_Serialises_Correctly', () async { + final attendee = Attendee( + name: 'Test Attendee', + emailAddress: 'test@t.com', + role: AttendeeRole.Required, + isOrganiser: true); + final stringAttendee = attendee.toJson(); + expect(stringAttendee, isNotNull); + final newAttendee = Attendee.fromJson(stringAttendee); + expect(newAttendee, isNotNull); + expect(newAttendee.name, equals(attendee.name)); + expect(newAttendee.emailAddress, equals(attendee.emailAddress)); + expect(newAttendee.role, equals(attendee.role)); + expect(newAttendee.isOrganiser, equals(attendee.isOrganiser)); + expect(newAttendee.iosAttendeeDetails, isNull); + expect(newAttendee.androidAttendeeDetails, isNull); + }); + + test('Event_Serializes_Correctly', () async { + final startTime = TZDateTime( + timeZoneDatabase.locations.entries.skip(20).first.value, + 1980, + 10, + 1, + 0, + 0, + 0); + final endTime = TZDateTime( + timeZoneDatabase.locations.entries.skip(21).first.value, + 1980, + 10, + 2, + 0, + 0, + 0); + final attendee = Attendee( + name: 'Test Attendee', + emailAddress: 'test@t.com', + role: AttendeeRole.Required, + isOrganiser: true); + final recurrence = RecurrenceRule(frequency: Frequency.daily); + final reminder = Reminder(minutes: 10); + var event = Event('calendarId', + eventId: 'eventId', + title: 'Test Event', + start: startTime, + location: 'Seattle, Washington', + url: Uri.dataFromString('http://www.example.com'), + end: endTime, + attendees: [attendee], + description: 'Test description', + recurrenceRule: recurrence, + reminders: [reminder], + availability: Availability.Busy, + status: EventStatus.Confirmed); + + final stringEvent = event.toJson(); + expect(stringEvent, isNotNull); + final newEvent = Event.fromJson(stringEvent); + expect(newEvent, isNotNull); + expect(newEvent.calendarId, equals(event.calendarId)); + expect(newEvent.eventId, equals(event.eventId)); + expect(newEvent.title, equals(event.title)); + expect(newEvent.start!.millisecondsSinceEpoch, + equals(event.start!.millisecondsSinceEpoch)); + expect(newEvent.end!.millisecondsSinceEpoch, + equals(event.end!.millisecondsSinceEpoch)); + expect(newEvent.description, equals(event.description)); + expect(newEvent.url, equals(event.url)); + expect(newEvent.location, equals(event.location)); + expect(newEvent.attendees, isNotNull); + expect(newEvent.attendees?.length, equals(1)); + expect(newEvent.recurrenceRule, isNotNull); + expect(newEvent.recurrenceRule?.frequency, + equals(event.recurrenceRule?.frequency)); + expect(newEvent.reminders, isNotNull); + expect(newEvent.reminders?.length, equals(1)); + expect(newEvent.availability, equals(event.availability)); + expect(newEvent.status, equals(event.status)); + }); +} From 3637a5c91a8a5cbbb2c809322f5e51e444ae3e75 Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Tue, 28 Nov 2023 11:32:14 +0100 Subject: [PATCH 04/11] added event color --- device_calendar/CHANGELOG.md | 200 --- device_calendar/LICENSE | 27 - device_calendar/README.md | 121 -- device_calendar/analysis_options.yaml | 30 - device_calendar/android/.gitignore | 8 - device_calendar/android/build.gradle | 58 - device_calendar/android/gradle.properties | 3 - .../android/gradle/wrapper/gradle-wrapper.jar | Bin 54708 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - device_calendar/android/gradlew | 172 --- device_calendar/android/gradlew.bat | 84 -- device_calendar/android/proguard-rules.pro | 1 - device_calendar/android/settings.gradle | 1 - .../android/src/main/AndroidManifest.xml | 3 - .../devicecalendar/AvailabilitySerializer.kt | 18 - .../devicecalendar/CalendarDelegate.kt | 1268 ----------------- .../devicecalendar/DeviceCalendarPlugin.kt | 301 ---- .../devicecalendar/EventStatusSerializer.kt | 15 - .../devicecalendar/common/Constants.kt | 117 -- .../devicecalendar/common/ErrorCodes.kt | 11 - .../devicecalendar/common/ErrorMessages.kt | 16 - .../devicecalendar/models/Attendee.kt | 10 - .../devicecalendar/models/Availability.kt | 7 - .../devicecalendar/models/Calendar.kt | 13 - .../CalendarMethodsParametersCacheModel.kt | 16 - .../devicecalendar/models/Event.kt | 23 - .../devicecalendar/models/EventStatus.kt | 7 - .../devicecalendar/models/RecurrenceRule.kt | 17 - .../devicecalendar/models/Reminder.kt | 3 - device_calendar/device_calendar.iml | 19 - device_calendar/device_calendar_android.iml | 30 - .../example/.flutter-plugins-dependencies | 1 - device_calendar/example/.gitignore | 9 - device_calendar/example/.metadata | 8 - device_calendar/example/README.md | 195 --- device_calendar/example/analysis_options.yaml | 30 - device_calendar/example/android/.gitignore | 10 - .../example/android/app/build.gradle | 60 - .../example/android/app/proguard-rules.pro | 1 - .../android/app/src/main/AndroidManifest.xml | 44 - .../devicecalendarexample/MainActivity.kt | 6 - .../main/res/drawable/launch_background.xml | 12 - .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 544 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 442 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 721 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 1031 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 1443 -> 0 bytes .../app/src/main/res/values/styles.xml | 8 - device_calendar/example/android/build.gradle | 29 - .../example/android/gradle.properties | 3 - .../android/gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - device_calendar/example/android/gradlew | 160 --- device_calendar/example/android/gradlew.bat | 90 -- .../example/android/settings.gradle | 15 - .../example/device_calendar_example.iml | 17 - .../device_calendar_example_android.iml | 27 - .../example/integration_test/app_test.dart | 89 -- .../integration_test/integration_test.dart | 8 - .../integration_test_android.dart | 29 - .../example/integration_test/ios.sh | 24 - device_calendar/example/ios/.gitignore | 45 - .../ios/Flutter/AppFrameworkInfo.plist | 30 - .../example/ios/Flutter/Debug.xcconfig | 2 - .../example/ios/Flutter/Release.xcconfig | 2 - .../ios/Flutter/flutter_export_environment.sh | 13 - device_calendar/example/ios/Podfile | 45 - device_calendar/example/ios/Podfile.lock | 34 - .../ios/Runner.xcodeproj/project.pbxproj | 507 ------- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/xcschemes/Runner.xcscheme | 87 -- .../contents.xcworkspacedata | 10 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../example/ios/Runner/AppDelegate.swift | 13 - .../AppIcon.appiconset/Contents.json | 122 -- .../Icon-App-1024x1024@1x.png | Bin 11112 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 0 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 0 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 0 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 0 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 0 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 0 bytes .../LaunchImage.imageset/Contents.json | 23 - .../LaunchImage.imageset/LaunchImage.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 68 -> 0 bytes .../LaunchImage.imageset/README.md | 5 - .../Runner/Base.lproj/LaunchScreen.storyboard | 37 - .../ios/Runner/Base.lproj/Main.storyboard | 26 - device_calendar/example/ios/Runner/Info.plist | 55 - .../ios/Runner/Runner-Bridging-Header.h | 1 - .../example/lib/common/app_routes.dart | 3 - device_calendar/example/lib/main.dart | 29 - .../lib/presentation/date_time_picker.dart | 81 -- .../example/lib/presentation/event_item.dart | 342 ----- .../lib/presentation/input_dropdown.dart | 42 - .../lib/presentation/pages/calendar_add.dart | 164 --- .../presentation/pages/calendar_event.dart | 1261 ---------------- .../presentation/pages/calendar_events.dart | 190 --- .../lib/presentation/pages/calendars.dart | 161 --- .../presentation/pages/event_attendee.dart | 174 --- .../presentation/pages/event_reminders.dart | 102 -- .../presentation/recurring_event_dialog.dart | 101 -- device_calendar/example/pubspec.yaml | 27 - device_calendar/ios/.gitignore | 36 - device_calendar/ios/Assets/.gitkeep | 0 .../ios/Classes/DeviceCalendarPlugin.h | 4 - .../ios/Classes/DeviceCalendarPlugin.m | 8 - .../Classes/SwiftDeviceCalendarPlugin.swift | 1136 --------------- device_calendar/ios/device_calendar.podspec | 22 - device_calendar/lib/device_calendar.dart | 17 - .../lib/src/common/calendar_enums.dart | 315 ---- .../lib/src/common/channel_constants.dart | 26 - .../lib/src/common/error_codes.dart | 6 - .../lib/src/common/error_messages.dart | 27 - device_calendar/lib/src/device_calendar.dart | 443 ------ device_calendar/lib/src/models/attendee.dart | 81 -- device_calendar/lib/src/models/calendar.dart | 56 - device_calendar/lib/src/models/event.dart | 306 ---- .../android/attendance_status.dart | 15 - .../android/attendee_details.dart | 23 - .../ios/attendance_status.dart | 18 - .../ios/attendee_details.dart | 21 - device_calendar/lib/src/models/reminder.dart | 18 - device_calendar/lib/src/models/result.dart | 33 - .../src/models/retrieve_events_params.dart | 7 - device_calendar/pubspec.yaml | 29 - .../test/device_calendar_test.dart | 245 ---- 137 files changed, 10134 deletions(-) delete mode 100644 device_calendar/CHANGELOG.md delete mode 100644 device_calendar/LICENSE delete mode 100644 device_calendar/README.md delete mode 100644 device_calendar/analysis_options.yaml delete mode 100644 device_calendar/android/.gitignore delete mode 100644 device_calendar/android/build.gradle delete mode 100644 device_calendar/android/gradle.properties delete mode 100644 device_calendar/android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 device_calendar/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 device_calendar/android/gradlew delete mode 100644 device_calendar/android/gradlew.bat delete mode 100644 device_calendar/android/proguard-rules.pro delete mode 100644 device_calendar/android/settings.gradle delete mode 100644 device_calendar/android/src/main/AndroidManifest.xml delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt delete mode 100644 device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt delete mode 100644 device_calendar/device_calendar.iml delete mode 100644 device_calendar/device_calendar_android.iml delete mode 100644 device_calendar/example/.flutter-plugins-dependencies delete mode 100644 device_calendar/example/.gitignore delete mode 100644 device_calendar/example/.metadata delete mode 100644 device_calendar/example/README.md delete mode 100644 device_calendar/example/analysis_options.yaml delete mode 100644 device_calendar/example/android/.gitignore delete mode 100644 device_calendar/example/android/app/build.gradle delete mode 100644 device_calendar/example/android/app/proguard-rules.pro delete mode 100644 device_calendar/example/android/app/src/main/AndroidManifest.xml delete mode 100644 device_calendar/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt delete mode 100644 device_calendar/example/android/app/src/main/res/drawable/launch_background.xml delete mode 100644 device_calendar/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100644 device_calendar/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100644 device_calendar/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png delete mode 100644 device_calendar/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100644 device_calendar/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100644 device_calendar/example/android/app/src/main/res/values/styles.xml delete mode 100644 device_calendar/example/android/build.gradle delete mode 100644 device_calendar/example/android/gradle.properties delete mode 100644 device_calendar/example/android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 device_calendar/example/android/gradle/wrapper/gradle-wrapper.properties delete mode 100755 device_calendar/example/android/gradlew delete mode 100644 device_calendar/example/android/gradlew.bat delete mode 100644 device_calendar/example/android/settings.gradle delete mode 100644 device_calendar/example/device_calendar_example.iml delete mode 100644 device_calendar/example/device_calendar_example_android.iml delete mode 100644 device_calendar/example/integration_test/app_test.dart delete mode 100644 device_calendar/example/integration_test/integration_test.dart delete mode 100644 device_calendar/example/integration_test/integration_test_android.dart delete mode 100755 device_calendar/example/integration_test/ios.sh delete mode 100755 device_calendar/example/ios/.gitignore delete mode 100755 device_calendar/example/ios/Flutter/AppFrameworkInfo.plist delete mode 100755 device_calendar/example/ios/Flutter/Debug.xcconfig delete mode 100755 device_calendar/example/ios/Flutter/Release.xcconfig delete mode 100755 device_calendar/example/ios/Flutter/flutter_export_environment.sh delete mode 100644 device_calendar/example/ios/Podfile delete mode 100755 device_calendar/example/ios/Podfile.lock delete mode 100644 device_calendar/example/ios/Runner.xcodeproj/project.pbxproj delete mode 100755 device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100755 device_calendar/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme delete mode 100755 device_calendar/example/ios/Runner.xcworkspace/contents.xcworkspacedata delete mode 100755 device_calendar/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100755 device_calendar/example/ios/Runner/AppDelegate.swift delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png delete mode 100755 device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md delete mode 100755 device_calendar/example/ios/Runner/Base.lproj/LaunchScreen.storyboard delete mode 100755 device_calendar/example/ios/Runner/Base.lproj/Main.storyboard delete mode 100755 device_calendar/example/ios/Runner/Info.plist delete mode 100755 device_calendar/example/ios/Runner/Runner-Bridging-Header.h delete mode 100644 device_calendar/example/lib/common/app_routes.dart delete mode 100644 device_calendar/example/lib/main.dart delete mode 100644 device_calendar/example/lib/presentation/date_time_picker.dart delete mode 100644 device_calendar/example/lib/presentation/event_item.dart delete mode 100644 device_calendar/example/lib/presentation/input_dropdown.dart delete mode 100644 device_calendar/example/lib/presentation/pages/calendar_add.dart delete mode 100644 device_calendar/example/lib/presentation/pages/calendar_event.dart delete mode 100644 device_calendar/example/lib/presentation/pages/calendar_events.dart delete mode 100644 device_calendar/example/lib/presentation/pages/calendars.dart delete mode 100644 device_calendar/example/lib/presentation/pages/event_attendee.dart delete mode 100644 device_calendar/example/lib/presentation/pages/event_reminders.dart delete mode 100644 device_calendar/example/lib/presentation/recurring_event_dialog.dart delete mode 100644 device_calendar/example/pubspec.yaml delete mode 100644 device_calendar/ios/.gitignore delete mode 100644 device_calendar/ios/Assets/.gitkeep delete mode 100644 device_calendar/ios/Classes/DeviceCalendarPlugin.h delete mode 100644 device_calendar/ios/Classes/DeviceCalendarPlugin.m delete mode 100644 device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift delete mode 100644 device_calendar/ios/device_calendar.podspec delete mode 100644 device_calendar/lib/device_calendar.dart delete mode 100644 device_calendar/lib/src/common/calendar_enums.dart delete mode 100644 device_calendar/lib/src/common/channel_constants.dart delete mode 100644 device_calendar/lib/src/common/error_codes.dart delete mode 100644 device_calendar/lib/src/common/error_messages.dart delete mode 100644 device_calendar/lib/src/device_calendar.dart delete mode 100644 device_calendar/lib/src/models/attendee.dart delete mode 100644 device_calendar/lib/src/models/calendar.dart delete mode 100644 device_calendar/lib/src/models/event.dart delete mode 100644 device_calendar/lib/src/models/platform_specifics/android/attendance_status.dart delete mode 100644 device_calendar/lib/src/models/platform_specifics/android/attendee_details.dart delete mode 100644 device_calendar/lib/src/models/platform_specifics/ios/attendance_status.dart delete mode 100644 device_calendar/lib/src/models/platform_specifics/ios/attendee_details.dart delete mode 100644 device_calendar/lib/src/models/reminder.dart delete mode 100644 device_calendar/lib/src/models/result.dart delete mode 100644 device_calendar/lib/src/models/retrieve_events_params.dart delete mode 100644 device_calendar/pubspec.yaml delete mode 100644 device_calendar/test/device_calendar_test.dart diff --git a/device_calendar/CHANGELOG.md b/device_calendar/CHANGELOG.md deleted file mode 100644 index 2fa175fa..00000000 --- a/device_calendar/CHANGELOG.md +++ /dev/null @@ -1,200 +0,0 @@ -# Changelog - - - -## [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 - - *Note:* `timezone 0.9.0` [removed named database files](https://pub.dev/packages/timezone/changelog#090). If you are only using `device_calendar`, you can ignore this note. -- Added support for all-day multi-day events on iOS -- Fixed iOS issue of adding attendees to events -- Fixed Android issue of the `ownerAccount` being null - -## [4.2.0](https://github.com/builttoroam/device_calendar/releases/tag/4.2.0) - -- Fix: apks can be build correctly now -- Support for viewing and editing attendee status - - iOS needs a specific native view and permissions to edit attendees due to iOS restrictions. See README and example app. - -## [4.1.0](https://github.com/builttoroam/device_calendar/releases/tag/4.1.0) - -- Fix: title, descriptions etc are now retrieved properly. -- Fix: Event JSONs created and are now readable. Previous (mislabeled) JSONs are also readable with warnings. -- Fix: removed depreceated plugins from Example. -- Integration tests are now working. Android instructions are ready. -- Gradle plug-ins are updated. -- Compiles with jvm 1.8, should be compilable for Flutter 2.9+ -- Android: proper support for all day events, and multi-day all day events. - -## [4.0.1](https://github.com/builttoroam/device_calendar/releases/tag/4.0.1) - -- Fix: event time are now properly retrieved - -## [4.0.0](https://github.com/builttoroam/device_calendar/releases/tag/4.0.0) - -- Timezone plugin and logic implemented. All issues related to timezone shoulde be fixed. -- Events.availability defaults to busy when not specified [354](https://github.com/builttoroam/device_calendar/pull/354) -- Events parameter now includes location and url. [319](https://github.com/builttoroam/device_calendar/pull/319) -- Android: Fixed bug where platform exception appeared, when Events.availability was null on Event [241](https://github.com/builttoroam/device_calendar/issues/241) -- Fixed various issues in example [270](https://github.com/builttoroam/device_calendar/issues/270), [268](https://github.com/builttoroam/device_calendar/issues/268) -- Android: deleteEvent code aligned with flutter [258](https://github.com/builttoroam/device_calendar/issues/258) -- Android: Updated to V2 embeddding [326](https://github.com/builttoroam/device_calendar/issues/326) -- iOS: Updated swift versions, possibly improved compability with Obj-C [flutter/flutter#16049 (comment)](https://github.com/flutter/flutter/issues/16049#issuecomment-611192738) - -## [3.9.0](https://github.com/builttoroam/device_calendar/releases/tag/3.9.0) - -- Migrated to null safety -- Updated multiple underlying dependencies -- Rebuilt iOS podfile -- Upgraded to new Android plugins APIs for flutter - -## 3.1.0 25th March 2020 - Bug fixes and new features - -- Boolean variable `isDefault` added for issue [145](https://github.com/builttoroam/device_calendar/issues/145) (**NOTE**: This is not supported Android API 16 or lower, `isDefault` will always be false) -- Events with 'null' title now defaults to 'New Event', issue [126](https://github.com/builttoroam/device_calendar/issues/126) -- Updated property summaries for issues [121](https://github.com/builttoroam/device_calendar/issues/121) and [122](https://github.com/builttoroam/device_calendar/issues/122) -- Updated example documentation for issue [119](https://github.com/builttoroam/device_calendar/issues/119) -- Read-only calendars cannot be edited or deleted for the example app -- Added `DayOfWeekGroup` enum and an extension `getDays` to get corresponding dates of the enum values -- Added to retrieve colour for calendars. Thanks to [nadavfima](https://github.com/nadavfima) for the contribution and PR to add colour support for both Android and iOS -- Added compatibility with a new Flutter plugin for Android. Thanks to the PR submitted by [RohitKumarMishra](https://github.com/RohitKumarMishra) -- [Android] Fixed all day timezone issue [164](https://github.com/builttoroam/device_calendar/issues/164) -- Added support for deleting individual or multiple instances of a recurring event for issue [108](https://github.com/builttoroam/device_calendar/issues/108) -- Ability to add local calendars with a desired colour for issue [115](https://github.com/builttoroam/device_calendar/issues/115) -- Returns account name and type for each calendars for issue [179](https://github.com/builttoroam/device_calendar/issues/179) - -## 3.0.0+3 3rd February 2020 - -- Fixed all day conditional check for issue [162](https://github.com/builttoroam/device_calendar/issues/162) - -## 3.0.0+2 30th January 2020 - -- Updated `event.allDay` property in `createOrUpdateEvent` method to be null-aware - -## 3.0.0+1 28th January 2020 - -- Updated `event.url` property in `createOrUpdateEvent` method to be null-aware for issue [152](https://github.com/builttoroam/device_calendar/issues/152) - -## 3.0.0 21st January 2020 - -- **BREAKING CHANGE** Properties for the attendee model in `attendee.dart` file have been changed: - - Boolean property `isRequired` has been replaced to `AttendeeRole` enum - - New arugment added for `AttendeeRole` property -- **BREAKING CHANGE** Package updates: - - [Android] Updated Gradle plugin to 3.5.2 and Gradle wrapper to 5.4.1 - - [iOS] Updated Swift to 5 -- `name` and `isOrganiser` (read-only) properties have been added -- Attendee UI update for the example app -- Ability to add, modify or remove an attendee - -## 2.0.0 17th January 2020 - -- **BREAKING CHANGE** The recurrence models in `recurrence_rule.dart` file have been chaged -- **BREAKING CHANGE** All articles used in property names or arugments have been removed (i.e. enum `DayOfTheWeek` to `DayOfWeek`) -- Recurrence fix for monthly and yearly frequencies -- UI update for the example app -- Add support for all day events - -## 1.0.0+3 9th January 2020 - -- Flutter upgrade to 1.12.13 -- Added an URL input for calendar events for issue [132](https://github.com/builttoroam/device_calendar/issues/132) - -## 1.0.0+2 30th August 2019 - -- Fix home page URL - -## 1.0.0+1 30th August 2019 - -- Add integration tests to example app. Note that this is more for internal use at the moment as it currently requires an Android device with a calendar that can be written to and assumes that the tests are executed from a Mac. - -## 1.0.0 28th August 2019 - -- **BREAKING CHANGE** `retrieveCalendars` and `retrieveEvents` now return lists that cannot be modified (`UnmodifiableListView`) to address part of issue [113](https://github.com/builttoroam/device_calendar/issues/113) -- Support for more advanced recurrence rules -- Update README to include information about using ProGuard for issue [99](https://github.com/builttoroam/device_calendar/issues/99) -- Made event title optional to fix issue [72](https://github.com/builttoroam/device_calendar/issues/72) -- Return information about the organiser of the event as per issue [73](https://github.com/builttoroam/device_calendar/issues/73) -- Return attendance status of attendees and if they're required for an event. These are details are different across iOS and Android and so are returned within platform-specific objects -- Ability to modify attendees for an event -- Ability to create reminders for events expressed in minutes before the event starts - -## 0.2.2 19th August 2019 - -- Add support for specifying the location of an event. Thanks to [oli06](https://github.com/oli06) and [zemanux](https://github.com/zemanux) for submitting PRs to add the functionality to iOS and Android respectively - -## 0.2.1+1 5th August 2019 - -- Fixing date in changelog for version 0.2.1 - -## 0.2.1 5th August 2019 - -- [Android] Fixes issue [101](https://github.com/builttoroam/device_calendar/issues/101) where plugin results in a crash with headless execution - -## 0.2.0 30th July 2019 - -- **BREAKING CHANGE** [Android] Updated to use Gradle plugin to 3.4.2, Gradle wrapper to 5.1.1, Kotlin version to 1.3.41 and bumped Android dependencies -- Add initial support for recurring events. Note that currently editing or deleting a recurring event will affect all instances of it. Future releases will look at supporting more advanced recurrence rules -- Remove old example app to avoid confusion - -## 0.1.3 5th July 2019 - -- [iOS] Fixes issue [94](https://github.com/builttoroam/device_calendar/issues/94) that occurred on 32-bit iOS devices around date of events. Thanks to the PR submitted by [duzenko](https://github.com/duzenko) - -## 0.1.2+2 28th May 2019 - -- Non-functional release. Minor refactoring in Android code to address issues found in Codefactor and fix build status badge in README - -## 0.1.2+1 17th May 2019 - -- Non-functional release. Fixed formatting in changelog and code comments -- Added more info about potential issues in consuming the plugin within an Objective-C project - -## 0.1.2 - 16th May 2019 - -- [Android] An updated fix to address issue [79](https://github.com/builttoroam/device_calendar/issues/79), thanks to the PR submitted by [Gerry High](https://github.com/gerryhigh) - -## 0.1.1 - 1st March 2019 - -- Fixed issue [79](https://github.com/builttoroam/device_calendar/issues/79) where on Android, the plugin was indicating that it was handling permissions that it shouldn't have - -## 0.1.0 - 26th February 2019 - -- **BREAKING CHANGE** Migrated to the plugin to use AndroidX instead of the deprecated Android support libraries. Please ensure you have migrated your application following the guide [here](https://developer.android.com/jetpack/androidx/migrate) -- **BREAKING CHANGE** Updated Kotlin to version 1.3.21 -- **BREAKING CHANGE** Updated Gradle plugin to 3.3.1 and distribution to 4.10.2 - -## 0.0.8 - 26th February 2019 - -- This was a breaking change that should've been incremented as minor version update instead of a patch version update. See changelog for 0.1.0 for the details of this update - -## 0.0.7 - 16th November 2018 - -- Fixes issue [##67](https://github.com/builttoroam/device_calendar/issues/67) and [##68](https://github.com/builttoroam/device_calendar/issues/68). Thanks to PR submitted by huzhiren. - -## 0.0.6 - 18th June 2018 - -- [iOS] Fix an issue when adding/updating an event with a null description - -## 0.0.5 - 14th June 2018 - -- [Android] Fixed an issue with retrieving events by id only - -## 0.0.4 - 12th June 2018 - -- Reordering changelog -- Creating new example for the Pub Dart Example tab -- Moving existing example to the example_app GitHub folder - -## 0.0.2 - 0.0.3 - 7th June 2018 - -- Fixing incorrect Travis build links - -## 0.0.1 - 7th June 2018 - -- Ability to retrieve device calendars -- CRUD operations on calendar events diff --git a/device_calendar/LICENSE b/device_calendar/LICENSE deleted file mode 100644 index 81f1b5da..00000000 --- a/device_calendar/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2018 Built to Roam. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Built to Roam nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/device_calendar/README.md b/device_calendar/README.md deleted file mode 100644 index 2af1e99d..00000000 --- a/device_calendar/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# Device Calendar Plugin - -[![pub package](https://img.shields.io/pub/v/device_calendar.svg)](https://pub.dartlang.org/packages/device_calendar) ![Pub Version (including pre-releases)](https://img.shields.io/pub/v/device_calendar?include_prereleases&label=Prerelease) [![build](https://github.com/builttoroam/device_calendar/actions/workflows/dart.yml/badge.svg?branch=develop)](https://github.com/builttoroam/device_calendar/actions/workflows/dart.yml) - -A cross platform plugin for modifying calendars on the user's device. - -## Breaking changes at v4 - -* **If you're upgrading from previous versions, your code will need to be modified (slightly), otherwise it will not run after update. See [Timezone support](https://github.com/builttoroam/device_calendar#timezone-support-with-tzdatetime) for more details.** -* **There are some changes to event JSON formats at v4. Pay extra care if you handle event JSONs. Directly calling to and from device calendars should be unaffected.** - -## Features - -* Request permissions to modify calendars on the user's device -* Check if permissions to modify the calendars on the user's device have been granted -* Add or retrieve calendars on the user's device -* Retrieve events associated with a calendar -* Add, update or delete events from a calendar -* Set up, edit or delete recurring events - * **NOTE**: Editing a recurring event will currently edit all instances of it - * **NOTE**: Deleting multiple instances in **Android** takes time to update, you'll see the changes after a few seconds -* Add, modify or remove attendees and receive if an attendee is an organiser for an event -* Setup reminders for an event -* Specify a time zone for event start and end date - * **NOTE**: Due to a limitation of iOS API, single time zone property is used for iOS (`event.startTimeZone`) - * **NOTE**: For the time zone list, please refer to the `TZ database name` column on [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) - -## Timezone support with TZDateTime - -Due to feedback we received, starting from `4.0.0` we will be using the `timezone` package to better handle all timezone data. - -This is already included in this package. However, you need to add this line whenever the package is needed. - -```dart -import 'package:timezone/timezone.dart'; -``` - -If you don't need any timezone specific features in your app, you may use `flutter_native_timezone` to get your devices' current timezone, then convert your previous `DateTime` with it. - -```dart -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; - -initializeTimeZones(); - -// As an example, our default timezone is UTC. -Location _currentLocation = getLocation('Etc/UTC'); - -Future setCurentLocation() async { - String timezone = 'Etc/UTC'; - try { - timezone = await FlutterNativeTimezone.getLocalTimezone(); - } catch (e) { - print('Could not get the local timezone'); - } - _currentLocation = getLocation(timezone); - setLocalLocation(_currentLocation); -} - -... - -event.start = TZDateTime.from(oldDateTime, _currentLocation); -``` - -For other use cases, feedback or future developments on the feature, feel free to open a discussion on GitHub. - -## Null-safety migration - -From `v3.9.0`, device_calendar is null-safe. However, not all workflows have been checked and bugs from older versions still persist. - -You are strongly advised to test your workflow with the new package before shipping. -Better yet, please leave a note for what works and what doesn't, or contribute some bug fixes! - -## Android Integration - -The following will need to be added to the `AndroidManifest.xml` file for your application to indicate permissions to modify calendars are needed - -```xml - - -``` - -### 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()`. - -You may add the following setting to the ProGuard rules file `proguard-rules.pro` (thanks to [Britannio Jarrett](https://github.com/britannio)). Read more about the issue [here](https://github.com/builttoroam/device_calendar/issues/99) - -``` --keep class com.builttoroam.devicecalendar.** { *; } -``` - -See [here](https://github.com/builttoroam/device_calendar/issues/99#issuecomment-612449677) for an example setup. - -For more information, refer to the guide at [Android Developer](https://developer.android.com/studio/build/shrink-code#keep-code) - -### AndroidX migration - -Since `v.1.0`, this version has migrated to use AndroidX instead of the deprecated Android support libraries. When using `0.10.0` and onwards for this plugin, please ensure your application has been migrated following the guide [here](https://developer.android.com/jetpack/androidx/migrate) - -## iOS Integration - -For iOS 10+ support, you'll need to modify the `Info.plist` to add the following key/value pair - -```xml -NSCalendarsUsageDescription -Access most functions for calendar viewing and editing. - -NSContactsUsageDescription -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/device_calendar/analysis_options.yaml b/device_calendar/analysis_options.yaml deleted file mode 100644 index 68a79339..00000000 --- a/device_calendar/analysis_options.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# 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/device_calendar/android/.gitignore b/device_calendar/android/.gitignore deleted file mode 100644 index c6cbe562..00000000 --- a/device_calendar/android/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures diff --git a/device_calendar/android/build.gradle b/device_calendar/android/build.gradle deleted file mode 100644 index 1f5ff500..00000000 --- a/device_calendar/android/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -group 'com.builttoroam.devicecalendar' -version '1.0-SNAPSHOT' - -buildscript { - ext.kotlin_version = '1.6.0' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -rootProject.allprojects { - repositories { - google() - mavenCentral() - } -} - -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion 33 - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - defaultConfig { - minSdkVersion 19 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'proguard-rules.pro' - } - lintOptions { - disable 'InvalidPackage' - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - namespace 'com.builttoroam.devicecalendar' -} - -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.12.2' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' -} diff --git a/device_calendar/android/gradle.properties b/device_calendar/android/gradle.properties deleted file mode 100644 index 4d3226ab..00000000 --- a/device_calendar/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file diff --git a/device_calendar/android/gradle/wrapper/gradle-wrapper.jar b/device_calendar/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7a3265ee94c0ab25cf079ac8ccdf87f41d455d42..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54708 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

_x$+l{{cH8$W#CT diff --git a/device_calendar/android/gradle/wrapper/gradle-wrapper.properties b/device_calendar/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 3c9d0852..00000000 --- a/device_calendar/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/device_calendar/android/gradlew b/device_calendar/android/gradlew deleted file mode 100644 index cccdd3d5..00000000 --- a/device_calendar/android/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/device_calendar/android/gradlew.bat b/device_calendar/android/gradlew.bat deleted file mode 100644 index f9553162..00000000 --- a/device_calendar/android/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/device_calendar/android/proguard-rules.pro b/device_calendar/android/proguard-rules.pro deleted file mode 100644 index d7668e11..00000000 --- a/device_calendar/android/proguard-rules.pro +++ /dev/null @@ -1 +0,0 @@ --keep class com.builttoroam.devicecalendar.** { *; } diff --git a/device_calendar/android/settings.gradle b/device_calendar/android/settings.gradle deleted file mode 100644 index ef870028..00000000 --- a/device_calendar/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'device_calendar' diff --git a/device_calendar/android/src/main/AndroidManifest.xml b/device_calendar/android/src/main/AndroidManifest.xml deleted file mode 100644 index 1479c8d6..00000000 --- a/device_calendar/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt deleted file mode 100644 index 5a803a6b..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.builttoroam.devicecalendar - -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) { - return JsonPrimitive(src.name) - } - return JsonObject() - } -} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt deleted file mode 100644 index 491370e7..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ /dev/null @@ -1,1268 +0,0 @@ -package com.builttoroam.devicecalendar - -import android.Manifest -import android.annotation.SuppressLint -import android.content.ContentResolver -import android.content.ContentUris -import android.content.ContentValues -import android.content.Context -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.ErrorMessages -import com.builttoroam.devicecalendar.models.* -import com.builttoroam.devicecalendar.models.Calendar -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -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.RecurrenceRule.WeekdayNum -import java.util.* -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()) - - init { - val gsonBuilder = GsonBuilder() - 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 - - if (!_cachedParametersMap.containsKey(requestCode)) { - // this plugin doesn't handle this request code - return false - } - - 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 - - try { - if (!permissionGranted) { - finishWithError( - EC.NOT_AUTHORIZED, - EM.NOT_AUTHORIZED_MESSAGE, - cachedValues.pendingChannelResult - ) - return false - } - - when (cachedValues.calendarDelegateMethodCode) { - RETRIEVE_CALENDARS_REQUEST_CODE -> { - retrieveCalendars(cachedValues.pendingChannelResult) - } - RETRIEVE_EVENTS_REQUEST_CODE -> { - 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 - ) - } - DELETE_EVENT_REQUEST_CODE -> { - deleteEvent( - cachedValues.calendarId, - cachedValues.eventId, - cachedValues.pendingChannelResult - ) - } - REQUEST_PERMISSIONS_REQUEST_CODE -> { - finishWithSuccess(permissionGranted, cachedValues.pendingChannelResult) - } - DELETE_CALENDAR_REQUEST_CODE -> { - deleteCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) - } - } - - return true - } finally { - _cachedParametersMap.remove(cachedValues.calendarDelegateMethodCode) - } - } - - fun requestPermissions(pendingChannelResult: MethodChannel.Result) { - if (arePermissionsGranted()) { - finishWithSuccess(true, pendingChannelResult) - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - REQUEST_PERMISSIONS_REQUEST_CODE - ) - requestPermissions(parameters) - } - } - - fun hasPermissions(pendingChannelResult: MethodChannel.Result) { - finishWithSuccess(arePermissionsGranted(), pendingChannelResult) - } - - @SuppressLint("MissingPermission") - fun retrieveCalendars(pendingChannelResult: MethodChannel.Result) { - if (arePermissionsGranted()) { - val contentResolver: ContentResolver? = _context?.contentResolver - val uri: Uri = CalendarContract.Calendars.CONTENT_URI - val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query(uri, Cst.CALENDAR_PROJECTION, null, null, null) - } else { - contentResolver?.query(uri, Cst.CALENDAR_PROJECTION_OLDER_API, null, null, null) - } - val calendars: MutableList = mutableListOf() - try { - while (cursor?.moveToNext() == true) { - val calendar = parseCalendarRow(cursor) ?: continue - calendars.add(calendar) - } - - finishWithSuccess(_gson?.toJson(calendars), pendingChannelResult) - } catch (e: Exception) { - finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) - } finally { - cursor?.close() - } - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - RETRIEVE_CALENDARS_REQUEST_CODE - ) - requestPermissions(parameters) - } - } - - 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( - EC.INVALID_ARGUMENT, - EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, - pendingChannelResult - ) - } - return null - } - - val contentResolver: ContentResolver? = _context?.contentResolver - val uri: Uri = CalendarContract.Calendars.CONTENT_URI - - val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query( - ContentUris.withAppendedId(uri, calendarIdNumber), - Cst.CALENDAR_PROJECTION, - null, - null, - null - ) - } else { - contentResolver?.query( - ContentUris.withAppendedId(uri, calendarIdNumber), - Cst.CALENDAR_PROJECTION_OLDER_API, - null, - null, - null - ) - } - - try { - if (cursor?.moveToFirst() == true) { - val calendar = parseCalendarRow(cursor) - if (isInternalCall) { - return calendar - } else { - finishWithSuccess(_gson?.toJson(calendar), pendingChannelResult) - } - } else { - if (!isInternalCall) { - finishWithError( - EC.NOT_FOUND, - "The calendar with the ID $calendarId could not be found", - pendingChannelResult - ) - } - } - } catch (e: Exception) { - finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) - } finally { - cursor?.close() - } - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - RETRIEVE_CALENDAR_REQUEST_CODE, - calendarId - ) - requestPermissions(parameters) - } - - return null - } - - fun deleteCalendar( - calendarId: String, - pendingChannelResult: MethodChannel.Result, - isInternalCall: Boolean = false - ): Calendar? { - if (isInternalCall || arePermissionsGranted()) { - val calendarIdNumber = calendarId.toLongOrNull() - if (calendarIdNumber == null) { - if (!isInternalCall) { - 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 deleteSucceeded = contentResolver?.delete(calenderUriWithId, null, null) ?: 0 - finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) - } else { - if (!isInternalCall) { - 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 - ) - requestPermissions(parameters) - } - - return null - } - - 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() - 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.OWNER_ACCOUNT, localAccountName) - 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 - val calendarId = java.lang.Long.parseLong(result?.lastPathSegment!!) - - finishWithSuccess(calendarId.toString(), pendingChannelResult) - } - - fun retrieveEvents( - calendarId: String, - startDate: Long?, - endDate: Long?, - eventIds: List, - pendingChannelResult: MethodChannel.Result - ) { - if (startDate == null && endDate == null && eventIds.isEmpty()) { - 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( - EC.NOT_FOUND, - "Couldn't retrieve the Calendar with ID $calendarId", - pendingChannelResult - ) - return - } - - val contentResolver: ContentResolver? = _context?.contentResolver - val eventsUriBuilder = CalendarContract.Instances.CONTENT_URI.buildUpon() - ContentUris.appendId(eventsUriBuilder, startDate ?: Date(0).time) - ContentUris.appendId(eventsUriBuilder, endDate ?: Date(Long.MAX_VALUE).time) - - val eventsUri = eventsUriBuilder.build() - val eventsCalendarQuery = "(${Events.CALENDAR_ID} = $calendarId)" - val eventsNotDeletedQuery = "(${Events.DELETED} != 1)" - val eventsIdsQuery = - "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" - - var eventsSelectionQuery = "$eventsCalendarQuery AND $eventsNotDeletedQuery" - if (eventIds.isNotEmpty()) { - eventsSelectionQuery += " AND ($eventsIdsQuery)" - } - val eventsSortOrder = Events.DTSTART + " DESC" - - val eventsCursor = contentResolver?.query( - eventsUri, - Cst.EVENT_PROJECTION, - eventsSelectionQuery, - null, - eventsSortOrder - ) - - val events: MutableList = mutableListOf() - - val exceptionHandler = CoroutineExceptionHandler { _, exception -> - uiThreadHandler.post { - finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) - } - } - - GlobalScope.launch(Dispatchers.IO + exceptionHandler) { - while (eventsCursor?.moveToNext() == true) { - val event = parseEvent(calendarId, eventsCursor) ?: continue - events.add(event) - } - for (event in events) { - val attendees = retrieveAttendees(calendar, event.eventId!!, contentResolver) - event.organizer = - attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } - event.attendees = attendees - event.reminders = retrieveReminders(event.eventId!!, contentResolver) - } - }.invokeOnCompletion { cause -> - eventsCursor?.close() - if (cause == null) { - uiThreadHandler.post { - finishWithSuccess(_gson?.toJson(events), pendingChannelResult) - } - } - } - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - RETRIEVE_EVENTS_REQUEST_CODE, - calendarId, - startDate, - endDate - ) - requestPermissions(parameters) - } - - return - } - - fun createOrUpdateEvent( - calendarId: String, - event: Event?, - pendingChannelResult: MethodChannel.Result - ) { - if (arePermissionsGranted()) { - if (event == null) { - finishWithError( - EC.GENERIC_ERROR, - EM.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, - pendingChannelResult - ) - return - } - - val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) - if (calendar == null) { - finishWithError( - EC.NOT_FOUND, - "Couldn't retrieve the Calendar with ID $calendarId", - pendingChannelResult - ) - return - } - - val contentResolver: ContentResolver? = _context?.contentResolver - val values = buildEventContentValues(event, calendarId) - - val exceptionHandler = CoroutineExceptionHandler { _, exception -> - uiThreadHandler.post { - finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) - } - } - - val job: Job - var eventId: Long? = event.eventId?.toLongOrNull() - if (eventId == null) { - val uri = contentResolver?.insert(Events.CONTENT_URI, values) - // get the event ID that is the last element in the Uri - eventId = java.lang.Long.parseLong(uri?.lastPathSegment!!) - job = GlobalScope.launch(Dispatchers.IO + exceptionHandler) { - insertAttendees(event.attendees, eventId, contentResolver) - insertReminders(event.reminders, eventId, contentResolver) - } - } 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 - for (attendeeToDelete in attendeesToDelete) { - deleteAttendee(eventId, attendeeToDelete, contentResolver) - } - - val attendeesToInsert = - event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } - insertAttendees(attendeesToInsert, eventId, contentResolver) - deleteExistingReminders(contentResolver, eventId) - insertReminders(event.reminders, eventId, contentResolver!!) - - val existingSelfAttendee = existingAttendees.firstOrNull { - it.emailAddress == calendar.ownerAccount - } - val newSelfAttendee = event.attendees.firstOrNull { - it.emailAddress == calendar.ownerAccount - } - if (existingSelfAttendee != null && newSelfAttendee != null && - newSelfAttendee.attendanceStatus != null && - existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus - ) { - updateAttendeeStatus(eventId, newSelfAttendee, contentResolver) - } - } - } - job.invokeOnCompletion { cause -> - if (cause == null) { - uiThreadHandler.post { - finishWithSuccess(eventId.toString(), pendingChannelResult) - } - } - } - } else { - 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 - ) - ) - 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) - } - if (reminderUri != null) { - contentResolver?.delete(reminderUri, null, null) - } - } - cursor?.close() - } - - @SuppressLint("MissingPermission") - private fun insertReminders( - reminders: List, - eventId: Long?, - contentResolver: ContentResolver - ) { - if (reminders.isEmpty()) { - return - } - val remindersContentValues = reminders.map { - ContentValues().apply { - put(CalendarContract.Reminders.EVENT_ID, eventId) - put(CalendarContract.Reminders.MINUTES, it.minutes) - put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) - } - }.toTypedArray() - contentResolver.bulkInsert(CalendarContract.Reminders.CONTENT_URI, remindersContentValues) - } - - private fun buildEventContentValues(event: Event, calendarId: String): ContentValues { - val values = ContentValues() - - 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.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.AVAILABILITY, getAvailability(event.availability)) - 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 - } - - private fun getTimeZone(timeZoneString: String?): TimeZone { - val deviceTimeZone: TimeZone = java.util.Calendar.getInstance().timeZone - var timeZone = TimeZone.getTimeZone(timeZoneString ?: deviceTimeZone.id) - - // Invalid time zone names defaults to GMT so update that to be device's time zone - if (timeZone.id == "GMT" && timeZoneString != "GMT") { - timeZone = TimeZone.getTimeZone(deviceTimeZone.id) - } - - return timeZone - } - - private fun getAvailability(availability: Availability?): Int? = when (availability) { - Availability.BUSY -> Events.AVAILABILITY_BUSY - Availability.FREE -> Events.AVAILABILITY_FREE - Availability.TENTATIVE -> Events.AVAILABILITY_TENTATIVE - else -> null - } - - private fun getEventStatus(eventStatus: EventStatus?): Int? = when (eventStatus) { - EventStatus.CONFIRMED -> Events.STATUS_CONFIRMED - EventStatus.TENTATIVE -> Events.STATUS_TENTATIVE - EventStatus.CANCELED -> Events.STATUS_CANCELED - else -> null - } - - @SuppressLint("MissingPermission") - private fun insertAttendees( - attendees: List, - eventId: Long?, - contentResolver: ContentResolver? - ) { - if (attendees.isEmpty()) { - return - } - - val attendeesValues = attendees.map { - ContentValues().apply { - put(CalendarContract.Attendees.ATTENDEE_NAME, it.name) - put(CalendarContract.Attendees.ATTENDEE_EMAIL, it.emailAddress) - put( - CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, - CalendarContract.Attendees.RELATIONSHIP_ATTENDEE - ) - put(CalendarContract.Attendees.ATTENDEE_TYPE, it.role) - put( - CalendarContract.Attendees.ATTENDEE_STATUS, - it.attendanceStatus - ) - put(CalendarContract.Attendees.EVENT_ID, eventId) - } - }.toTypedArray() - - contentResolver?.bulkInsert(CalendarContract.Attendees.CONTENT_URI, attendeesValues) - } - - @SuppressLint("MissingPermission") - 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 + " = ?)" - 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 - ) - } - - 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( - EC.NOT_FOUND, - "The calendar with the ID $calendarId could not be found", - pendingChannelResult - ) - return - } - - if (existingCal.isReadOnly) { - finishWithError( - EC.NOT_ALLOWED, - "Calendar with ID $calendarId is read-only", - pendingChannelResult - ) - return - } - - val eventIdNumber = eventId.toLongOrNull() - if (eventIdNumber == null) { - finishWithError( - EC.INVALID_ARGUMENT, - EM.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, - pendingChannelResult - ) - return - } - - val contentResolver: ContentResolver? = _context?.contentResolver - if (startDate == null && endDate == null && followingInstances == null) { // Delete all instances - val eventsUriWithId = ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) - val deleteSucceeded = contentResolver?.delete(eventsUriWithId, null, null) ?: 0 - finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) - } else { - if (!followingInstances!!) { // Only this instance - val exceptionUriWithId = - ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) - val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query( - contentResolver, - Cst.EVENT_INSTANCE_DELETION, - startDate!!, - endDate!! - ) - - while (instanceCursor.moveToNext()) { - val foundEventID = - instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) - - if (eventIdNumber == foundEventID) { - values.put( - Events.ORIGINAL_INSTANCE_TIME, - instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_BEGIN_INDEX) - ) - values.put(Events.STATUS, Events.STATUS_CANCELED) - } - } - - val deleteSucceeded = contentResolver?.insert(exceptionUriWithId, values) - instanceCursor.close() - finishWithSuccess(deleteSucceeded != null, pendingChannelResult) - } else { // This and following instances - val eventsUriWithId = - ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) - val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query( - contentResolver, - Cst.EVENT_INSTANCE_DELETION, - startDate!!, - endDate!! - ) - - while (instanceCursor.moveToNext()) { - val foundEventID = - instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) - - if (eventIdNumber == foundEventID) { - 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, - Cst.EVENT_INSTANCE_DELETION, - startDate, - lastDate - ) - while (cursor.moveToNext()) { - 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, - Cst.EVENT_INSTANCE_DELETION, - startDate - DateUtils.YEAR_IN_MILLIS, - startDate - 1 - ) - var lastRecurrenceDate: Long? = null - - while (cursor.moveToNext()) { - if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { - lastRecurrenceDate = - cursor.getLong(Cst.EVENT_INSTANCE_DELETION_END_INDEX) - } - } - - if (lastRecurrenceDate != null) { - newRule.until = DateTime(lastRecurrenceDate) - } else { - newRule.until = DateTime(startDate - 1) - } - cursor.close() - } - - values.put(Events.RRULE, newRule.toString()) - contentResolver?.update(eventsUriWithId, values, null, null) - finishWithSuccess(true, pendingChannelResult) - } - } - instanceCursor.close() - } - } - } else { - val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult, - DELETE_EVENT_REQUEST_CODE, - calendarId - ) - parameters.eventId = eventId - requestPermissions(parameters) - } - } - - private fun arePermissionsGranted(): Boolean { - if (atLeastAPI(23) && _binding != null) { - val writeCalendarPermissionGranted = _binding!!.activity.checkSelfPermission(Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED - val readCalendarPermissionGranted = _binding!!.activity.checkSelfPermission(Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED - return writeCalendarPermissionGranted && readCalendarPermissionGranted - } - - return true - } - - private fun requestPermissions(parameters: CalendarMethodsParametersCacheModel) { - val requestCode: Int = generateUniqueRequestCodeAndCacheParameters(parameters) - requestPermissions(requestCode) - } - - private fun requestPermissions(requestCode: Int) { - if (atLeastAPI(23)) { - _binding!!.activity.requestPermissions( - arrayOf( - Manifest.permission.WRITE_CALENDAR, - Manifest.permission.READ_CALENDAR - ), requestCode - ) - } - } - - private fun parseCalendarRow(cursor: Cursor?): Calendar? { - if (cursor == null) { - return null - } - - 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(), - displayName, - calendarColor, - accountName, - accountType, - ownerAccount - ) - - calendar.isReadOnly = isCalendarReadOnly(accessLevel) - if (atLeastAPI(17)) { - val isPrimary = cursor.getString(Cst.CALENDAR_PROJECTION_IS_PRIMARY_INDEX) - calendar.isDefault = isPrimary == "1" - } else { - calendar.isDefault = false - } - return calendar - } - - private fun parseEvent(calendarId: String, cursor: Cursor?): Event? { - if (cursor == null) { - return null - } - 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 eventColor = cursor.getLong(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) - - val event = Event() - event.eventTitle = title ?: "New Event" - event.eventId = eventId.toString() - event.calendarId = calendarId - event.eventDescription = description - event.eventStartDate = begin - event.eventEndDate = end - event.eventAllDay = allDay - event.eventLocation = location - event.eventURL = url - event.recurrenceRule = parseRecurrenceRuleString(recurringRule) - event.eventStartTimeZone = startTimeZone - event.eventEndTimeZone = endTimeZone - event.availability = availability - event.eventStatus = eventStatus - event.eventColor = eventColor - - return event - } - - private fun parseRecurrenceRuleString(recurrenceRuleString: String?): RecurrenceRule? { - if (recurrenceRuleString == null) { - return null - } - val rfcRecurrenceRule = Rrule(recurrenceRuleString) - val frequency = when (rfcRecurrenceRule.freq) { - 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) - - recurrenceRule.count = rfcRecurrenceRule.count - recurrenceRule.interval = rfcRecurrenceRule.interval - - val until = rfcRecurrenceRule.until - if (until != null) { - recurrenceRule.until = formatDateTime(dateTime = until) - } - - 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) - } - - recurrenceRule.bysetpos = rfcRecurrenceRule.getByPart(Rrule.Part.BYSETPOS) - - return recurrenceRule - } - - private fun formatDateTime(dateTime: DateTime): String { - assert(dateTime.year in 0..9999) - - fun twoDigits(n: Int): String { - return if (n < 10) "0$n" else "$n" - } - - 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" - } - - 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? { - if (cursor == null) { - return null - } - - val emailAddress = cursor.getString(Cst.ATTENDEE_EMAIL_INDEX) - - return Attendee( - emailAddress, - 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 - ) - } - - private fun parseReminderRow(cursor: Cursor?): Reminder? { - if (cursor == null) { - return null - } - - return Reminder(cursor.getInt(Cst.REMINDER_MINUTES_INDEX)) - } - - private fun isCalendarReadOnly(accessLevel: Int): Boolean { - return when (accessLevel) { - Events.CAL_ACCESS_CONTRIBUTOR, - Events.CAL_ACCESS_ROOT, - Events.CAL_ACCESS_OWNER, - Events.CAL_ACCESS_EDITOR - -> false - else -> true - } - } - - @SuppressLint("MissingPermission") - 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, - Cst.ATTENDEE_PROJECTION, - attendeesQuery, - null, - null - ) - attendeesCursor.use { cursor -> - if (cursor?.moveToFirst() == true) { - do { - val attendee = parseAttendeeRow(calendar, attendeesCursor) ?: continue - attendees.add(attendee) - } while (cursor.moveToNext()) - } - } - - return attendees - } - - @SuppressLint("MissingPermission") - 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, - Cst.REMINDER_PROJECTION, - remindersQuery, - null, - null - ) - remindersCursor.use { cursor -> - if (cursor?.moveToFirst() == true) { - do { - val reminder = parseReminderRow(remindersCursor) ?: continue - reminders.add(reminder) - } while (cursor.moveToNext()) - } - } - - return reminders - } - - @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 - val uniqueRequestCode: Int = (_cachedParametersMap.keys.maxOrNull() ?: 0) + 1 - parameters.ownCacheKey = uniqueRequestCode - _cachedParametersMap[uniqueRequestCode] = parameters - - return uniqueRequestCode - } - - private fun finishWithSuccess(result: T, pendingChannelResult: MethodChannel.Result) { - pendingChannelResult.success(result) - clearCachedParameters(pendingChannelResult) - } - - 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() - for (cachedParameter in cachedParameters) { - if (_cachedParametersMap.containsKey(cachedParameter.ownCacheKey)) { - _cachedParametersMap.remove(cachedParameter.ownCacheKey) - } - } - } - - private fun atLeastAPI(api: Int): Boolean { - return api <= Build.VERSION.SDK_INT - } - - 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.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.wkst != null) { - rr.weekStart = Weekday.valueOf(recurrenceRule.wkst!!) - } - - if (recurrenceRule.byday != null) { - rr.byDayPart = recurrenceRule.byday?.mapNotNull { - WeekdayNum.valueOf(it) - }?.toMutableList() - } - - if (recurrenceRule.bymonthday != null) { - rr.setByPart(Rrule.Part.BYMONTHDAY, recurrenceRule.bymonthday!!) - } - - if (recurrenceRule.byyearday != null) { - rr.setByPart(Rrule.Part.BYYEARDAY, recurrenceRule.byyearday!!) - } - - 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) - } - rr.setByPart(Rrule.Part.BYMONTH, newMonth) - } - - if (recurrenceRule.bysetpos != null) { - rr.setByPart(Rrule.Part.BYSETPOS, recurrenceRule.bysetpos!!) - } - return rr.toString() - } - - 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) { - Events.AVAILABILITY_BUSY -> Availability.BUSY - Events.AVAILABILITY_FREE -> Availability.FREE - Events.AVAILABILITY_TENTATIVE -> Availability.TENTATIVE - else -> null - } - - private fun parseEventStatus(status: Int): EventStatus? = when(status) { - Events.STATUS_CONFIRMED -> EventStatus.CONFIRMED - Events.STATUS_CANCELED -> EventStatus.CANCELED - Events.STATUS_TENTATIVE -> EventStatus.TENTATIVE - else -> null - } -} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt deleted file mode 100644 index c1f14533..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ /dev/null @@ -1,301 +0,0 @@ -package com.builttoroam.devicecalendar - -import android.app.Activity -import android.content.Context -import androidx.annotation.NonNull -import com.builttoroam.devicecalendar.common.Constants -import com.builttoroam.devicecalendar.models.* -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -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 org.dmfs.rfc5545.recur.Freq - -const val CHANNEL_NAME = "plugins.builttoroam.com/device_calendar" - -// 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 - /// - /// This local reference serves to register the plugin with the Flutter Engine and unregister it - /// when the Flutter Engine is detached from the Activity - private lateinit var channel: MethodChannel - private var context: Context? = null - private var activity: Activity? = null - - private lateinit var _calendarDelegate: CalendarDelegate - - override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) - channel.setMethodCallHandler(this) - _calendarDelegate = CalendarDelegate(null, context!!) - } - - override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activity = binding.activity - _calendarDelegate = CalendarDelegate(binding, context!!) - binding.addRequestPermissionsResultListener(_calendarDelegate) - } - - override fun onDetachedFromActivityForConfigChanges() { - activity = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activity = binding.activity - _calendarDelegate = CalendarDelegate(binding, context!!) - binding.addRequestPermissionsResultListener(_calendarDelegate) - } - - override fun onDetachedFromActivity() { - activity = null - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - when (call.method) { - REQUEST_PERMISSIONS_METHOD -> { - _calendarDelegate.requestPermissions(result) - } - HAS_PERMISSIONS_METHOD -> { - _calendarDelegate.hasPermissions(result) - } - RETRIEVE_CALENDARS_METHOD -> { - _calendarDelegate.retrieveCalendars(result) - } - RETRIEVE_EVENTS_METHOD -> { - val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - 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 -> { - val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - val eventId = call.argument(EVENT_ID_ARGUMENT) - - _calendarDelegate.deleteEvent(calendarId!!, eventId!!, result) - } - DELETE_EVENT_INSTANCE_METHOD -> { - val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - val eventId = call.argument(EVENT_ID_ARGUMENT) - val startDate = call.argument(EVENT_START_DATE_ARGUMENT) - val endDate = call.argument(EVENT_END_DATE_ARGUMENT) - val followingInstances = call.argument(FOLLOWING_INSTANCES) - - _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 - ) - } - DELETE_CALENDAR_METHOD -> { - val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - _calendarDelegate.deleteCalendar(calendarId!!, result) - } - else -> { - result.notImplemented() - } - } - } - - private fun parseEventArgs(call: MethodCall, calendarId: String?): Event { - val event = Event() - event.eventTitle = call.argument(EVENT_TITLE_ARGUMENT) - event.calendarId = calendarId - event.eventId = call.argument(EVENT_ID_ARGUMENT) - event.eventDescription = call.argument(EVENT_DESCRIPTION_ARGUMENT) - event.eventAllDay = call.argument(EVENT_ALL_DAY_ARGUMENT) ?: false - event.eventStartDate = call.argument(EVENT_START_DATE_ARGUMENT)!! - event.eventEndDate = call.argument(EVENT_END_DATE_ARGUMENT)!! - event.eventStartTimeZone = call.argument(EVENT_START_TIMEZONE_ARGUMENT) - event.eventEndTimeZone = call.argument(EVENT_END_TIMEZONE_ARGUMENT) - event.eventLocation = call.argument(EVENT_LOCATION_ARGUMENT) - event.eventURL = call.argument(EVENT_URL_ARGUMENT) - 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 - ) { - val recurrenceRule = parseRecurrenceRuleArgs(call) - event.recurrenceRule = recurrenceRule - } - - 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( - attendeeArgs[EMAIL_ADDRESS_ARGUMENT] as String, - attendeeArgs[NAME_ARGUMENT] as String?, - attendeeArgs[ROLE_ARGUMENT] as Int, - attendeeArgs[ATTENDANCE_STATUS_ARGUMENT] as Int?, - null, 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 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(UNTIL_ARGUMENT)) { - recurrenceRule.until = recurrenceRuleArgs[UNTIL_ARGUMENT] as String? - } - - if (recurrenceRuleArgs.containsKey(BY_WEEK_DAYS_ARGUMENT)) { - recurrenceRule.byday = - recurrenceRuleArgs[BY_WEEK_DAYS_ARGUMENT].toListOf()?.toMutableList() - } - - if (recurrenceRuleArgs.containsKey(BY_MONTH_DAYS_ARGUMENT)) { - recurrenceRule.bymonthday = - recurrenceRuleArgs[BY_MONTH_DAYS_ARGUMENT] as MutableList? - } - - if (recurrenceRuleArgs.containsKey(BY_YEAR_DAYS_ARGUMENT)) { - recurrenceRule.byyearday = - recurrenceRuleArgs[BY_YEAR_DAYS_ARGUMENT] as MutableList? - } - - 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 - } - - private inline fun Any?.toListOf(): List? { - return (this as List<*>?)?.filterIsInstance()?.toList() - } - - private fun parseAvailability(value: String?): Availability? = - if (value == null || value == Constants.AVAILABILITY_UNAVAILABLE) { - null - } else { - Availability.valueOf(value) - } - - private fun parseEventStatus(value: String?): EventStatus? = - if (value == null || value == Constants.EVENT_STATUS_NONE) { - null - } else { - EventStatus.valueOf(value) - } -} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt deleted file mode 100644 index 4883b7a7..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.builttoroam.devicecalendar - -import com.builttoroam.devicecalendar.models.EventStatus -import com.google.gson.* -import java.lang.reflect.Type - -class EventStatusSerializer: JsonSerializer { - override fun serialize(src: EventStatus?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { - return JsonPrimitive(src.name) - } - return JsonObject() - } - -} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt deleted file mode 100644 index 052c7278..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.builttoroam.devicecalendar.common - -import android.provider.CalendarContract - -class Constants { - companion object { - const val CALENDAR_PROJECTION_ID_INDEX: Int = 0 - const val CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX: Int = 1 - const val CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX: Int = 2 - const val CALENDAR_PROJECTION_DISPLAY_NAME_INDEX: Int = 3 - const val CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX: Int = 4 - const val CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX: Int = 5 - const val CALENDAR_PROJECTION_COLOR_INDEX: Int = 6 - const val CALENDAR_PROJECTION_IS_PRIMARY_INDEX: Int = 7 - - // 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 - - ) - - // 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 - ) - - const val EVENT_PROJECTION_ID_INDEX: Int = 0 - const val EVENT_PROJECTION_TITLE_INDEX: Int = 1 - const val EVENT_PROJECTION_DESCRIPTION_INDEX: Int = 2 - const val EVENT_PROJECTION_BEGIN_INDEX: Int = 3 - const val EVENT_PROJECTION_END_INDEX: Int = 4 - const val EVENT_PROJECTION_RECURRING_RULE_INDEX: Int = 7 - const val EVENT_PROJECTION_ALL_DAY_INDEX: Int = 8 - const val EVENT_PROJECTION_EVENT_LOCATION_INDEX: Int = 9 - const val EVENT_PROJECTION_CUSTOM_APP_URI_INDEX: Int = 10 - const val EVENT_PROJECTION_START_TIMEZONE_INDEX: Int = 11 - const val EVENT_PROJECTION_END_TIMEZONE_INDEX: Int = 12 - 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, - CalendarContract.Events.TITLE, - CalendarContract.Events.DESCRIPTION, - CalendarContract.Instances.BEGIN, - CalendarContract.Instances.END, - CalendarContract.Instances.DURATION, - CalendarContract.Events.RDATE, - CalendarContract.Events.RRULE, - CalendarContract.Events.ALL_DAY, - CalendarContract.Events.EVENT_LOCATION, - CalendarContract.Events.CUSTOM_APP_URI, - CalendarContract.Events.EVENT_TIMEZONE, - CalendarContract.Events.EVENT_END_TIMEZONE, - CalendarContract.Events.AVAILABILITY, - CalendarContract.Events.STATUS, - CalendarContract.Events.EVENT_COLOR, - ) - - const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 - const val EVENT_INSTANCE_DELETION_RRULE_INDEX: Int = 1 - const val EVENT_INSTANCE_DELETION_LAST_DATE_INDEX: Int = 2 - const val EVENT_INSTANCE_DELETION_BEGIN_INDEX: Int = 3 - 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 - ) - - const val ATTENDEE_ID_INDEX: Int = 0 - const val ATTENDEE_EVENT_ID_INDEX: Int = 1 - const val ATTENDEE_NAME_INDEX: Int = 2 - const val ATTENDEE_EMAIL_INDEX: Int = 3 - const val ATTENDEE_TYPE_INDEX: Int = 4 - const val ATTENDEE_RELATIONSHIP_INDEX: Int = 5 - 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 - ) - - const val REMINDER_MINUTES_INDEX = 1 - val REMINDER_PROJECTION: Array = arrayOf( - CalendarContract.Reminders.EVENT_ID, - CalendarContract.Reminders.MINUTES - ) - - const val AVAILABILITY_UNAVAILABLE = "UNAVAILABLE" - - const val EVENT_STATUS_NONE = "NONE" - } -} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt deleted file mode 100644 index 3509ad11..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.builttoroam.devicecalendar.common - -class ErrorCodes { - companion object { - const val INVALID_ARGUMENT: String = "400" - const val NOT_FOUND: String = "404" - const val NOT_ALLOWED: String = "405" - const val NOT_AUTHORIZED: String = "401" - const val GENERIC_ERROR: String = "500" - } -} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt deleted file mode 100644 index e8486baa..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt +++ /dev/null @@ -1,16 +0,0 @@ -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)" - } -} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt deleted file mode 100644 index 825ca964..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt +++ /dev/null @@ -1,10 +0,0 @@ -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 diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt deleted file mode 100644 index 0ac7faa1..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.builttoroam.devicecalendar.models - -enum class Availability { - BUSY, - FREE, - TENTATIVE -} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt deleted file mode 100644 index 6e10b7fe..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ /dev/null @@ -1,13 +0,0 @@ -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? -) { - var isReadOnly: Boolean = false - var isDefault: Boolean = false -} diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt deleted file mode 100644 index 22bb4c4b..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt +++ /dev/null @@ -1,16 +0,0 @@ -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 -) { - var ownCacheKey: Int? = null -} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt deleted file mode 100644 index a3918d75..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.builttoroam.devicecalendar.models - -class Event { - var eventTitle: String? = null - var eventId: String? = null - var calendarId: String? = null - var eventDescription: String? = null - var eventStartDate: Long? = null - var eventEndDate: Long? = null - var eventStartTimeZone: String? = null - var eventEndTimeZone: String? = null - var eventAllDay: Boolean = false - var eventLocation: String? = null - var eventURL: String? = null - var attendees: MutableList = mutableListOf() - var recurrenceRule: RecurrenceRule? = null - var organizer: Attendee? = null - var reminders: MutableList = mutableListOf() - var availability: Availability? = null - var eventStatus: EventStatus? = null - var eventColor: Long? = null - -} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt deleted file mode 100644 index c8422795..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.builttoroam.devicecalendar.models - -enum class EventStatus { - CONFIRMED, - CANCELED, - TENTATIVE -} \ No newline at end of file diff --git a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt deleted file mode 100644 index 1da83111..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.builttoroam.devicecalendar.models - -import org.dmfs.rfc5545.recur.Freq - -class RecurrenceRule(val freq: Freq) { - var count: Int? = null - var interval: 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/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt b/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt deleted file mode 100644 index c9695796..00000000 --- a/device_calendar/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.builttoroam.devicecalendar.models - -class Reminder(val minutes: Int) \ No newline at end of file diff --git a/device_calendar/device_calendar.iml b/device_calendar/device_calendar.iml deleted file mode 100644 index 73e7ebd0..00000000 --- a/device_calendar/device_calendar.iml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/device_calendar/device_calendar_android.iml b/device_calendar/device_calendar_android.iml deleted file mode 100644 index ac5d744d..00000000 --- a/device_calendar/device_calendar_android.iml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/device_calendar/example/.flutter-plugins-dependencies b/device_calendar/example/.flutter-plugins-dependencies deleted file mode 100644 index 4bf359d7..00000000 --- a/device_calendar/example/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"device_calendar","path":"/Users/naokreuzeder/Workspace_SAVE/AndroidStudio/FLUTTER/github/device_calendar/","native_build":true,"dependencies":[]},{"name":"flutter_native_timezone","path":"/Users/naokreuzeder/.pub-cache/hosted/pub.dev/flutter_native_timezone-2.0.0/","native_build":true,"dependencies":[]},{"name":"integration_test","path":"/Users/naokreuzeder/Development/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"android":[{"name":"device_calendar","path":"/Users/naokreuzeder/Workspace_SAVE/AndroidStudio/FLUTTER/github/device_calendar/","native_build":true,"dependencies":[]},{"name":"flutter_native_timezone","path":"/Users/naokreuzeder/.pub-cache/hosted/pub.dev/flutter_native_timezone-2.0.0/","native_build":true,"dependencies":[]},{"name":"integration_test","path":"/Users/naokreuzeder/Development/flutter/packages/integration_test/","native_build":true,"dependencies":[]}],"macos":[{"name":"flutter_native_timezone","path":"/Users/naokreuzeder/.pub-cache/hosted/pub.dev/flutter_native_timezone-2.0.0/","native_build":true,"dependencies":[]}],"linux":[],"windows":[],"web":[{"name":"flutter_native_timezone","path":"/Users/naokreuzeder/.pub-cache/hosted/pub.dev/flutter_native_timezone-2.0.0/","dependencies":[]}]},"dependencyGraph":[{"name":"device_calendar","dependencies":[]},{"name":"flutter_native_timezone","dependencies":[]},{"name":"integration_test","dependencies":[]}],"date_created":"2023-11-28 11:15:47.783591","version":"3.13.5"} \ No newline at end of file diff --git a/device_calendar/example/.gitignore b/device_calendar/example/.gitignore deleted file mode 100644 index dee655cc..00000000 --- a/device_calendar/example/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.DS_Store -.dart_tool/ - -.packages -.pub/ - -build/ - -.flutter-plugins diff --git a/device_calendar/example/.metadata b/device_calendar/example/.metadata deleted file mode 100644 index 8cab361b..00000000 --- a/device_calendar/example/.metadata +++ /dev/null @@ -1,8 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: 44b7e7d3f42f050a79712daab253af06e9daf530 - channel: beta diff --git a/device_calendar/example/README.md b/device_calendar/example/README.md deleted file mode 100644 index a24543bc..00000000 --- a/device_calendar/example/README.md +++ /dev/null @@ -1,195 +0,0 @@ -# Examples - -Most of the APIs are covered in [calendar_event.dart](https://github.com/builttoroam/device_calendar/blob/master/example/lib/presentation/pages/calendar_event.dart) or [calendar_events.dart](https://github.com/builttoroam/device_calendar/blob/master/example/lib/presentation/pages/calendar_events.dart) files in the example app. -You'll be able to get a reference of how the APIs are used. - -For a full API reference, the documentation can be found at [pub.dev](https://pub.dev/documentation/device_calendar/latest/device_calendar/device_calendar-library.html). - -## DayOfWeekGroup Enum - -`DayOfWeekGroup` enum allows to explicitly choose and return a list of `DayOfWeek` enum values by using an extension `getDays`: - -* `DayOfWeekGroup.Weekday.getDays` will return: - - ```dart - [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday]; - ``` - -* `DayOfWeekGroup.Weekend.getDays` will return: - - ```dart - [DayOfWeek.Saturday, DayOfWeek.Sunday]; - ``` - -* `DayOfWeekGroup.Alldays.getDays` will return: - - ```dart - [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday]; - ``` - -## Attendee Examples - -Examples below present on how to initialise an `Attendee` model in Dart: - -* A required attendee: - - ```dart - Attendee( - name: 'Test User 1', - emailAddress: 'test1@example.com', - role: AttendeeRole.Required); - ``` - -* An optional attendee: - - ```dart - Attendee( - name: 'Test User 2', - emailAddress: 'test2@example.com', - role: AttendeeRole.Optional); - ``` - -## Reminder Examples - -Examples below present on how to initialise a `Reminder` model in Dart: - -* 30 minutes - - ```dart - Reminder(minutes: 30); - ``` - -* 1 day - - ```dart - Reminder(minutes: 1440); - ``` - -## Recurrence Rule Examples - -Examples below present sample parameters of recurrence rules received by each platform and required properties for the `RecurrenceRule` model in Dart.\ -**Please note**: Receiving monthly and yearly recurrence parameters are slightly different for the two platforms. - -You can find more standard examples at [iCalendar.org](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html). - -### **Daily Rule** - -Daily every 5 days and end after 3 occurrences - -* Recurrence parameter example (Android and iOS):\ -`FREQ=DAILY;INTERVAL=5;COUNT=3` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Daily, - interval: 5, - totalOccurrences: 3); - ``` - -### **Weekly Rule** - -Weekly on Monday, Tuesday and Saturday every 2 weeks and end on 31 Jan 2020 - -* Recurrence parameter example (Android and iOS):\ -`FREQ=WEEKLY;BYDAY=MO,TU,SA;INTERVAL=2;UNTIL=20200130T130000Z` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Weekly, - interval: 2, - endDate: DateTime(2020, 1, 31), - daysOfWeek: [ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Saturday ]); - ``` - -### **Monthly/Yearly SetPosition (Week Number) Rule** - -Monthly on third Thursday - -* Recurrence parameter example (Android):\ -`FREQ=MONTHLY;INTERVAL=1;BYDAY=3TH` -* Recurrence parameter example (iOS):\ -`FREQ=MONTHLY;INTERVAL=1;BYDAY=TH;BYSETPOS=3` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Monthly, - interval: 1, - daysOfWeek: [ DayOfWeek.Thursday ], - weekOfMonth: WeekNumber.Third); - ``` - -Monthly on last Thursday - -* Recurrence parameter example (Android and iOS):\ -`FREQ=MONTHLY;INTERVAL=1;BYDAY=-1TH` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Monthly, - interval: 1, - daysOfWeek: [ DayOfWeek.Thursday ], - weekOfMonth: WeekNumber.Last); - ``` - -Yearly on third Thursday of January - -* Recurrence parameter example (Android and iOS):\ -`FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYDAY=3TH` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Yearly, - interval: 1, - monthOfYear: MonthOfYear.January, - weekOfMonth: WeekNumber.Third); - ``` - -Yearly on last Thursday of January - -* Recurrence parameter example (Android and iOS):\ -`FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYDAY=-1TH` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Yearly, - interval: 1, - monthOfYear: MonthOfYear.January, - weekOfMonth: WeekNumber.Last); - ``` - -### **Monthly/Yearly By Day of a Month Rule** - -Monthly on 8th - -* Recurrence parameter example (Android and iOS):\ -`FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=8` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Monthly, - interval: 1, - dayOfMonth: 8); - ``` - -Yearly on 8th of February - -* Recurrence parameter example (Android):\ -`FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=8;BYMONTH=2` -* Recurrence parameter example (iOS):\ -`FREQ=YEARLY;INTERVAL=1` -* Dart example: - - ```dart - RecurrenceRule( - RecurrenceFrequency.Yearly, - interval: 1, - monthOfYear: MonthOfYear.February, - dayOfMonth: 8); - ``` diff --git a/device_calendar/example/analysis_options.yaml b/device_calendar/example/analysis_options.yaml deleted file mode 100644 index 3e1200f9..00000000 --- a/device_calendar/example/analysis_options.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# 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 diff --git a/device_calendar/example/android/.gitignore b/device_calendar/example/android/.gitignore deleted file mode 100644 index 65b7315a..00000000 --- a/device_calendar/example/android/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -*.iml -*.class -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -GeneratedPluginRegistrant.java diff --git a/device_calendar/example/android/app/build.gradle b/device_calendar/example/android/app/build.gradle deleted file mode 100644 index 776dc817..00000000 --- a/device_calendar/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 32 - ndkVersion '22.1.7171670' - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.builttoroam.devicecalendarexample" - minSdkVersion 19 - targetSdkVersion 31 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -} diff --git a/device_calendar/example/android/app/proguard-rules.pro b/device_calendar/example/android/app/proguard-rules.pro deleted file mode 100644 index 77023c9c..00000000 --- a/device_calendar/example/android/app/proguard-rules.pro +++ /dev/null @@ -1 +0,0 @@ --keep class com.builttoroam.devicecalendar.** { *; } \ No newline at end of file diff --git a/device_calendar/example/android/app/src/main/AndroidManifest.xml b/device_calendar/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 630265e9..00000000 --- a/device_calendar/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/device_calendar/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt b/device_calendar/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt deleted file mode 100644 index 2d71b1d4..00000000 --- a/device_calendar/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.builttoroam.devicecalendarexample - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity : FlutterActivity() { -} diff --git a/device_calendar/example/android/app/src/main/res/drawable/launch_background.xml b/device_calendar/example/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f8..00000000 --- a/device_calendar/example/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/device_calendar/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/device_calendar/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b0906d62b1847e87f15cdcacf6a4f29..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ diff --git a/device_calendar/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/device_calendar/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a35cc66c3c1fd44f5a5526c1b78be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ diff --git a/device_calendar/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/device_calendar/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a88e3f88bea192c3a370d44689c3c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof diff --git a/device_calendar/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/device_calendar/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb28e45604e46eeda8dd24651419bc0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` diff --git a/device_calendar/example/android/app/src/main/res/values/styles.xml b/device_calendar/example/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 00fa4417..00000000 --- a/device_calendar/example/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/device_calendar/example/android/build.gradle b/device_calendar/example/android/build.gradle deleted file mode 100644 index d3f65307..00000000 --- a/device_calendar/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - ext.kotlin_version = '1.6.0' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/device_calendar/example/android/gradle.properties b/device_calendar/example/android/gradle.properties deleted file mode 100644 index 946d709d..00000000 --- a/device_calendar/example/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -android.enableJetifier=true -android.useAndroidX=true -org.gradle.jvmargs=-Xmx1536M \ No newline at end of file diff --git a/device_calendar/example/android/gradle/wrapper/gradle-wrapper.jar b/device_calendar/example/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 13372aef5e24af05341d49695ee84e5f9b594659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53636 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ⅅ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 H-+%o-(+5aQ diff --git a/device_calendar/example/android/gradle/wrapper/gradle-wrapper.properties b/device_calendar/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 6f5b3ec5..00000000 --- a/device_calendar/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Tue Jun 16 16:20:15 AEST 2020 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip diff --git a/device_calendar/example/android/gradlew b/device_calendar/example/android/gradlew deleted file mode 100755 index 9d82f789..00000000 --- a/device_calendar/example/android/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/device_calendar/example/android/gradlew.bat b/device_calendar/example/android/gradlew.bat deleted file mode 100644 index 8a0b282a..00000000 --- a/device_calendar/example/android/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/device_calendar/example/android/settings.gradle b/device_calendar/example/android/settings.gradle deleted file mode 100644 index 5a2f14fb..00000000 --- a/device_calendar/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/device_calendar/example/device_calendar_example.iml b/device_calendar/example/device_calendar_example.iml deleted file mode 100644 index c92516a5..00000000 --- a/device_calendar/example/device_calendar_example.iml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/device_calendar/example/device_calendar_example_android.iml b/device_calendar/example/device_calendar_example_android.iml deleted file mode 100644 index b050030a..00000000 --- a/device_calendar/example/device_calendar_example_android.iml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/device_calendar/example/integration_test/app_test.dart b/device_calendar/example/integration_test/app_test.dart deleted file mode 100644 index 6e4a1908..00000000 --- a/device_calendar/example/integration_test/app_test.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:uuid/uuid.dart'; - -import 'package:device_calendar_example/main.dart' as app; - -/// NOTE: These integration tests are currently made to be run on a physical device where there is at least a calendar that can be written to. -/// Calendar permissions are needed. See example/test_driver/integration_test.dart for how to run this on Android -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('Calendar plugin example', () { - final eventTitle = const Uuid().v1(); - final saveEventButtonFinder = find.byKey(const Key('saveEventButton')); - final eventTitleFinder = find.text(eventTitle); - final firstWritableCalendarFinder = - find.byKey(const Key('writableCalendar0')); - final addEventButtonFinder = find.byKey(const Key('addEventButton')); - final titleFieldFinder = find.byKey(const Key('titleField')); - final deleteButtonFinder = find.byKey(const Key('deleteEventButton')); -//TODO: remove redundant restarts. Currently needed because the first screen is always "test starting..." - testWidgets('starts on calendars page', (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - expect(find.byKey(const Key('calendarsPage')), findsOneWidget); - }); - testWidgets('select first writable calendar', (WidgetTester tester) async { - app.main(); - - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - expect(firstWritableCalendarFinder, findsOneWidget); - }); - testWidgets('go to add event page', (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - await tester.tap(firstWritableCalendarFinder); - - await tester.pumpAndSettle(); - expect(addEventButtonFinder, findsOneWidget); - print('found add event button'); - await tester.tap(addEventButtonFinder); - await tester.pumpAndSettle(); - expect(saveEventButtonFinder, findsOneWidget); - }); - testWidgets('try to save event without entering mandatory fields', - (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - await tester.tap(firstWritableCalendarFinder); - await tester.pumpAndSettle(); - await tester.tap(addEventButtonFinder); - - await tester.pumpAndSettle(); - await tester.tap(saveEventButtonFinder); - await tester.pumpAndSettle(); - expect(find.text('Please fix the errors in red before submitting.'), - findsOneWidget); - }); - testWidgets('save event with title $eventTitle', - (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - await tester.tap(firstWritableCalendarFinder); - await tester.pumpAndSettle(); - await tester.tap(addEventButtonFinder); - - await tester.pumpAndSettle(); - await tester.tap(titleFieldFinder); - - await tester.enterText(titleFieldFinder, eventTitle); - await tester.tap(saveEventButtonFinder); - await tester.pumpAndSettle(); - expect(eventTitleFinder, findsOneWidget); - }); - testWidgets('delete event with title $eventTitle', - (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - await tester.tap(firstWritableCalendarFinder); - await tester.pumpAndSettle(); - await tester.tap(eventTitleFinder); - - await tester.scrollUntilVisible(deleteButtonFinder, -5); - await tester.tap(deleteButtonFinder); - await tester.pumpAndSettle(); - expect(eventTitleFinder, findsNothing); - }); - }); -} diff --git a/device_calendar/example/integration_test/integration_test.dart b/device_calendar/example/integration_test/integration_test.dart deleted file mode 100644 index ca6e9ef3..00000000 --- a/device_calendar/example/integration_test/integration_test.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:integration_test/integration_test_driver.dart'; - -/// Instruction for iOS: -/// See `ios.sh` -/// Instruction for android: -/// See `integration_test_android.dart` - -Future main() => integrationDriver(); diff --git a/device_calendar/example/integration_test/integration_test_android.dart b/device_calendar/example/integration_test/integration_test_android.dart deleted file mode 100644 index 2c840da5..00000000 --- a/device_calendar/example/integration_test/integration_test_android.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'dart:io'; - -import 'package:integration_test/integration_test_driver.dart'; - -// make sure 'adb devices' works on your local machine, then from the root of the plugin, run the following: -/* -1. -cd example -2. -flutter drive --driver=integration_test/integration_test.dart --target=integration_test/app_test.dart - */ - -Future main() async { - await Process.run('adb', [ - 'shell', - 'pm', - 'grant', - 'com.builttoroam.devicecalendarexample', - 'android.permission.READ_CALENDAR' - ]); - await Process.run('adb', [ - 'shell', - 'pm', - 'grant', - 'com.builttoroam.devicecalendarexample', - 'android.permission.WRITE_CALENDAR' - ]); - await integrationDriver(); -} diff --git a/device_calendar/example/integration_test/ios.sh b/device_calendar/example/integration_test/ios.sh deleted file mode 100755 index 8f8eede4..00000000 --- a/device_calendar/example/integration_test/ios.sh +++ /dev/null @@ -1,24 +0,0 @@ -# Use: integration_test/ios.sh -# -# Executes the device_calendar integration test for iOS -# This script creates and starts a new iOS simulator, grants calendar permission -# to the app then runs the integration tests and finally deletes the simulator. -# -# Prerequisites: Xcode, Xcode Command Line Tools, Xcode iOS Simulator -# -# To run an integration test, make sure the script has execute permission -# example: `chmod +x example/integration_test/ios.sh` then: -# 1. cd example -# 2. integration_test/ios.sh -# 3. You should see `All tests passed` -# -# Success - "All tests passed." is printed to the console -# -deviceId=$(xcrun simctl create builtToRoamCalendarTest "iPhone 13" 2> /dev/null | tail -1) -echo "Created device: $deviceId, booting..." -xcrun simctl boot $deviceId -xcrun simctl privacy $deviceId grant calendar com.builttoroam.deviceCalendarExample00 -echo "Running tests..." -flutter drive --driver=integration_test/integration_test.dart --target=integration_test/app_test.dart -d $deviceId -echo "Removing device: $deviceId" -xcrun simctl delete $deviceId diff --git a/device_calendar/example/ios/.gitignore b/device_calendar/example/ios/.gitignore deleted file mode 100755 index 1c202be0..00000000 --- a/device_calendar/example/ios/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/app.flx -/Flutter/app.zip -/Flutter/flutter_assets/ -/Flutter/App.framework -/Flutter/Flutter.framework -/Flutter/Generated.xcconfig -/ServiceDefinitions.json - -**/.symlinks/ -Pods/ diff --git a/device_calendar/example/ios/Flutter/AppFrameworkInfo.plist b/device_calendar/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100755 index 9b41e7d8..00000000 --- a/device_calendar/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 11.0 - - diff --git a/device_calendar/example/ios/Flutter/Debug.xcconfig b/device_calendar/example/ios/Flutter/Debug.xcconfig deleted file mode 100755 index e8efba11..00000000 --- a/device_calendar/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/device_calendar/example/ios/Flutter/Release.xcconfig b/device_calendar/example/ios/Flutter/Release.xcconfig deleted file mode 100755 index 399e9340..00000000 --- a/device_calendar/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/device_calendar/example/ios/Flutter/flutter_export_environment.sh b/device_calendar/example/ios/Flutter/flutter_export_environment.sh deleted file mode 100755 index bc51da7b..00000000 --- a/device_calendar/example/ios/Flutter/flutter_export_environment.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/naokreuzeder/Development/flutter" -export "FLUTTER_APPLICATION_PATH=/Users/naokreuzeder/Workspace_SAVE/AndroidStudio/FLUTTER/github/example" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_TARGET=lib/main.dart" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=3.2.0" -export "FLUTTER_BUILD_NUMBER=3.2.0" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/device_calendar/example/ios/Podfile b/device_calendar/example/ios/Podfile deleted file mode 100644 index 997d1cb3..00000000 --- a/device_calendar/example/ios/Podfile +++ /dev/null @@ -1,45 +0,0 @@ -# Uncomment this line to define a global platform for your project -platform :ios, '11.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - flutter_additional_ios_build_settings(target) - config.build_settings['SWIFT_VERSION'] = '5.0' - # Or whatever Swift version your app is using that works with your plugins - end - end -end diff --git a/device_calendar/example/ios/Podfile.lock b/device_calendar/example/ios/Podfile.lock deleted file mode 100755 index cb8f159b..00000000 --- a/device_calendar/example/ios/Podfile.lock +++ /dev/null @@ -1,34 +0,0 @@ -PODS: - - device_calendar (0.0.1): - - Flutter - - Flutter (1.0.0) - - flutter_native_timezone (0.0.1): - - Flutter - - integration_test (0.0.1): - - Flutter - -DEPENDENCIES: - - device_calendar (from `.symlinks/plugins/device_calendar/ios`) - - Flutter (from `Flutter`) - - flutter_native_timezone (from `.symlinks/plugins/flutter_native_timezone/ios`) - - integration_test (from `.symlinks/plugins/integration_test/ios`) - -EXTERNAL SOURCES: - device_calendar: - :path: ".symlinks/plugins/device_calendar/ios" - Flutter: - :path: Flutter - flutter_native_timezone: - :path: ".symlinks/plugins/flutter_native_timezone/ios" - integration_test: - :path: ".symlinks/plugins/integration_test/ios" - -SPEC CHECKSUMS: - device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 - integration_test: 13825b8a9334a850581300559b8839134b124670 - -PODFILE CHECKSUM: 10625bdc9b9ef8574174815aabd5b048e6e29bff - -COCOAPODS: 1.11.3 diff --git a/device_calendar/example/ios/Runner.xcodeproj/project.pbxproj b/device_calendar/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 160e1d14..00000000 --- a/device_calendar/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,507 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 763C5C3662C48FEF7F2B0120 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E098C60D243A71853922C094 /* Pods_Runner.framework */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2AD784C0989B63BAA46EFDFD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E098C60D243A71853922C094 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F504DA6DD95FB326B0725EEE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 763C5C3662C48FEF7F2B0120 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 0C6DE7144DB7716571BF5210 /* Pods */ = { - isa = PBXGroup; - children = ( - F504DA6DD95FB326B0725EEE /* Pods-Runner.debug.xcconfig */, - 2AD784C0989B63BAA46EFDFD /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 6400E78A7B626A4B08303DA0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - E098C60D243A71853922C094 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 0C6DE7144DB7716571BF5210 /* Pods */, - 6400E78A7B626A4B08303DA0 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - A6EB42BBC2C9005F5277AB4F /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 122A90E1AB6EBE6FCB241A06 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1300; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = PG8Q9ZR89L; - LastSwiftMigration = 1130; - ProvisioningStyle = Automatic; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 122A90E1AB6EBE6FCB241A06 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/device_calendar/device_calendar.framework", - "${BUILT_PRODUCTS_DIR}/flutter_native_timezone/flutter_native_timezone.framework", - "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_calendar.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_timezone.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - A6EB42BBC2C9005F5277AB4F /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = PG8Q9ZR89L; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = PG8Q9ZR89L; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index 919434a6..00000000 --- a/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/device_calendar/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/device_calendar/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/device_calendar/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100755 index 14d255fd..00000000 --- a/device_calendar/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/device_calendar/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/device_calendar/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index 21a3cc14..00000000 --- a/device_calendar/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/device_calendar/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/device_calendar/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index 18d98100..00000000 --- a/device_calendar/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/device_calendar/example/ios/Runner/AppDelegate.swift b/device_calendar/example/ios/Runner/AppDelegate.swift deleted file mode 100755 index 70693e4a..00000000 --- a/device_calendar/example/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100755 index d36b1fab..00000000 --- a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100755 index 3d43d11e66f4de3da27ed045ca4fe38ad8b48094..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11112 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 P-1+_A+?hGkL;m(J7kc&K diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100755 index 28c6bf03016f6c994b70f38d1b7346e5831b531f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100755 index f091b6b0bca859a3f474b03065bef75ba58a9e4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100755 index d0ef06e7edb86cdfe0d15b4b0d98334a86163658..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1716 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 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100755 index c8f9ed8f5cee1c98386d13b17e89f719e83555b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100755 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100755 index a6d6b8609df07bf62e5100a53a01510388bd2b22..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100755 index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3831 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 diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/device_calendar/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100755 index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100755 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100755 index 9da19eacad3b03bb08bbddbbf4ac48dd78b3d838..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v diff --git a/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100755 index 89c2725b..00000000 --- a/device_calendar/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/device_calendar/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/device_calendar/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100755 index f2e259c7..00000000 --- a/device_calendar/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/device_calendar/example/ios/Runner/Base.lproj/Main.storyboard b/device_calendar/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100755 index f3c28516..00000000 --- a/device_calendar/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/device_calendar/example/ios/Runner/Info.plist b/device_calendar/example/ios/Runner/Info.plist deleted file mode 100755 index ad25cf2b..00000000 --- a/device_calendar/example/ios/Runner/Info.plist +++ /dev/null @@ -1,55 +0,0 @@ - - - - - NSContactsUsageDescription - Contacts are used for event attendee editing. - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - device_calendar_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - NSCalendarsUsageDescription - We need access to your calendar to help you track events - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/device_calendar/example/ios/Runner/Runner-Bridging-Header.h b/device_calendar/example/ios/Runner/Runner-Bridging-Header.h deleted file mode 100755 index 7335fdf9..00000000 --- a/device_calendar/example/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/device_calendar/example/lib/common/app_routes.dart b/device_calendar/example/lib/common/app_routes.dart deleted file mode 100644 index 991a9d70..00000000 --- a/device_calendar/example/lib/common/app_routes.dart +++ /dev/null @@ -1,3 +0,0 @@ -class AppRoutes { - static const calendars = '/'; -} diff --git a/device_calendar/example/lib/main.dart b/device_calendar/example/lib/main.dart deleted file mode 100644 index 3b5d61ee..00000000 --- a/device_calendar/example/lib/main.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'common/app_routes.dart'; -import 'presentation/pages/calendars.dart'; - -void main() => runApp(const MyApp()); - -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); - - @override - State createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData(), - themeMode: ThemeMode.system, - darkTheme: ThemeData.dark(), - routes: { - AppRoutes.calendars: (context) { - return const CalendarsPage(key: Key('calendarsPage')); - } - }, - ); - } -} diff --git a/device_calendar/example/lib/presentation/date_time_picker.dart b/device_calendar/example/lib/presentation/date_time_picker.dart deleted file mode 100644 index dc11e8d9..00000000 --- a/device_calendar/example/lib/presentation/date_time_picker.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -import 'input_dropdown.dart'; - -class DateTimePicker extends StatelessWidget { - const DateTimePicker( - {Key? key, - this.labelText, - this.selectedDate, - this.selectedTime, - this.selectDate, - this.selectTime, - this.enableTime = true}) - : super(key: key); - - final String? labelText; - final DateTime? selectedDate; - final TimeOfDay? selectedTime; - final ValueChanged? selectDate; - final ValueChanged? selectTime; - final bool enableTime; - - Future _selectDate(BuildContext context) async { - final picked = await showDatePicker( - context: context, - initialDate: selectedDate != null - ? DateTime.parse(selectedDate.toString()) - : DateTime.now(), - firstDate: DateTime(2015, 8), - lastDate: DateTime(2101)); - if (picked != null && picked != selectedDate && selectDate != null) { - selectDate!(picked); - } - } - - Future _selectTime(BuildContext context) async { - if (selectedTime == null) return; - final picked = - await showTimePicker(context: context, initialTime: selectedTime!); - if (picked != null && picked != selectedTime) selectTime!(picked); - } - - @override - Widget build(BuildContext context) { - final valueStyle = Theme.of(context).textTheme.headline6; - return Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - flex: 4, - child: InputDropdown( - labelText: labelText, - valueText: selectedDate == null - ? '' - : DateFormat.yMMMd().format(selectedDate as DateTime), - valueStyle: valueStyle, - onPressed: () { - _selectDate(context); - }, - ), - ), - if (enableTime) ...[ - const SizedBox(width: 12.0), - Expanded( - flex: 3, - child: InputDropdown( - valueText: selectedTime?.format(context) ?? '', - valueStyle: valueStyle, - onPressed: () { - _selectTime(context); - }, - ), - ), - ] - ], - ); - } -} diff --git a/device_calendar/example/lib/presentation/event_item.dart b/device_calendar/example/lib/presentation/event_item.dart deleted file mode 100644 index f91bb7de..00000000 --- a/device_calendar/example/lib/presentation/event_item.dart +++ /dev/null @@ -1,342 +0,0 @@ -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 'recurring_event_dialog.dart'; - -class EventItem extends StatefulWidget { - final Event? _calendarEvent; - final DeviceCalendarPlugin _deviceCalendarPlugin; - final bool _isReadOnly; - - final Function(Event) _onTapped; - final VoidCallback _onLoadingStarted; - final Function(bool) _onDeleteFinished; - - const EventItem( - this._calendarEvent, - this._deviceCalendarPlugin, - this._onLoadingStarted, - this._onDeleteFinished, - this._onTapped, - this._isReadOnly, - {Key? key}) - : super(key: key); - - @override - State createState() { - return _EventItemState(); - } -} - -class _EventItemState extends State { - final double _eventFieldNameWidth = 75.0; - Location? _currentLocation; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => setCurentLocation()); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - if (widget._calendarEvent != null) { - widget._onTapped(widget._calendarEvent as Event); - } - }, - child: Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Padding( - padding: EdgeInsets.symmetric(vertical: 10.0), - child: FlutterLogo(), - ), - ListTile( - title: Text(widget._calendarEvent?.title ?? ''), - subtitle: Text(widget._calendarEvent?.description ?? '')), - Container( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - children: [ - if (_currentLocation != null) - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Starts'), - ), - Text( - widget._calendarEvent == null - ? '' - : _formatDateTime( - dateTime: widget._calendarEvent!.start!, - ), - ) - ], - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 5.0), - ), - if (_currentLocation != null) - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Ends'), - ), - Text( - widget._calendarEvent?.end == null - ? '' - : _formatDateTime( - dateTime: widget._calendarEvent!.end!, - ), - ), - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('All day?'), - ), - Text(widget._calendarEvent?.allDay != null && - widget._calendarEvent?.allDay == true - ? 'Yes' - : 'No') - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Location'), - ), - Expanded( - child: Text( - widget._calendarEvent?.location ?? '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('URL'), - ), - Expanded( - child: Text( - widget._calendarEvent?.url?.data?.contentText ?? '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Attendees'), - ), - Expanded( - child: Text( - widget._calendarEvent?.attendees - ?.where((a) => a?.name?.isNotEmpty ?? false) - .map((a) => a?.name) - .join(', ') ?? - '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Availability'), - ), - Expanded( - child: Text( - widget._calendarEvent?.availability.enumToString ?? - '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - const SizedBox( - height: 10.0, - ), - Align( - alignment: Alignment.topLeft, - child: Row( - children: [ - SizedBox( - width: _eventFieldNameWidth, - child: const Text('Status'), - ), - Expanded( - child: Text( - widget._calendarEvent?.status?.enumToString ?? '', - overflow: TextOverflow.ellipsis, - ), - ) - ], - ), - ), - ], - ), - ), - ButtonBar( - children: [ - if (!widget._isReadOnly) ...[ - IconButton( - onPressed: () { - if (widget._calendarEvent != null) { - widget._onTapped(widget._calendarEvent as Event); - } - }, - icon: const Icon(Icons.edit), - ), - IconButton( - onPressed: () async { - await showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - if (widget._calendarEvent?.recurrenceRule == null) { - return AlertDialog( - title: const Text( - 'Are you sure you want to delete this event?'), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () async { - Navigator.of(context).pop(); - widget._onLoadingStarted(); - final deleteResult = await widget - ._deviceCalendarPlugin - .deleteEvent( - widget._calendarEvent?.calendarId, - widget._calendarEvent?.eventId); - widget._onDeleteFinished( - deleteResult.isSuccess && - deleteResult.data != null); - }, - child: const Text('Delete'), - ), - ], - ); - } else { - if (widget._calendarEvent == null) { - return const SizedBox(); - } - return RecurringEventDialog( - widget._deviceCalendarPlugin, - widget._calendarEvent!, - widget._onLoadingStarted, - widget._onDeleteFinished); - } - }, - ); - }, - icon: const Icon(Icons.delete), - ), - ] else ...[ - IconButton( - onPressed: () { - if (widget._calendarEvent != null) { - widget._onTapped(widget._calendarEvent!); - } - }, - icon: const Icon(Icons.remove_red_eye), - ), - ] - ], - ) - ], - ), - ), - ); - } - - void setCurentLocation() async { - String? timezone; - try { - timezone = await FlutterNativeTimezone.getLocalTimezone(); - } catch (e) { - debugPrint('Could not get the local timezone'); - } - timezone ??= 'Etc/UTC'; - _currentLocation = timeZoneDatabase.locations[timezone]; - setState(() {}); - } - - /// Formats [dateTime] into a human-readable string. - /// If [_calendarEvent] is an Android allDay event, then the output will - /// omit the time. - String _formatDateTime({DateTime? dateTime}) { - if (dateTime == null) { - return 'Error'; - } - var output = ''; - if (Platform.isAndroid && widget._calendarEvent?.allDay == true) { - // just the dates, no times - output = DateFormat.yMd().format(dateTime); - } else { - output = DateFormat('yyyy-MM-dd HH:mm:ss') - .format(TZDateTime.from(dateTime, _currentLocation!)); - } - return output; - } -} diff --git a/device_calendar/example/lib/presentation/input_dropdown.dart b/device_calendar/example/lib/presentation/input_dropdown.dart deleted file mode 100644 index a6c19820..00000000 --- a/device_calendar/example/lib/presentation/input_dropdown.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class InputDropdown extends StatelessWidget { - const InputDropdown( - {Key? key, - this.child, - this.labelText, - this.valueText, - this.valueStyle, - this.onPressed}) - : super(key: key); - - final String? labelText; - final String? valueText; - final TextStyle? valueStyle; - final VoidCallback? onPressed; - final Widget? child; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onPressed, - child: InputDecorator( - decoration: InputDecoration( - labelText: labelText, - ), - baseStyle: valueStyle, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - if (valueText != null) Text(valueText!, style: valueStyle), - Icon(Icons.arrow_drop_down, - color: Theme.of(context).brightness == Brightness.light - ? Colors.grey.shade700 - : Colors.white70), - ], - ), - ), - ); - } -} diff --git a/device_calendar/example/lib/presentation/pages/calendar_add.dart b/device_calendar/example/lib/presentation/pages/calendar_add.dart deleted file mode 100644 index 7d6d8820..00000000 --- a/device_calendar/example/lib/presentation/pages/calendar_add.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'dart:io'; - -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(); - } -} - -class _CalendarAddPageState extends State { - final GlobalKey _formKey = GlobalKey(); - final GlobalKey _scaffoldKey = GlobalKey(); - late DeviceCalendarPlugin _deviceCalendarPlugin; - - AutovalidateMode _autovalidate = AutovalidateMode.disabled; - String _calendarName = ''; - ColorChoice? _colorChoice; - String _localAccountName = ''; - - _CalendarAddPageState() { - _deviceCalendarPlugin = DeviceCalendarPlugin(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: const Text('Create Calendar'), - ), - body: Form( - autovalidateMode: _autovalidate, - key: _formKey, - child: Container( - padding: const EdgeInsets.all(10), - child: Column( - children: [ - TextFormField( - decoration: const InputDecoration( - labelText: 'Calendar Name', - hintText: 'My New Calendar', - ), - validator: _validateCalendarName, - onSaved: (String? value) => _calendarName = value ?? '', - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('Calendar Color'), - DropdownButton( - onChanged: (selectedColor) { - setState(() => _colorChoice = selectedColor); - }, - value: _colorChoice, - items: ColorChoice.values - .map((color) => DropdownMenuItem( - value: color, - child: Text(color.toString().split('.').last), - )) - .toList(), - ), - ], - ), - if (Platform.isAndroid) - TextFormField( - decoration: const InputDecoration( - labelText: 'Local Account Name', - hintText: 'Device Calendar', - ), - onSaved: (String? value) => _localAccountName = value ?? '', - ), - ], - ), - ), - ), - floatingActionButton: FloatingActionButton( - 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.'); - } else { - form?.save(); - var result = await _deviceCalendarPlugin.createCalendar( - _calendarName, - calendarColor: _colorChoice?.value, - localAccountName: _localAccountName, - ); - - if (result.isSuccess) { - Navigator.pop(context, true); - } else { - showInSnackBar(result.errors - .map((err) => '[${err.errorCode}] ${err.errorMessage}') - .join(' | ')); - } - } - }, - child: const Icon(Icons.check), - ), - ); - } - - String? _validateCalendarName(String? value) { - if (value == null) return null; - if (value.isEmpty) { - return 'Calendar name is required.'; - } - - return null; - } - - void showInSnackBar(String value) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); - } -} - -enum ColorChoice { - Red, - Orange, - Yellow, - Green, - Blue, - Purple, - Brown, - Black, - White -} - -extension ColorChoiceExtension on ColorChoice { - static Color _value(ColorChoice val) { - switch (val) { - case ColorChoice.Red: - return Colors.red; - case ColorChoice.Orange: - return Colors.orange; - case ColorChoice.Yellow: - return Colors.yellow; - case ColorChoice.Green: - return Colors.green; - case ColorChoice.Blue: - return Colors.blue; - case ColorChoice.Purple: - return Colors.purple; - case ColorChoice.Brown: - return Colors.brown; - case ColorChoice.Black: - return Colors.black; - case ColorChoice.White: - return Colors.white; - default: - return Colors.red; - } - } - - Color get value => _value(this); -} diff --git a/device_calendar/example/lib/presentation/pages/calendar_event.dart b/device_calendar/example/lib/presentation/pages/calendar_event.dart deleted file mode 100644 index 72c4cb5b..00000000 --- a/device_calendar/example/lib/presentation/pages/calendar_event.dart +++ /dev/null @@ -1,1261 +0,0 @@ -import 'dart:io'; - -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 '../date_time_picker.dart'; -import '../recurring_event_dialog.dart'; -import 'event_attendee.dart'; -import 'event_reminders.dart'; - -enum RecurrenceRuleEndType { Indefinite, MaxOccurrences, SpecifiedEndDate } - -class CalendarEventPage extends StatefulWidget { - final Calendar _calendar; - final Event? _event; - final RecurringEventDialog? _recurringEventDialog; - - const CalendarEventPage(this._calendar, - [this._event, this._recurringEventDialog, Key? key]) - : super(key: key); - - @override - _CalendarEventPageState createState() { - return _CalendarEventPageState(_calendar, _event, _recurringEventDialog); - } -} - -class _CalendarEventPageState extends State { - final GlobalKey _formKey = GlobalKey(); - final GlobalKey _scaffoldKey = GlobalKey(); - final Calendar _calendar; - - Event? _event; - 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; - - TZDateTime? _endDate; - TimeOfDay? _endTime; - - AutovalidateMode _autovalidate = AutovalidateMode.disabled; - DayOfWeekGroup _dayOfWeekGroup = DayOfWeekGroup.None; - - RecurrenceRuleEndType _recurrenceRuleEndType = - RecurrenceRuleEndType.Indefinite; - RecurrenceRule? _rrule; - - final List _validDaysOfMonth = []; - - Availability _availability = Availability.Busy; - EventStatus? _eventStatus; - List? _attendees; - List? _reminders; - String _timezone = 'Etc/UTC'; - - _CalendarEventPageState( - this._calendar, this._event, this._recurringEventDialog) { - getCurentLocation(); - } - - void getCurentLocation() async { - try { - _timezone = await FlutterNativeTimezone.getLocalTimezone(); - } catch (e) { - debugPrint('Could not get the local timezone'); - } - - _deviceCalendarPlugin = DeviceCalendarPlugin(); - - final event = _event; - if (event == null) { - debugPrint( - 'calendar_event _timezone ------------------------- $_timezone'); - final currentLocation = timeZoneDatabase.locations[_timezone]; - if (currentLocation != null) { - 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']; - 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, availability: _availability); - - debugPrint('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); - - _eventStatus = EventStatus.None; - } else { - 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); - } - - final attendees = event.attendees; - if (attendees != null && attendees.isNotEmpty) { - _attendees = []; - _attendees?.addAll(attendees as Iterable); - } - - final reminders = event.reminders; - if (reminders != null && reminders.isNotEmpty) { - _reminders = []; - _reminders?.addAll(reminders); - } - - final rrule = event.recurrenceRule; - if (rrule != null) { - // debugPrint('OLD_RRULE: ${rrule.toString()}'); - _rrule = rrule; - if (rrule.count != null) { - _recurrenceRuleEndType = RecurrenceRuleEndType.MaxOccurrences; - } - if (rrule.until != null) { - _recurrenceRuleEndType = RecurrenceRuleEndType.SpecifiedEndDate; - } - } - - _availability = event.availability; - _eventStatus = event.status; - } - - // Getting days of the current month (or a selected month for the yearly recurrence) as a default - _getValidDaysOfMonth(_rrule?.frequency); - setState(() {}); - } - - void printAttendeeDetails(Attendee attendee) { - debugPrint( - 'attendee name: ${attendee.name}, email address: ${attendee.emailAddress}, type: ${attendee.role?.enumToString}'); - debugPrint( - 'ios specifics - status: ${attendee.iosAttendeeDetails?.attendanceStatus}, type: ${attendee.iosAttendeeDetails?.attendanceStatus?.enumToString}'); - debugPrint( - 'android specifics - status ${attendee.androidAttendeeDetails?.attendanceStatus}, type: ${attendee.androidAttendeeDetails?.attendanceStatus?.enumToString}'); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: Text(_event?.eventId?.isEmpty ?? true - ? 'Create event' - : _calendar.isReadOnly == true - ? 'View event ${_event?.title}' - : 'Edit event ${_event?.title}'), - ), - 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?.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; - }, - ), - ), - 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; - } - }, - ), - ), - ListTile( - leading: const Text( - 'Availability', - style: TextStyle(fontSize: 16), - ), - trailing: DropdownButton( - value: _availability, - onChanged: (Availability? newValue) { - setState(() { - if (newValue != null) { - _availability = newValue; - _event?.availability = newValue; - } - }); - }, - items: Availability.values - .map>( - (Availability value) { - return DropdownMenuItem( - value: value, - child: Text(value.enumToString), - ); - }).toList(), - ), - ), - if (Platform.isAndroid) - ListTile( - leading: const 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: const Text('All Day'), - ), - if (_startDate != null) - Padding( - padding: const EdgeInsets.all(10.0), - child: DateTimePicker( - labelText: 'From', - enableTime: _event?.allDay == false, - selectedDate: _startDate, - selectedTime: _startTime, - selectDate: (DateTime date) { - setState(() { - var currentLocation = - timeZoneDatabase.locations[_timezone]; - if (currentLocation != null) { - _startDate = - TZDateTime.from(date, currentLocation); - _event?.start = _combineDateWithTime( - _startDate, _startTime); - } - }); - }, - selectTime: (TimeOfDay time) { - setState( - () { - _startTime = time; - _event?.start = _combineDateWithTime( - _startDate, _startTime); - }, - ); - }, - ), - ), - 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); - _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: const 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) => - const EventAttendeePage())); - if (result != null) { - _attendees ??= []; - setState(() { - _attendees?.add(result); - }); - } - } - : null, - leading: const Icon(Icons.people), - title: Text(_calendar.isReadOnly == false - ? 'Add Attendees' - : 'Attendees'), - ), - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: _attendees?.length ?? 0, - itemBuilder: (context, index) { - return Container( - color: (_attendees?[index].isOrganiser ?? false) - ? MediaQuery.of(context).platformBrightness == - Brightness.dark - ? Colors.black26 - : 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), - 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 ?? - 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: const 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: const 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: const 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 ?? true) - Text(_calendar.isReadOnly == false - ? 'Add reminders' - : 'Reminders'), - for (var reminder in _reminders ?? []) - Text('${reminder.minutes} minutes before; ') - ], - ), - ), - ), - ), - CheckboxListTile( - value: _rrule != null, - title: const Text('Is recurring'), - onChanged: (isChecked) { - if (isChecked != null) { - setState(() { - if (isChecked) { - _rrule = - RecurrenceRule(frequency: Frequency.daily); - } else { - _rrule = null; - } - }); - } - }, - ), - if (_rrule != null) ...[ - ListTile( - leading: const Text('Select a Recurrence Type'), - trailing: DropdownButton( - onChanged: (selectedFrequency) { - setState(() { - _onFrequencyChange( - selectedFrequency ?? Frequency.daily); - _getValidDaysOfMonth(selectedFrequency); - }); - }, - 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(), - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), - child: Row( - children: [ - const Text('Repeat Every '), - Flexible( - child: TextFormField( - initialValue: '${_rrule?.interval ?? 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) { - _rrule = _rrule?.copyWith( - interval: int.tryParse(value)); - } - }, - ), - ), - _recurrenceFrequencyToIntervalText( - _rrule?.frequency), - ], - ), - ), - 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(() { - 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: 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: const Text('Delete'), - ), - ], - ], - ), - ), - ), - ), - floatingActionButton: Visibility( - visible: _calendar.isReadOnly == false, - child: FloatingActionButton( - key: const Key('saveEventButton'), - onPressed: () async { - final form = _formKey.currentState; - if (form?.validate() == false) { - _autovalidate = - AutovalidateMode.always; // Start validating on every change. - showInSnackBar( - context, 'Please fix the errors in red before submitting.'); - return; - } else { - form?.save(); - _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: const Icon(Icons.check), - ), - ), - ); - } - - 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(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 const Text('Indefinitely'); - case RecurrenceRuleEndType.MaxOccurrences: - return const Text('After a set number of times'); - case RecurrenceRuleEndType.SpecifiedEndDate: - return const Text('Continues until a specified date'); - default: - return const Text(''); - } - } - - // Get total days of a month - void _getValidDaysOfMonth(Frequency? frequency) { - _validDaysOfMonth.clear(); - var totalDays = 0; - - // Year frequency: Get total days of the selected month - if (frequency == Frequency.yearly) { - totalDays = DateTime(DateTime.now().year, - (_rrule?.hasByMonths ?? false) ? _rrule!.byMonths.first : 1, 0) - .day; - } else { - // Otherwise, get total days of the current month - var now = DateTime.now(); - totalDays = DateTime(now.year, now.month + 1, 0).day; - } - - for (var i = 1; i <= totalDays; i++) { - _validDaysOfMonth.add(i); - } - } - - void _updateDaysOfWeek() { - 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: - _rrule = _rrule?.copyWith(byWeekDays: { - ByWeekDayEntry(1), - ByWeekDayEntry(2), - ByWeekDayEntry(3), - ByWeekDayEntry(4), - ByWeekDayEntry(5), - ByWeekDayEntry(6), - ByWeekDayEntry(7), - }); - break; - case DayOfWeekGroup.None: - default: - _rrule?.byWeekDays.clear(); - break; - } - // () => setState(() => {}); - } - - void _updateDaysOfWeekGroup({DayOfWeek? selectedDay}) { - 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; - } - } - } - - int _weekNumFromWeekDayOccurence(Set weekdays) { - final weekNum = weekdays.first.occurrence; - if (weekNum != null) { - return weekNum - 1; - } else { - return 0; - } - } - - 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); - } - } - } - } - - /// 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; - } - } - } - - String? _validateTotalOccurrences(String? value) { - if (value == null) return null; - if (value.isNotEmpty && int.tryParse(value) == null) { - return 'Total occurrences needs to be a valid number'; - } - return null; - } - - String? _validateInterval(String? value) { - if (value == null) return null; - if (value.isNotEmpty && int.tryParse(value) == null) { - return 'Interval needs to be a valid number'; - } - return null; - } - - String? _validateTitle(String? value) { - if (value == null) return null; - if (value.isEmpty) { - return 'Name is required.'; - } - return null; - } - - TZDateTime? _combineDateWithTime(TZDateTime? date, TimeOfDay? time) { - if (date == null) return null; - var currentLocation = timeZoneDatabase.locations[_timezone]; - - final dateWithoutTime = TZDateTime.from( - DateTime.parse(DateFormat('y-MM-dd 00:00:00').format(date)), - currentLocation!); - - if (time == null) return dateWithoutTime; - if (Platform.isAndroid && _event?.allDay == true) return dateWithoutTime; - - return dateWithoutTime - .add(Duration(hours: time.hour, minutes: time.minute)); - } - - void showInSnackBar(BuildContext context, String value) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); - } -} diff --git a/device_calendar/example/lib/presentation/pages/calendar_events.dart b/device_calendar/example/lib/presentation/pages/calendar_events.dart deleted file mode 100644 index a8d4b2b2..00000000 --- a/device_calendar/example/lib/presentation/pages/calendar_events.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'dart:async'; - -import 'package:device_calendar/device_calendar.dart'; -import 'package:flutter/material.dart'; - -import '../event_item.dart'; -import '../recurring_event_dialog.dart'; -import 'calendar_event.dart'; - -class CalendarEventsPage extends StatefulWidget { - final Calendar _calendar; - - const CalendarEventsPage(this._calendar, {Key? key}) : super(key: key); - - @override - _CalendarEventsPageState createState() { - return _CalendarEventsPageState(_calendar); - } -} - -class _CalendarEventsPageState extends State { - final Calendar _calendar; - final GlobalKey _scaffoldstate = GlobalKey(); - - late DeviceCalendarPlugin _deviceCalendarPlugin; - List _calendarEvents = []; - bool _isLoading = true; - - _CalendarEventsPageState(this._calendar) { - _deviceCalendarPlugin = DeviceCalendarPlugin(); - } - - @override - void initState() { - super.initState(); - _retrieveCalendarEvents(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - key: _scaffoldstate, - appBar: AppBar( - title: Text('${_calendar.name} events'), - actions: [_getDeleteButton()], - ), - body: (_calendarEvents.isNotEmpty || _isLoading) - ? Stack( - children: [ - ListView.builder( - itemCount: _calendarEvents.length, - itemBuilder: (BuildContext context, int index) { - return EventItem( - _calendarEvents[index], - _deviceCalendarPlugin, - _onLoading, - _onDeletedFinished, - _onTapped, - _calendar.isReadOnly != null && - _calendar.isReadOnly as bool); - }, - ), - if (_isLoading) - const Center( - child: CircularProgressIndicator(), - ) - ], - ) - : const Center(child: Text('No events found')), - floatingActionButton: _getAddEventButton(context)); - } - - Widget? _getAddEventButton(BuildContext context) { - if (_calendar.isReadOnly == false || _calendar.isReadOnly == null) { - return FloatingActionButton( - key: const Key('addEventButton'), - onPressed: () async { - final refreshEvents = await Navigator.push(context, - MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventPage(_calendar); - })); - if (refreshEvents == true) { - await _retrieveCalendarEvents(); - } - }, - child: const Icon(Icons.add), - ); - } else { - return null; - } - } - - void _onLoading() { - setState(() { - _isLoading = true; - }); - } - - Future _onDeletedFinished(bool deleteSucceeded) async { - if (deleteSucceeded) { - await _retrieveCalendarEvents(); - } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Oops, we ran into an issue deleting the event'), - backgroundColor: Colors.red, - duration: Duration(seconds: 5), - )); - setState(() { - _isLoading = false; - }); - } - } - - Future _onTapped(Event event) async { - final refreshEvents = await Navigator.push(context, - MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventPage( - _calendar, - event, - RecurringEventDialog( - _deviceCalendarPlugin, - event, - _onLoading, - _onDeletedFinished, - ), - ); - })); - if (refreshEvents != null && refreshEvents) { - await _retrieveCalendarEvents(); - } - } - - Future _retrieveCalendarEvents() async { - final startDate = DateTime.now().add(const Duration(days: -30)); - final endDate = DateTime.now().add(const Duration(days: 365 * 10)); - var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( - _calendar.id, - RetrieveEventsParams(startDate: startDate, endDate: endDate)); - setState(() { - _calendarEvents = calendarEventsResult.data ?? []; - _isLoading = false; - }); - } - - Widget _getDeleteButton() { - return IconButton( - icon: const Icon(Icons.delete), - onPressed: () async { - await _showDeleteDialog(); - }); - } - - Future _showDeleteDialog() async { - return showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Warning'), - content: SingleChildScrollView( - child: ListBody( - children: const [ - Text('This will delete this calendar'), - Text('Are you sure?'), - ], - ), - ), - actions: [ - TextButton( - onPressed: () async { - var returnValue = - await _deviceCalendarPlugin.deleteCalendar(_calendar.id!); - debugPrint( - 'returnValue: ${returnValue.data}, ${returnValue.errors}'); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - }, - child: const Text('Delete!'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - ], - ); - }, - ); - } -} diff --git a/device_calendar/example/lib/presentation/pages/calendars.dart b/device_calendar/example/lib/presentation/pages/calendars.dart deleted file mode 100644 index 71c47ea5..00000000 --- a/device_calendar/example/lib/presentation/pages/calendars.dart +++ /dev/null @@ -1,161 +0,0 @@ -import 'package:device_calendar/device_calendar.dart'; -import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'calendar_events.dart'; - -class CalendarsPage extends StatefulWidget { - const CalendarsPage({Key? key}) : super(key: key); - - @override - _CalendarsPageState createState() { - return _CalendarsPageState(); - } -} - -class _CalendarsPageState extends State { - late DeviceCalendarPlugin _deviceCalendarPlugin; - List _calendars = []; - List get _writableCalendars => - _calendars.where((c) => c.isReadOnly == false).toList(); - - List get _readOnlyCalendars => - _calendars.where((c) => c.isReadOnly == true).toList(); - - _CalendarsPageState() { - _deviceCalendarPlugin = DeviceCalendarPlugin(); - } - - @override - void initState() { - super.initState(); - _retrieveCalendars(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Calendars'), - actions: [_getRefreshButton()], - ), - body: Column( - children: [ - Padding( - 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, - ), - ), - Expanded( - flex: 1, - child: ListView.builder( - 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)}'), - onTap: () async { - await Navigator.push(context, - MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventsPage(_calendars[index], - key: const Key('calendarEventsPage')); - })); - }, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - children: [ - Expanded( - 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, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color(_calendars[index].color!)), - ), - 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: const Text('Default'), - ), - Icon(_calendars[index].isReadOnly == true - ? Icons.lock - : Icons.lock_open) - ], - ), - ), - ); - }, - ), - ), - ], - ), - floatingActionButton: FloatingActionButton( - onPressed: () async { - final createCalendar = await Navigator.push(context, - MaterialPageRoute(builder: (BuildContext context) { - return const CalendarAddPage(); - })); - - if (createCalendar == true) { - _retrieveCalendars(); - } - }, - child: const Icon(Icons.add), - ), - ); - } - - void _retrieveCalendars() async { - try { - var permissionsGranted = await _deviceCalendarPlugin.hasPermissions(); - if (permissionsGranted.isSuccess && - (permissionsGranted.data == null || - permissionsGranted.data == false)) { - permissionsGranted = await _deviceCalendarPlugin.requestPermissions(); - if (!permissionsGranted.isSuccess || - permissionsGranted.data == null || - permissionsGranted.data == false) { - return; - } - } - - final calendarsResult = await _deviceCalendarPlugin.retrieveCalendars(); - setState(() { - _calendars = calendarsResult.data as List; - }); - } on PlatformException catch (e, s) { - debugPrint('RETRIEVE_CALENDARS: $e, $s'); - } - } - - Widget _getRefreshButton() { - return IconButton( - icon: const Icon(Icons.refresh), - onPressed: () async { - _retrieveCalendars(); - }); - } -} diff --git a/device_calendar/example/lib/presentation/pages/event_attendee.dart b/device_calendar/example/lib/presentation/pages/event_attendee.dart deleted file mode 100644 index 2fff734e..00000000 --- a/device_calendar/example/lib/presentation/pages/event_attendee.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'dart:io'; - -import 'package:device_calendar/device_calendar.dart'; -import 'package:device_calendar_example/common/app_routes.dart'; -import 'package:flutter/material.dart'; - -late DeviceCalendarPlugin _deviceCalendarPlugin; - -class EventAttendeePage extends StatefulWidget { - final Attendee? attendee; - final String? eventId; - const EventAttendeePage({Key? key, this.attendee, this.eventId}) - : super(key: key); - - @override - _EventAttendeePageState createState() => - _EventAttendeePageState(attendee, eventId ?? ''); -} - -class _EventAttendeePageState extends State { - Attendee? _attendee; - final _formKey = GlobalKey(); - final _nameController = TextEditingController(); - final _emailAddressController = TextEditingController(); - var _role = AttendeeRole.None; - var _status = AndroidAttendanceStatus.None; - String _eventId = ''; - - _EventAttendeePageState(Attendee? attendee, eventId) { - if (attendee != null) { - _attendee = attendee; - _nameController.text = _attendee!.name!; - _emailAddressController.text = _attendee!.emailAddress!; - _role = _attendee!.role!; - _status = _attendee!.androidAttendeeDetails?.attendanceStatus ?? - AndroidAttendanceStatus.None; - } - _eventId = eventId; - } - - @override - void dispose() { - _nameController.dispose(); - _emailAddressController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(_attendee != null - ? 'Edit attendee ${_attendee!.name}' - : 'Add an Attendee'), - ), - body: Column( - children: [ - Form( - key: _formKey, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - controller: _nameController, - validator: (value) { - if (_attendee?.isCurrentUser == false && - (value == null || value.isEmpty)) { - return 'Please enter a name'; - } - return null; - }, - decoration: const InputDecoration(labelText: 'Name'), - ), - ), - Padding( - padding: const EdgeInsets.all(10.0), - child: TextFormField( - controller: _emailAddressController, - validator: (value) { - if (value == null || - value.isEmpty || - !value.contains('@')) { - return 'Please enter a valid email address'; - } - return null; - }, - decoration: - const InputDecoration(labelText: 'Email Address'), - ), - ), - ListTile( - leading: const Text('Role'), - trailing: DropdownButton( - onChanged: (value) { - setState(() { - _role = value as AttendeeRole; - }); - }, - value: _role, - items: AttendeeRole.values - .map((role) => DropdownMenuItem( - value: role, - child: Text(role.enumToString), - )) - .toList(), - ), - ), - Visibility( - visible: Platform.isIOS, - child: ListTile( - onTap: () async { - _deviceCalendarPlugin = DeviceCalendarPlugin(); - - await _deviceCalendarPlugin - .showiOSEventModal(_eventId); - Navigator.popUntil( - context, ModalRoute.withName(AppRoutes.calendars)); - //TODO: finish calling and getting attendee details from iOS - }, - leading: const Icon(Icons.edit), - title: const Text('View / edit iOS attendance details'), - ), - ), - Visibility( - visible: Platform.isAndroid, - child: ListTile( - leading: const Text('Android attendee status'), - trailing: DropdownButton( - onChanged: (value) { - setState(() { - _status = value as AndroidAttendanceStatus; - }); - }, - value: _status, - items: AndroidAttendanceStatus.values - .map((status) => DropdownMenuItem( - value: status, - child: Text(status.enumToString), - )) - .toList(), - ), - ), - ) - ], - ), - ), - ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - setState(() { - _attendee = Attendee( - name: _nameController.text, - emailAddress: _emailAddressController.text, - role: _role, - isOrganiser: _attendee?.isOrganiser ?? false, - isCurrentUser: _attendee?.isCurrentUser ?? false, - iosAttendeeDetails: _attendee?.iosAttendeeDetails, - androidAttendeeDetails: AndroidAttendeeDetails.fromJson( - {'attendanceStatus': _status.index})); - - _emailAddressController.clear(); - }); - - Navigator.pop(context, _attendee); - } - }, - child: Text(_attendee != null ? 'Update' : 'Add'), - ) - ], - ), - ); - } -} diff --git a/device_calendar/example/lib/presentation/pages/event_reminders.dart b/device_calendar/example/lib/presentation/pages/event_reminders.dart deleted file mode 100644 index 4b0a11f3..00000000 --- a/device_calendar/example/lib/presentation/pages/event_reminders.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:device_calendar/device_calendar.dart'; - -class EventRemindersPage extends StatefulWidget { - final List _reminders; - const EventRemindersPage(this._reminders, {Key? key}) : super(key: key); - - @override - _EventRemindersPageState createState() => - _EventRemindersPageState(_reminders); -} - -class _EventRemindersPageState extends State { - List _reminders = []; - final _formKey = GlobalKey(); - final _minutesController = TextEditingController(); - - _EventRemindersPageState(List reminders) { - _reminders = [...reminders]; - } - - @override - void dispose() { - _minutesController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Reminders'), - ), - body: Column( - children: [ - Form( - key: _formKey, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Row( - children: [ - Expanded( - child: TextFormField( - controller: _minutesController, - validator: (value) { - if (value == null || - value.isEmpty || - int.tryParse(value) == null) { - return 'Please enter a reminder time in minutes'; - } - return null; - }, - decoration: const InputDecoration( - labelText: 'Minutes before start'), - ), - ), - ElevatedButton( - onPressed: () { - if (_formKey.currentState!.validate()) { - setState(() { - _reminders.add(Reminder( - minutes: int.parse(_minutesController.text))); - _minutesController.clear(); - }); - } - }, - child: const Text('Add'), - ), - ], - ), - ), - ), - Expanded( - child: ListView.builder( - itemCount: _reminders.length, - itemBuilder: (context, index) { - return ListTile( - title: Text('${_reminders[index].minutes} minutes'), - trailing: ElevatedButton( - onPressed: () { - setState(() { - _reminders.removeWhere( - (a) => a.minutes == _reminders[index].minutes); - }); - }, - child: const Text('Delete'), - ), - ); - }, - ), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(context, _reminders); - }, - child: const Text('Done'), - ) - ], - ), - ); - } -} diff --git a/device_calendar/example/lib/presentation/recurring_event_dialog.dart b/device_calendar/example/lib/presentation/recurring_event_dialog.dart deleted file mode 100644 index c8b8ff35..00000000 --- a/device_calendar/example/lib/presentation/recurring_event_dialog.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:device_calendar/device_calendar.dart'; - -class RecurringEventDialog extends StatefulWidget { - final DeviceCalendarPlugin _deviceCalendarPlugin; - final Event _calendarEvent; - - final VoidCallback _onLoadingStarted; - final Function(bool) _onDeleteFinished; - - const RecurringEventDialog(this._deviceCalendarPlugin, this._calendarEvent, - this._onLoadingStarted, this._onDeleteFinished, - {Key? key}) - : super(key: key); - - @override - _RecurringEventDialogState createState() => - _RecurringEventDialogState(_deviceCalendarPlugin, _calendarEvent, - onLoadingStarted: _onLoadingStarted, - onDeleteFinished: _onDeleteFinished); -} - -class _RecurringEventDialogState extends State { - late DeviceCalendarPlugin _deviceCalendarPlugin; - late Event _calendarEvent; - VoidCallback? _onLoadingStarted; - Function(bool)? _onDeleteFinished; - - _RecurringEventDialogState( - DeviceCalendarPlugin deviceCalendarPlugin, Event calendarEvent, - {VoidCallback? onLoadingStarted, Function(bool)? onDeleteFinished}) { - _deviceCalendarPlugin = deviceCalendarPlugin; - _calendarEvent = calendarEvent; - _onLoadingStarted = onLoadingStarted; - _onDeleteFinished = onDeleteFinished; - } - - @override - Widget build(BuildContext context) { - return SimpleDialog( - title: const Text('Are you sure you want to delete this event?'), - children: [ - SimpleDialogOption( - onPressed: () async { - Navigator.of(context).pop(true); - if (_onLoadingStarted != null) _onLoadingStarted!(); - final deleteResult = - await _deviceCalendarPlugin.deleteEventInstance( - _calendarEvent.calendarId, - _calendarEvent.eventId, - _calendarEvent.start?.millisecondsSinceEpoch, - _calendarEvent.end?.millisecondsSinceEpoch, - false); - if (_onDeleteFinished != null) { - _onDeleteFinished!( - deleteResult.isSuccess && deleteResult.data != null); - } - }, - child: const Text('This instance only'), - ), - SimpleDialogOption( - onPressed: () async { - Navigator.of(context).pop(true); - if (_onLoadingStarted != null) _onLoadingStarted!(); - final deleteResult = - await _deviceCalendarPlugin.deleteEventInstance( - _calendarEvent.calendarId, - _calendarEvent.eventId, - _calendarEvent.start?.millisecondsSinceEpoch, - _calendarEvent.end?.millisecondsSinceEpoch, - true); - if (_onDeleteFinished != null) { - _onDeleteFinished!( - deleteResult.isSuccess && deleteResult.data != null); - } - }, - child: const Text('This and following instances'), - ), - SimpleDialogOption( - onPressed: () async { - Navigator.of(context).pop(true); - if (_onLoadingStarted != null) _onLoadingStarted!(); - final deleteResult = await _deviceCalendarPlugin.deleteEvent( - _calendarEvent.calendarId, _calendarEvent.eventId); - if (_onDeleteFinished != null) { - _onDeleteFinished!( - deleteResult.isSuccess && deleteResult.data != null); - } - }, - child: const Text('All instances'), - ), - SimpleDialogOption( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: const Text('Cancel'), - ) - ], - ); - } -} diff --git a/device_calendar/example/pubspec.yaml b/device_calendar/example/pubspec.yaml deleted file mode 100644 index b32d2fb0..00000000 --- a/device_calendar/example/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -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" - flutter: ">=1.20.0" - -dependencies: - flutter: - sdk: flutter - intl: ^0.17.0 - uuid: ^3.0.6 - flutter_native_timezone: ^2.0.0 - device_calendar: - path: ../ - -dev_dependencies: - integration_test: - sdk: flutter - flutter_test: - sdk: flutter - flutter_lints: ^2.0.1 - -flutter: - uses-material-design: true \ No newline at end of file diff --git a/device_calendar/ios/.gitignore b/device_calendar/ios/.gitignore deleted file mode 100644 index 710ec6cf..00000000 --- a/device_calendar/ios/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig diff --git a/device_calendar/ios/Assets/.gitkeep b/device_calendar/ios/Assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/device_calendar/ios/Classes/DeviceCalendarPlugin.h b/device_calendar/ios/Classes/DeviceCalendarPlugin.h deleted file mode 100644 index 0d5ad0b9..00000000 --- a/device_calendar/ios/Classes/DeviceCalendarPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface DeviceCalendarPlugin : NSObject -@end diff --git a/device_calendar/ios/Classes/DeviceCalendarPlugin.m b/device_calendar/ios/Classes/DeviceCalendarPlugin.m deleted file mode 100644 index 774b46d3..00000000 --- a/device_calendar/ios/Classes/DeviceCalendarPlugin.m +++ /dev/null @@ -1,8 +0,0 @@ -#import "DeviceCalendarPlugin.h" -#import - -@implementation DeviceCalendarPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - [SwiftDeviceCalendarPlugin registerWithRegistrar:registrar]; -} -@end diff --git a/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift b/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift deleted file mode 100644 index f37d1a5a..00000000 --- a/device_calendar/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ /dev/null @@ -1,1136 +0,0 @@ -import EventKit -import EventKitUI -import Flutter -import Foundation -import UIKit - -extension Date { - var millisecondsSinceEpoch: Double { return self.timeIntervalSince1970 * 1000.0 } -} - -extension EKParticipant { - var emailAddress: String? { - return self.value(forKey: "emailAddress") as? String - } -} - -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" } - - if (!iCloudSources.isEmpty) { - return iCloudSources.first - } - - return nil - } - - 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 - } - - 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)) - } - } - - 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 { - 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"; - } - } - - 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 { - 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(fourYearsTimeInterval) - while currentEndDate <= endDate { - 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 [fourYearsTimeInterval] - currentStartDate = currentEndDate - currentEndDate = currentStartDate.addingTimeInterval(fourYearsTimeInterval) - } - - // If the cycle doesn't end exactly on the end date - if currentStartDate <= endDate { - 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) - } - } - } - - 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 { - for ekParticipant in ekEvent.attendees! { - let attendee = convertEkParticipantToAttendee(ekParticipant: ekParticipant) - 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, - calendarId: calendarId, - eventTitle: ekEvent.title ?? "New Event", - eventDescription: ekEvent.notes, - eventStartDate: Int64(ekEvent.startDate.millisecondsSinceEpoch), - eventEndDate: Int64(ekEvent.endDate.millisecondsSinceEpoch), - eventStartTimeZone: ekEvent.timeZone?.identifier, - eventAllDay: ekEvent.isAllDay, - attendees: attendees, - eventLocation: ekEvent.location, - eventURL: ekEvent.url?.absoluteString, - recurrenceRule: recurrenceRule, - organizer: convertEkParticipantToAttendee(ekParticipant: ekEvent.organizer), - reminders: reminders, - availability: convertEkEventAvailability(ekEventAvailability: ekEvent.availability), - eventStatus: convertEkEventStatus(ekEventStatus: ekEvent.status) - ) - - return event - } - - 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 - } - - private func convertEkEventAvailability(ekEventAvailability: EKEventAvailability?) -> Availability? { - switch ekEventAvailability { - case .busy: - return Availability.BUSY - case .free: - return Availability.FREE - case .tentative: - return Availability.TENTATIVE - case .unavailable: - return Availability.UNAVAILABLE - default: - return nil - } - } - - private func convertEkEventStatus(ekEventStatus: EKEventStatus?) -> EventStatus? { - switch ekEventStatus { - case .confirmed: - return EventStatus.CONFIRMED - case .tentative: - return EventStatus.TENTATIVE - case .canceled: - return EventStatus.CANCELED - case .none?: - return EventStatus.NONE - default: - return nil - } - } - - private func parseEKRecurrenceRules(_ ekEvent: EKEvent) -> RecurrenceRule? { - var recurrenceRule: RecurrenceRule? - if ekEvent.hasRecurrenceRules { - let ekRecurrenceRule = ekEvent.recurrenceRules![0] - var frequency: String - switch ekRecurrenceRule.frequency { - case EKRecurrenceFrequency.daily: - frequency = "DAILY" - case EKRecurrenceFrequency.weekly: - frequency = "WEEKLY" - case EKRecurrenceFrequency.monthly: - frequency = "MONTHLY" - case EKRecurrenceFrequency.yearly: - frequency = "YEARLY" - default: - frequency = "DAILY" - } - - var count: Int? - var endDate: String? - if(ekRecurrenceRule.recurrenceEnd?.occurrenceCount != nil && ekRecurrenceRule.recurrenceEnd?.occurrenceCount != 0) { - count = ekRecurrenceRule.recurrenceEnd?.occurrenceCount - } - - let endDateRaw = ekRecurrenceRule.recurrenceEnd?.endDate - if(endDateRaw != nil) { - endDate = formateDateTime(dateTime: endDateRaw!) - } - - 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( - freq: frequency, - count: count, - interval: ekRecurrenceRule.interval, - 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 recurrenceFrequency = recurrenceRuleArguments![recurrenceFrequencyArgument] as? String - let totalOccurrences = recurrenceRuleArguments![countArgument] as? NSInteger - let interval = recurrenceRuleArguments![intervalArgument] as? NSInteger - var recurrenceInterval = 1 - 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 { - 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 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!)} - } - } - - 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: 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 - } - if existingAttendee != nil && ekEvent!.organizer?.emailAddress != existingAttendee?.emailAddress{ - attendees.append(existingAttendee!) - 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 - } - - switch availabilityValue.uppercased() { - case Availability.BUSY.rawValue: - return .busy - case Availability.FREE.rawValue: - return .free - 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 - let calendarId = arguments[calendarIdArgument] as! String - let eventId = arguments[eventIdArgument] as? String - let isAllDay = arguments[eventAllDayArgument] as! Bool - let startDateMillisecondsSinceEpoch = arguments[eventStartDateArgument] as! NSNumber - let endDateDateMillisecondsSinceEpoch = arguments[eventEndDateArgument] as! NSNumber - let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch.doubleValue / 1000.0) - let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch.doubleValue / 1000.0) - let startTimeZoneString = arguments[eventStartTimeZoneArgument] as? String - let title = arguments[self.eventTitleArgument] as? String - let description = arguments[self.eventDescriptionArgument] as? String - let location = arguments[self.eventLocationArgument] as? String - let url = arguments[self.eventURLArgument] 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 - } - - var ekEvent: EKEvent? - if eventId == nil { - ekEvent = EKEvent.init(eventStore: self.eventStore) - } else { - ekEvent = self.eventStore.event(withIdentifier: eventId!) - if(ekEvent == nil) { - self.finishWithEventNotFoundError(result: result, eventId: eventId!) - return - } - } - - ekEvent!.title = title ?? "" - ekEvent!.notes = description - ekEvent!.isAllDay = isAllDay - ekEvent!.startDate = startDate - ekEvent!.endDate = endDate - - if (!isAllDay) { - let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current - ekEvent!.timeZone = timeZone - } - - ekEvent!.calendar = ekCalendar! - ekEvent!.location = location - - // Create and add URL object only when if the input string is not empty or nil - if let urlCheck = url, !urlCheck.isEmpty { - let iosUrl = URL(string: url ?? "") - ekEvent!.url = iosUrl - } - 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) - } catch { - self.eventStore.reset() - result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) - } - }, result: result) - } - - private func createParticipant(name: String, emailAddress: String, role: Int) -> EKParticipant? { - 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") - return participant as? EKParticipant - } - return nil - } - - private func deleteEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - let arguments = call.arguments as! Dictionary - let calendarId = arguments[calendarIdArgument] as! String - let eventId = arguments[eventIdArgument] as! String - 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) - } catch { - self.eventStore.reset() - result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) - } - } - 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) - } - else { - try self.eventStore.remove(ekEvent!, span: .futureEvents, commit: true) - } - - result(true) - } catch { - self.eventStore.reset() - result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) - } - } - }, 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 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() - let jsonData = try jsonEncoder.encode(codable) - let jsonString = String(data: jsonData, encoding: .utf8) - result(jsonString) - } catch { - result(FlutterError(code: genericError, message: error.localizedDescription, details: nil)) - } - } - - private func checkPermissionsThenExecute(permissionsGrantedAction: () -> Void, result: @escaping FlutterResult) { - if hasEventPermissions() { - permissionsGrantedAction() - return - } - self.finishWithUnauthorizedError(result: result) - } - - private func requestPermissions(_ completion: @escaping (Bool) -> Void) { - if hasEventPermissions() { - completion(true) - return - } - 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) - if #available(iOS 17, *) { - return status == EKAuthorizationStatus.fullAccess - } else { - return status == EKAuthorizationStatus.authorized - } - } -} - -extension Date { - func convert(from initTimeZone: TimeZone, to targetTimeZone: TimeZone) -> Date { - let delta = TimeInterval(initTimeZone.secondsFromGMT() - targetTimeZone.secondsFromGMT()) - return addingTimeInterval(delta) - } -} - -extension UIColor { - func rgb() -> Int? { - var fRed : CGFloat = 0 - var fGreen : CGFloat = 0 - var fBlue : CGFloat = 0 - var fAlpha: CGFloat = 0 - if self.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha) { - let iRed = Int(fRed * 255.0) - let iGreen = Int(fGreen * 255.0) - let iBlue = Int(fBlue * 255.0) - let iAlpha = Int(fAlpha * 255.0) - - // (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue). - let rgb = (iAlpha << 24) + (iRed << 16) + (iGreen << 8) + iBlue - return rgb - } else { - // Could not extract RGBA components: - return nil - } - } - - public convenience init?(hex: String) { - let r, g, b, a: CGFloat - - if hex.hasPrefix("0x") { - let start = hex.index(hex.startIndex, offsetBy: 2) - let hexColor = String(hex[start...]) - - if hexColor.count == 8 { - let scanner = Scanner(string: hexColor) - var hexNumber: UInt64 = 0 - - if scanner.scanHexInt64(&hexNumber) { - a = CGFloat((hexNumber & 0xff000000) >> 24) / 255 - 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 - } - } - } - - return nil - } - -} diff --git a/device_calendar/ios/device_calendar.podspec b/device_calendar/ios/device_calendar.podspec deleted file mode 100644 index 3230bcf3..00000000 --- a/device_calendar/ios/device_calendar.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'device_calendar' - s.version = '0.0.1' - s.summary = 'A new flutter plugin project.' - s.description = <<-DESC -A new flutter plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - s.swift_version = '5.0' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end - diff --git a/device_calendar/lib/device_calendar.dart b/device_calendar/lib/device_calendar.dart deleted file mode 100644 index 3566d5df..00000000 --- a/device_calendar/lib/device_calendar.dart +++ /dev/null @@ -1,17 +0,0 @@ -library device_calendar; - -export 'src/common/calendar_enums.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 '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'; -export 'src/models/platform_specifics/android/attendance_status.dart'; -export 'src/device_calendar.dart'; -export 'package:timezone/timezone.dart'; diff --git a/device_calendar/lib/src/common/calendar_enums.dart b/device_calendar/lib/src/common/calendar_enums.dart deleted file mode 100644 index aa77ec02..00000000 --- a/device_calendar/lib/src/common/calendar_enums.dart +++ /dev/null @@ -1,315 +0,0 @@ -enum DayOfWeek { - Monday, - Tuesday, - Wednesday, - Thursday, - Friday, - Saturday, - Sunday, -} - -enum DayOfWeekGroup { - None, - Weekday, - Weekend, - AllDays, -} - -enum MonthOfYear { - January, - Feburary, - March, - April, - May, - June, - July, - August, - September, - October, - November, - December, -} - -enum WeekNumber { - First, - Second, - Third, - Fourth, - Last, -} - -enum AttendeeRole { - None, - Required, - Optional, - Resource, -} - -enum Availability { - Free, - Busy, - Tentative, - Unavailable, -} - -enum EventStatus { - None, - Confirmed, - Canceled, - Tentative, -} - -extension DayOfWeekExtension on DayOfWeek { - static int _value(DayOfWeek val) { - switch (val) { - case DayOfWeek.Monday: - return 1; - case DayOfWeek.Tuesday: - return 2; - case DayOfWeek.Wednesday: - return 3; - case DayOfWeek.Thursday: - return 4; - case DayOfWeek.Friday: - return 5; - case DayOfWeek.Saturday: - return 6; - case DayOfWeek.Sunday: - return 0; - default: - return 1; - } - } - - String _enumToString(DayOfWeek enumValue) { - return enumValue.toString().split('.').last; - } - - int get value => _value(this); - - String get enumToString => _enumToString(this); -} - -extension DaysOfWeekGroupExtension on DayOfWeekGroup { - static List _getDays(DayOfWeekGroup val) { - switch (val) { - case DayOfWeekGroup.Weekday: - return [ - DayOfWeek.Monday, - DayOfWeek.Tuesday, - DayOfWeek.Wednesday, - DayOfWeek.Thursday, - DayOfWeek.Friday - ]; - case DayOfWeekGroup.Weekend: - return [DayOfWeek.Saturday, DayOfWeek.Sunday]; - case DayOfWeekGroup.AllDays: - return [ - DayOfWeek.Monday, - DayOfWeek.Tuesday, - DayOfWeek.Wednesday, - DayOfWeek.Thursday, - DayOfWeek.Friday, - DayOfWeek.Saturday, - DayOfWeek.Sunday - ]; - default: - return []; - } - } - - String _enumToString(DayOfWeekGroup enumValue) { - return enumValue.toString().split('.').last; - } - - List get getDays => _getDays(this); - - String get enumToString => _enumToString(this); -} - -extension MonthOfYearExtension on MonthOfYear { - static int _value(MonthOfYear val) { - switch (val) { - case MonthOfYear.January: - return 1; - case MonthOfYear.Feburary: - return 2; - case MonthOfYear.March: - return 3; - case MonthOfYear.April: - return 4; - case MonthOfYear.May: - return 5; - case MonthOfYear.June: - return 6; - case MonthOfYear.July: - return 7; - case MonthOfYear.August: - return 8; - case MonthOfYear.September: - return 9; - case MonthOfYear.October: - return 10; - case MonthOfYear.November: - return 11; - case MonthOfYear.December: - return 12; - default: - return 1; - } - } - - String _enumToString(MonthOfYear enumValue) { - return enumValue.toString().split('.').last; - } - - int get value => _value(this); - - String get enumToString => _enumToString(this); -} - -extension WeekNumberExtension on WeekNumber { - static int _value(WeekNumber val) { - switch (val) { - case WeekNumber.First: - return 1; - case WeekNumber.Second: - return 2; - case WeekNumber.Third: - return 3; - case WeekNumber.Fourth: - return 4; - case WeekNumber.Last: - return -1; - default: - return 1; - } - } - - String _enumToString(WeekNumber enumValue) { - return enumValue.toString().split('.').last; - } - - int get value => _value(this); - - String get enumToString => _enumToString(this); -} - -extension IntExtensions on int { - static DayOfWeek _getDayOfWeekEnumValue(int val) { - switch (val) { - case 1: - return DayOfWeek.Monday; - case 2: - return DayOfWeek.Tuesday; - case 3: - return DayOfWeek.Wednesday; - case 4: - return DayOfWeek.Thursday; - case 5: - return DayOfWeek.Friday; - case 6: - return DayOfWeek.Saturday; - case 0: - return DayOfWeek.Sunday; - default: - return DayOfWeek.Monday; - } - } - - static MonthOfYear _getMonthOfYearEnumValue(int val) { - switch (val) { - case 1: - return MonthOfYear.January; - case 2: - return MonthOfYear.Feburary; - case 3: - return MonthOfYear.March; - case 4: - return MonthOfYear.April; - case 5: - return MonthOfYear.May; - case 6: - return MonthOfYear.June; - case 7: - return MonthOfYear.July; - case 8: - return MonthOfYear.August; - case 9: - return MonthOfYear.September; - case 10: - return MonthOfYear.October; - case 11: - return MonthOfYear.November; - case 12: - return MonthOfYear.December; - default: - return MonthOfYear.January; - } - } - - static WeekNumber _getWeekNumberEnumValue(int val) { - switch (val) { - case 1: - return WeekNumber.First; - case 2: - return WeekNumber.Second; - case 3: - return WeekNumber.Third; - case 4: - return WeekNumber.Fourth; - case -1: - return WeekNumber.Last; - default: - return WeekNumber.First; - } - } - - DayOfWeek get getDayOfWeekEnumValue => _getDayOfWeekEnumValue(this); - - MonthOfYear get getMonthOfYearEnumValue => _getMonthOfYearEnumValue(this); - - WeekNumber get getWeekNumberEnumValue => _getWeekNumberEnumValue(this); -} - -extension RoleExtensions on AttendeeRole { - String _enumToString(AttendeeRole enumValue) { - return enumValue.toString().split('.').last; - } - - String get enumToString => _enumToString(this); -} - -extension AvailabilityExtensions on Availability { - String _enumToString(Availability enumValue) { - switch (enumValue) { - case Availability.Busy: - return 'BUSY'; - case Availability.Free: - return 'FREE'; - case Availability.Tentative: - return 'TENTATIVE'; - case Availability.Unavailable: - return 'UNAVAILABLE'; - } - } - - String get enumToString => _enumToString(this); -} - -extension EventStatusExtensions on EventStatus { - String _enumToString(EventStatus enumValue) { - switch (enumValue) { - case EventStatus.Confirmed: - return 'CONFIRMED'; - case EventStatus.Tentative: - return 'TENTATIVE'; - case EventStatus.Canceled: - return 'CANCELED'; - case EventStatus.None: - return 'NONE'; - } - } - - String get enumToString => _enumToString(this); -} diff --git a/device_calendar/lib/src/common/channel_constants.dart b/device_calendar/lib/src/common/channel_constants.dart deleted file mode 100644 index 2eef3d2d..00000000 --- a/device_calendar/lib/src/common/channel_constants.dart +++ /dev/null @@ -1,26 +0,0 @@ -class ChannelConstants { - static const String channelName = 'plugins.builttoroam.com/device_calendar'; - - static const String methodNameRequestPermissions = 'requestPermissions'; - static const String methodNameHasPermissions = 'hasPermissions'; - static const String methodNameRetrieveCalendars = 'retrieveCalendars'; - static const String methodNameRetrieveEvents = 'retrieveEvents'; - static const String methodNameDeleteEvent = 'deleteEvent'; - static const String methodNameDeleteEventInstance = 'deleteEventInstance'; - static const String methodNameCreateOrUpdateEvent = 'createOrUpdateEvent'; - static const String methodNameCreateCalendar = 'createCalendar'; - static const String methodNameDeleteCalendar = 'deleteCalendar'; - static const String methodNameShowiOSEventModal = 'showiOSEventModal'; - - static const String parameterNameCalendarId = 'calendarId'; - static const String parameterNameStartDate = 'startDate'; - static const String parameterNameEndDate = 'endDate'; - static const String parameterNameEventId = 'eventId'; - static const String parameterNameEventIds = 'eventIds'; - static const String parameterNameEventStartDate = 'eventStartDate'; - static const String parameterNameEventEndDate = 'eventEndDate'; - static const String parameterNameFollowingInstances = 'followingInstances'; - static const String parameterNameCalendarName = 'calendarName'; - static const String parameterNameCalendarColor = 'calendarColor'; - static const String parameterNameLocalAccountName = 'localAccountName'; -} diff --git a/device_calendar/lib/src/common/error_codes.dart b/device_calendar/lib/src/common/error_codes.dart deleted file mode 100644 index e75b29de..00000000 --- a/device_calendar/lib/src/common/error_codes.dart +++ /dev/null @@ -1,6 +0,0 @@ -class ErrorCodes { - static const int invalidArguments = 400; - static const int platformSpecific = 599; - static const int generic = 500; - static const int unknown = 502; -} diff --git a/device_calendar/lib/src/common/error_messages.dart b/device_calendar/lib/src/common/error_messages.dart deleted file mode 100644 index f2126006..00000000 --- a/device_calendar/lib/src/common/error_messages.dart +++ /dev/null @@ -1,27 +0,0 @@ -class ErrorMessages { - static const String fromJsonMapIsNull = 'The json object is null'; - - static const String invalidMissingCalendarId = - 'Calendar ID is missing or invalid'; - - static const String invalidRetrieveEventsParams = - 'A valid instance of the RetrieveEventsParams class is required. Must the event ids to filter by or the start and end date to filter by or a combination of these'; - static const String deleteEventInvalidArgumentsMessage = - 'Calendar ID and/or Event ID argument(s) have not been specified or are invalid'; - static const String createOrUpdateEventInvalidArgumentsMessageAllDay = - "To create or update an all day event you must provide calendar ID, event with a title and event's start date"; - static const String createOrUpdateEventInvalidArgumentsMessage = - "To create or update an event you must provide calendar ID, event with a title and event's start date and end date (where start date must be before end date)"; - static const String createCalendarInvalidCalendarNameMessage = - 'Calendar name must not be null or empty'; - - static const String invalidRecurrencyFrequency = - 'Invalid recurrency frequency'; - - 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.'; - static const String unknownDeviceGenericExceptionTemplate = - 'Device calendar plugin ran into an issue, with message "%s"'; -} diff --git a/device_calendar/lib/src/device_calendar.dart b/device_calendar/lib/src/device_calendar.dart deleted file mode 100644 index 4c1d12f1..00000000 --- a/device_calendar/lib/src/device_calendar.dart +++ /dev/null @@ -1,443 +0,0 @@ -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; - -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 { - static const MethodChannel channel = - MethodChannel(ChannelConstants.channelName); - - static final DeviceCalendarPlugin _instance = DeviceCalendarPlugin.private(); - - factory DeviceCalendarPlugin({bool shouldInitTimezone = true}) { - if (shouldInitTimezone) { - tz.initializeTimeZones(); - } - return _instance; - } - - @visibleForTesting - DeviceCalendarPlugin.private(); - - /// Requests permissions to modify the calendars on the device - /// - /// Returns a [Result] indicating if calendar READ and WRITE permissions - /// have (true) or have not (false) been granted - Future> requestPermissions() async { - return _invokeChannelMethod( - ChannelConstants.methodNameRequestPermissions, - ); - } - - /// Checks if permissions for modifying the device calendars have been granted - /// - /// Returns a [Result] indicating if calendar READ and WRITE permissions - /// have (true) or have not (false) been granted - Future> hasPermissions() async { - return _invokeChannelMethod( - ChannelConstants.methodNameHasPermissions, - ); - } - - /// Retrieves all of the device defined calendars - /// - /// Returns a [Result] containing a list of device [Calendar] - Future>> retrieveCalendars() async { - return _invokeChannelMethod( - ChannelConstants.methodNameRetrieveCalendars, - evaluateResponse: (rawData) => UnmodifiableListView( - json.decode(rawData).map( - (decodedCalendar) => Calendar.fromJson(decodedCalendar), - ), - ), - ); - } - - /// Retrieves the events from the specified calendar - /// - /// The `calendarId` paramter is the id of the calendar that plugin will return events for - /// The `retrieveEventsParams` parameter combines multiple properties that - /// specifies conditions of the events retrieval. For instance, defining [RetrieveEventsParams.startDate] - /// and [RetrieveEventsParams.endDate] will return events only happening in that time range - /// - /// Returns a [Result] containing a list [Event], that fall - /// into the specified parameters - Future>> retrieveEvents( - 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( - 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.\ - /// To delete individual instance of a recurring event, please use [deleteEventInstance()] - /// - /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\ - /// The `eventId` parameter is the id of the event that plugin will try to delete - /// - /// Returns a [Result] indicating if the event has (true) or has not (false) been deleted from the calendar - Future> deleteEvent( - String? calendarId, - String? eventId, - ) async { - return _invokeChannelMethod( - ChannelConstants.methodNameDeleteEvent, - assertParameters: (result) { - _validateCalendarIdParameter( - result, - calendarId, - ); - - _assertParameter( - result, - eventId?.isNotEmpty ?? false, - ErrorCodes.invalidArguments, - ErrorMessages.deleteEventInvalidArgumentsMessage, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarId: calendarId, - ChannelConstants.parameterNameEventId: eventId, - }, - ); - } - - /// Deletes an instance of a recurring event from a calendar. This should be used for a recurring event only.\ - /// If `startDate`, `endDate` or `deleteFollowingInstances` is not valid or null, then all instances of the event will be deleted. - /// - /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\ - /// The `eventId` parameter is the id of the event that plugin will try to delete\ - /// The `startDate` parameter is the start date of the instance to delete\ - /// The `endDate` parameter is the end date of the instance to delete\ - /// The `deleteFollowingInstances` parameter will also delete the following instances if set to true - /// - /// Returns a [Result] indicating if the instance of the event has (true) or has not (false) been deleted from the calendar - Future> deleteEventInstance( - String? calendarId, - String? eventId, - int? startDate, - int? endDate, - bool deleteFollowingInstances, - ) async { - return _invokeChannelMethod( - ChannelConstants.methodNameDeleteEventInstance, - assertParameters: (result) { - _validateCalendarIdParameter( - result, - calendarId, - ); - - _assertParameter( - result, - eventId?.isNotEmpty ?? false, - ErrorCodes.invalidArguments, - ErrorMessages.deleteEventInvalidArgumentsMessage, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarId: calendarId, - ChannelConstants.parameterNameEventId: eventId, - ChannelConstants.parameterNameEventStartDate: startDate, - ChannelConstants.parameterNameEventEndDate: endDate, - ChannelConstants.parameterNameFollowingInstances: - deleteFollowingInstances, - }, - ); - } - - /// Creates or updates an event - /// - /// The `event` paramter specifies how event data should be saved into the calendar - /// Always specify the [Event.calendarId], to inform the plugin in which calendar - /// it should create or update the event. - /// - /// Returns a [Result] with the newly created or updated [Event.eventId] - Future?> createOrUpdateEvent(Event? event) async { - if (event == null) return null; - return _invokeChannelMethod( - ChannelConstants.methodNameCreateOrUpdateEvent, - assertParameters: (result) { - // Setting time to 0 for all day events - if (event.allDay == true) { - if (event.start != null) { - var dateStart = DateTime(event.start!.year, event.start!.month, - event.start!.day, 0, 0, 0); - // 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) - : TZDateTime.from(dateStart, - timeZoneDatabase.locations[event.start!.location.name]!); - } - if (event.end != null) { - var dateEnd = DateTime( - event.end!.year, event.end!.month, event.end!.day, 0, 0, 0); - // allDay events on Android need to be at midnight UTC on the - // day after the last day. For example, a 2-day allDay event on - // 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(const Duration(days: 1)) - : TZDateTime.from(dateEnd, - timeZoneDatabase.locations[event.end!.location.name]!); - } - } - - _assertParameter( - result, - !(event.allDay == true && (event.calendarId?.isEmpty ?? true) || - event.start == null || - event.end == null), - ErrorCodes.invalidArguments, - ErrorMessages.createOrUpdateEventInvalidArgumentsMessageAllDay, - ); - - _assertParameter( - result, - !(event.allDay != true && - ((event.calendarId?.isEmpty ?? true) || - event.start == null || - event.end == null || - (event.start != null && - event.end != null && - event.start!.isAfter(event.end!)))), - ErrorCodes.invalidArguments, - ErrorMessages.createOrUpdateEventInvalidArgumentsMessage, - ); - }, - arguments: () => event.toJson(), - ); - } - - /// Creates a new local calendar for the current device. - /// - /// The `calendarName` parameter is the name of the new calendar\ - /// The `calendarColor` parameter is the color of the calendar. If null, - /// a default color (red) will be used\ - /// The `localAccountName` parameter is the name of the local account: - /// - [Android] Required. If `localAccountName` parameter is null or empty, it will default to 'Device Calendar'. - /// If the account name already exists in the device, it will add another calendar under the account, - /// otherwise a new local account and a new calendar will be created. - /// - [iOS] Not used. A local account will be picked up automatically, if not found, an error will be thrown. - /// - /// Returns a [Result] with the newly created [Calendar.id] - Future> createCalendar( - String? calendarName, { - Color? calendarColor, - String? localAccountName, - }) async { - return _invokeChannelMethod( - ChannelConstants.methodNameCreateCalendar, - assertParameters: (result) { - calendarColor ??= Colors.red; - - _assertParameter( - result, - calendarName?.isNotEmpty == true, - ErrorCodes.invalidArguments, - ErrorMessages.createCalendarInvalidCalendarNameMessage, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarName: calendarName, - ChannelConstants.parameterNameCalendarColor: - '0x${calendarColor?.value.toRadixString(16)}', - ChannelConstants.parameterNameLocalAccountName: - localAccountName?.isEmpty ?? true - ? 'Device Calendar' - : localAccountName - }, - ); - } - - /// Deletes a calendar. - /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\/// - /// Returns a [Result] indicating if the instance of the calendar has (true) or has not (false) been deleted - Future> deleteCalendar( - String calendarId, - ) async { - return _invokeChannelMethod( - ChannelConstants.methodNameDeleteCalendar, - assertParameters: (result) { - _validateCalendarIdParameter( - result, - calendarId, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarId: calendarId, - }, - ); - } - - /// 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 - Future> showiOSEventModal( - String eventId, - ) { - return _invokeChannelMethod( - ChannelConstants.methodNameShowiOSEventModal, - arguments: () => { - ChannelConstants.parameterNameEventId: eventId, - }, - ); - } - - Future> _invokeChannelMethod( - String channelMethodName, { - Function(Result)? assertParameters, - Map Function()? arguments, - T Function(dynamic)? evaluateResponse, - }) async { - final result = Result(); - - try { - if (assertParameters != null) { - assertParameters(result); - if (result.hasErrors) { - return result; - } - } - - var rawData = await channel.invokeMethod( - channelMethodName, - arguments != null ? arguments() : null, - ); - - if (evaluateResponse != null) { - result.data = evaluateResponse(rawData); - } else { - result.data = rawData; - } - } 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; - } - - void _parsePlatformExceptionAndUpdateResult( - Exception? exception, Result result) { - if (exception == null) { - result.errors.add( - const ResultError( - ErrorCodes.unknown, - ErrorMessages.unknownDeviceIssue, - ), - ); - return; - } - - debugPrint('$exception'); - - if (exception is PlatformException) { - result.errors.add( - ResultError( - ErrorCodes.platformSpecific, - '${ErrorMessages.unknownDeviceExceptionTemplate}, Code: ${exception.code}, Exception: ${exception.message}', - ), - ); - } else { - result.errors.add( - ResultError( - ErrorCodes.generic, - '${ErrorMessages.unknownDeviceGenericExceptionTemplate} ${exception.toString}', - ), - ); - } - } - - void _assertParameter( - Result result, - bool predicate, - int errorCode, - String errorMessage, - ) { - if (result.data != null) { - debugPrint("RESULT of _assertParameter: ${result.data}"); - } - if (!predicate) { - result.errors.add( - ResultError(errorCode, errorMessage), - ); - } - } - - void _validateCalendarIdParameter( - Result result, - String? calendarId, - ) { - _assertParameter( - result, - calendarId?.isNotEmpty ?? false, - ErrorCodes.invalidArguments, - ErrorMessages.invalidMissingCalendarId, - ); - } -} diff --git a/device_calendar/lib/src/models/attendee.dart b/device_calendar/lib/src/models/attendee.dart deleted file mode 100644 index 2e8e4cb9..00000000 --- a/device_calendar/lib/src/models/attendee.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'dart:io' show Platform; - -import '../common/calendar_enums.dart'; -import '../common/error_messages.dart'; -import 'platform_specifics/android/attendee_details.dart'; -import 'platform_specifics/ios/attendee_details.dart'; - -/// A person attending an event -class Attendee { - /// The name of the attendee - String? name; - - /// The email address of the attendee - String? emailAddress; - - /// An attendee role: None, Optional, Required or Resource - AttendeeRole? role; - - /// Read-only. Returns true if the attendee is an organiser, else false - bool isOrganiser = false; - - /// Read-only. Returns true if the attendee is the current user, else false - bool isCurrentUser = false; - - /// Details about the attendee that are specific to iOS. - /// When reading details for an existing event, this will only be populated on iOS devices. - IosAttendeeDetails? iosAttendeeDetails; - - /// Details about the attendee that are specific to Android. - /// When reading details for an existing event, this will only be populated on Android devices. - AndroidAttendeeDetails? androidAttendeeDetails; - - Attendee({ - this.name, - this.emailAddress, - this.role, - this.isOrganiser = false, - this.isCurrentUser = false, - this.iosAttendeeDetails, - this.androidAttendeeDetails, - }); - - Attendee.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - - name = json['name']; - emailAddress = json['emailAddress']; - role = AttendeeRole.values[json['role'] ?? 0]; - isOrganiser = json['isOrganizer'] ?? - false; // Getting and setting an organiser for Android - isCurrentUser = json['isCurrentUser'] ?? false; - - if (Platform.isAndroid) { - androidAttendeeDetails = AndroidAttendeeDetails.fromJson(json); - } - - if (Platform.isIOS) { - iosAttendeeDetails = IosAttendeeDetails.fromJson(json); - } - } - - Map toJson() { - final data = { - 'name': name, - 'emailAddress': emailAddress, - 'role': role?.index, - 'isOrganizer': isOrganiser, - }; - - if (iosAttendeeDetails != null) { - data.addEntries(iosAttendeeDetails!.toJson().entries); - } - if (androidAttendeeDetails != null) { - data.addEntries(androidAttendeeDetails!.toJson().entries); - } - - return data; - } -} diff --git a/device_calendar/lib/src/models/calendar.dart b/device_calendar/lib/src/models/calendar.dart deleted file mode 100644 index c04ab405..00000000 --- a/device_calendar/lib/src/models/calendar.dart +++ /dev/null @@ -1,56 +0,0 @@ -/// A calendar on the user's device -class Calendar { - /// Read-only. The unique identifier for this calendar - String? id; - - /// The name of this calendar - String? name; - - /// Read-only. If the calendar is read-only - bool? isReadOnly; - - /// Read-only. If the calendar is the default - bool? isDefault; - - /// Read-only. Color of the calendar - int? color; - - // Read-only. Account name associated with the calendar - String? accountName; - - // Read-only. Account type associated with the calendar - String? accountType; - - Calendar( - {this.id, - this.name, - this.isReadOnly, - this.isDefault, - this.color, - this.accountName, - this.accountType}); - - Calendar.fromJson(Map json) { - id = json['id']; - name = json['name']; - isReadOnly = json['isReadOnly']; - isDefault = json['isDefault']; - color = json['color']; - accountName = json['accountName']; - accountType = json['accountType']; - } - - Map toJson() { - final data = { - 'id': id, - 'name': name, - 'isReadOnly': isReadOnly, - 'isDefault': isDefault, - 'color': color, - 'accountName': accountName, - 'accountType': accountType - }; - - return data; - } -} diff --git a/device_calendar/lib/src/models/event.dart b/device_calendar/lib/src/models/event.dart deleted file mode 100644 index 00ebfa52..00000000 --- a/device_calendar/lib/src/models/event.dart +++ /dev/null @@ -1,306 +0,0 @@ -import 'dart:io'; - -import 'package:collection/collection.dart'; - -import '../../device_calendar.dart'; -import '../common/error_messages.dart'; - -/// An event associated with a calendar -class Event { - /// Read-only. The unique identifier for this event. This is auto-generated when a new event is created - String? eventId; - - /// Read-only. The identifier of the calendar that this event is associated with - String? calendarId; - - /// The title of this event - String? title; - - /// The description for this event - String? description; - - /// Indicates when the event starts - TZDateTime? start; - - /// Indicates when the event ends - TZDateTime? end; - - /// Indicates if this is an all-day event - bool? allDay; - - /// The location of this event - String? location; - - /// An URL for this event - Uri? url; - - /// A list of attendees for this event - List? attendees; - - /// The recurrence rule for this event - RecurrenceRule? recurrenceRule; - - /// A list of reminders (by minutes) for this event - List? reminders; - - /// Indicates if this event counts as busy time, tentative, unavaiable or is still free time - late Availability availability; - - /// Indicates if this event is of confirmed, canceled, tentative or none status - EventStatus? status; - - //##### - /// Read-only. Color of the event - int? color; - - ///Note for development: - /// - ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. - ///Make sure all locations are updated if changes needed to be made. - ///Swift: - ///`ios/Classes/SwiftDeviceCalendarPlugin.swift` - ///Kotlin: - ///`android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt` - ///`android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt` - ///`android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt` - Event( - this.calendarId, { - this.eventId, - this.title, - this.start, - this.end, - this.description, - this.attendees, - this.recurrenceRule, - this.reminders, - this.availability = Availability.Busy, - this.location, - this.url, - this.allDay = false, - this.status, - this.color, - }); - - ///Get Event from JSON. - /// - ///Sample JSON: - ///{calendarId: 00, eventId: 0000, eventTitle: Sample Event, eventDescription: This is a sample event, eventStartDate: 1563719400000, eventStartTimeZone: Asia/Hong_Kong, eventEndDate: 1640532600000, eventEndTimeZone: Asia/Hong_Kong, eventAllDay: false, eventLocation: Yuenlong Station, eventURL: null, availability: BUSY, attendees: [{name: commonfolk, emailAddress: total.loss@hong.com, role: 1, isOrganizer: false, attendanceStatus: 3}], reminders: [{minutes: 39}]} - Event.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - String? foundUrl; - String? startLocationName; - String? endLocationName; - int? startTimestamp; - int? endTimestamp; - bool legacyJSON = false; - var legacyName = { - title: 'title', - description: 'description', - startTimestamp: 'start', - endTimestamp: 'end', - startLocationName: 'startTimeZone', - endLocationName: 'endTimeZone', - allDay: 'allDay', - location: 'location', - foundUrl: 'url', - }; - legacyName.forEach((key, value) { - if (json[value] != null) { - key = json[value]; - legacyJSON = true; - } - }); - - eventId = json['eventId']; - calendarId = json['calendarId']; - title = json['eventTitle']; - description = json['eventDescription']; - color = json['eventColor']; //##### - - startTimestamp = json['eventStartDate']; - startLocationName = json['eventStartTimeZone']; - var startTimeZone = timeZoneDatabase.locations[startLocationName]; - startTimeZone ??= local; - start = startTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(startTimeZone, startTimestamp) : TZDateTime.now(local); - - endTimestamp = json['eventEndDate']; - endLocationName = json['eventEndTimeZone']; - var endLocation = timeZoneDatabase.locations[endLocationName]; - endLocation ??= startTimeZone; - end = endTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(endLocation, endTimestamp) : TZDateTime.now(local); - allDay = json['eventAllDay'] ?? false; - if (Platform.isAndroid && (allDay ?? false)) { - // On Android, the datetime in an allDay event is adjusted to local - // timezone, which can result in the wrong day, so we need to bring the - // date back to midnight UTC to get the correct date - var startOffset = start?.timeZoneOffset.inMilliseconds ?? 0; - var endOffset = end?.timeZoneOffset.inMilliseconds ?? 0; - // subtract the offset to get back to midnight on the correct date - start = start?.subtract(Duration(milliseconds: startOffset)); - 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(const Duration(days: 1)); - } - location = json['eventLocation']; - availability = parseStringToAvailability(json['availability']); - status = parseStringToEventStatus(json['eventStatus']); - - foundUrl = json['eventURL']?.toString(); - if (foundUrl?.isEmpty ?? true) { - url = null; - } else { - url = Uri.dataFromString(foundUrl as String); - } - - if (json['attendees'] != null) { - attendees = json['attendees'].map((decodedAttendee) { - return Attendee.fromJson(decodedAttendee); - }).toList(); - } - - if (json['organizer'] != null) { - // Getting and setting an organiser for iOS - var organiser = Attendee.fromJson(json['organizer']); - - var attendee = attendees?.firstWhereOrNull((at) => at?.name == organiser.name && at?.emailAddress == organiser.emailAddress); - if (attendee != null) { - attendee.isOrganiser = true; - } - } - - 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) { - reminders = json['reminders'].map((decodedReminder) { - return Reminder.fromJson(decodedReminder); - }).toList(); - } - if (legacyJSON) { - throw const FormatException('legacy JSON detected. Please update your current JSONs as they may not be supported later on.'); - } - } - - Map toJson() { - final data = {}; - - data['calendarId'] = calendarId; - data['eventId'] = eventId; - data['eventTitle'] = title; - data['eventDescription'] = description; - data['eventStartDate'] = start?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; - data['eventStartTimeZone'] = start?.location.name; - data['eventEndDate'] = end?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; - data['eventEndTimeZone'] = end?.location.name; - data['eventAllDay'] = allDay; - data['eventLocation'] = location; - data['eventURL'] = url?.data?.contentText; - data['availability'] = availability.enumToString; - data['eventStatus'] = status?.enumToString; - data['eventColor'] = color; //##### - - if (attendees != null) { - data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); - } - - if (attendees != null) { - data['organizer'] = attendees?.firstWhereOrNull((a) => a!.isOrganiser)?.toJson(); - } - - 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; - } - - Availability parseStringToAvailability(String? value) { - var testValue = value?.toUpperCase(); - switch (testValue) { - case 'BUSY': - return Availability.Busy; - case 'FREE': - return Availability.Free; - case 'TENTATIVE': - return Availability.Tentative; - case 'UNAVAILABLE': - return Availability.Unavailable; - } - return Availability.Busy; - } - - EventStatus? parseStringToEventStatus(String? value) { - var testValue = value?.toUpperCase(); - switch (testValue) { - case 'CONFIRMED': - return EventStatus.Confirmed; - case 'TENTATIVE': - return EventStatus.Tentative; - case 'CANCELED': - return EventStatus.Canceled; - case 'NONE': - return EventStatus.None; - } - return null; - } - - bool updateStartLocation(String? newStartLocation) { - if (newStartLocation == null) return false; - try { - var location = timeZoneDatabase.get(newStartLocation); - start = TZDateTime.from(start as TZDateTime, location); - return true; - } on LocationNotFoundException { - return false; - } - } - - bool updateEndLocation(String? newEndLocation) { - if (newEndLocation == null) return false; - try { - var location = timeZoneDatabase.get(newEndLocation); - end = TZDateTime.from(end as TZDateTime, location); - return true; - } on LocationNotFoundException { - return false; - } - } -} diff --git a/device_calendar/lib/src/models/platform_specifics/android/attendance_status.dart b/device_calendar/lib/src/models/platform_specifics/android/attendance_status.dart deleted file mode 100644 index d895877f..00000000 --- a/device_calendar/lib/src/models/platform_specifics/android/attendance_status.dart +++ /dev/null @@ -1,15 +0,0 @@ -enum AndroidAttendanceStatus { - None, - Accepted, - Declined, - Invited, - Tentative, -} - -extension AndroidAttendanceStatusExtensions on AndroidAttendanceStatus { - String _enumToString(AndroidAttendanceStatus enumValue) { - return enumValue.toString().split('.').last; - } - - String get enumToString => _enumToString(this); -} diff --git a/device_calendar/lib/src/models/platform_specifics/android/attendee_details.dart b/device_calendar/lib/src/models/platform_specifics/android/attendee_details.dart deleted file mode 100644 index ba6f3b7a..00000000 --- a/device_calendar/lib/src/models/platform_specifics/android/attendee_details.dart +++ /dev/null @@ -1,23 +0,0 @@ -import '../../../common/error_messages.dart'; -import 'attendance_status.dart'; - -class AndroidAttendeeDetails { - AndroidAttendanceStatus? attendanceStatus; - - AndroidAttendeeDetails({this.attendanceStatus}); - - AndroidAttendeeDetails.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - - if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { - attendanceStatus = - AndroidAttendanceStatus.values[json['attendanceStatus']]; - } - } - - Map toJson() { - return {'attendanceStatus': attendanceStatus?.index}; - } -} diff --git a/device_calendar/lib/src/models/platform_specifics/ios/attendance_status.dart b/device_calendar/lib/src/models/platform_specifics/ios/attendance_status.dart deleted file mode 100644 index bd958391..00000000 --- a/device_calendar/lib/src/models/platform_specifics/ios/attendance_status.dart +++ /dev/null @@ -1,18 +0,0 @@ -enum IosAttendanceStatus { - Unknown, - Pending, - Accepted, - Declined, - Tentative, - Delegated, - Completed, - InProcess, -} - -extension IosAttendanceStatusExtensions on IosAttendanceStatus { - String _enumToString(IosAttendanceStatus enumValue) { - return enumValue.toString().split('.').last; - } - - String get enumToString => _enumToString(this); -} diff --git a/device_calendar/lib/src/models/platform_specifics/ios/attendee_details.dart b/device_calendar/lib/src/models/platform_specifics/ios/attendee_details.dart deleted file mode 100644 index 305a1b42..00000000 --- a/device_calendar/lib/src/models/platform_specifics/ios/attendee_details.dart +++ /dev/null @@ -1,21 +0,0 @@ -import '../../../common/error_messages.dart'; -import 'attendance_status.dart'; - -class IosAttendeeDetails { - IosAttendanceStatus? attendanceStatus; - IosAttendeeDetails({this.attendanceStatus}); - - IosAttendeeDetails.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - - if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { - attendanceStatus = IosAttendanceStatus.values[json['attendanceStatus']]; - } - } - - Map toJson() { - return {'attendanceStatus': attendanceStatus?.index}; - } -} diff --git a/device_calendar/lib/src/models/reminder.dart b/device_calendar/lib/src/models/reminder.dart deleted file mode 100644 index 761ab676..00000000 --- a/device_calendar/lib/src/models/reminder.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class Reminder { - /// The time when the reminder should be triggered expressed in terms of minutes before the start of the event - int? minutes; - - Reminder({@required this.minutes}) - : assert(minutes != null && minutes >= 0, - 'Minutes must be greater than or equal than zero'); - - Reminder.fromJson(Map json) { - minutes = json['minutes'] as int; - } - - Map toJson() { - return {'minutes': minutes}; - } -} diff --git a/device_calendar/lib/src/models/result.dart b/device_calendar/lib/src/models/result.dart deleted file mode 100644 index fff23c71..00000000 --- a/device_calendar/lib/src/models/result.dart +++ /dev/null @@ -1,33 +0,0 @@ -class Result { - /// Indicates if the request was successfull or not - /// - /// Returns true if data is not null and there're no error messages, otherwise returns false - bool get isSuccess { - var res = data != null && errors.isEmpty; - if (res) { - if (data is String) { - res = (data as String).isNotEmpty; - } - } - - return res; - } - - /// Indicates if there are errors. This isn't exactly the same as !isSuccess since - /// it doesn't look at the state of the data. - /// - /// Returns true if there are error messages, otherwise false - bool get hasErrors { - return errors.isNotEmpty; - } - - T? data; - List errors = []; -} - -class ResultError { - final int errorCode; - final String errorMessage; - - const ResultError(this.errorCode, this.errorMessage); -} diff --git a/device_calendar/lib/src/models/retrieve_events_params.dart b/device_calendar/lib/src/models/retrieve_events_params.dart deleted file mode 100644 index 7965b0e1..00000000 --- a/device_calendar/lib/src/models/retrieve_events_params.dart +++ /dev/null @@ -1,7 +0,0 @@ -class RetrieveEventsParams { - final List? eventIds; - final DateTime? startDate; - final DateTime? endDate; - - const RetrieveEventsParams({this.eventIds, this.startDate, this.endDate}); -} diff --git a/device_calendar/pubspec.yaml b/device_calendar/pubspec.yaml deleted file mode 100644 index 7c99a9c5..00000000 --- a/device_calendar/pubspec.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: device_calendar -description: A cross platform plugin for modifying calendars on the user's device. -version: 4.3.1 -homepage: https://github.com/builttoroam/device_calendar/tree/master - -dependencies: - flutter: - sdk: flutter - collection: ^1.16.0 - timezone: ^0.9.0 - rrule: ^0.2.10 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^2.0.1 - -flutter: - plugin: - platforms: - android: - package: com.builttoroam.devicecalendar - pluginClass: DeviceCalendarPlugin - ios: - pluginClass: DeviceCalendarPlugin - -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.20.0" diff --git a/device_calendar/test/device_calendar_test.dart b/device_calendar/test/device_calendar_test.dart deleted file mode 100644 index 132aad61..00000000 --- a/device_calendar/test/device_calendar_test.dart +++ /dev/null @@ -1,245 +0,0 @@ -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'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - const channel = MethodChannel('plugins.builttoroam.com/device_calendar'); - var deviceCalendarPlugin = DeviceCalendarPlugin(); - - final log = []; - - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - print('Calling channel method ${methodCall.method}'); - log.add(methodCall); - - return null; - }); - - log.clear(); - }); - - test('HasPermissions_Returns_Successfully', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - return true; - }); - - final result = await deviceCalendarPlugin.hasPermissions(); - expect(result.isSuccess, true); - expect(result.errors, isEmpty); - expect(result.data, true); - }); - - test('RequestPermissions_Returns_Successfully', () async { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - return true; - }); - - final result = await deviceCalendarPlugin.requestPermissions(); - expect(result.isSuccess, true); - expect(result.errors, isEmpty); - expect(result.data, true); - }); - - test('RetrieveCalendars_Returns_Successfully', () async { - const fakeCalendarName = 'fakeCalendarName'; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - return '[{"id":"1","isReadOnly":false,"name":"$fakeCalendarName"}]'; - }); - - final result = await deviceCalendarPlugin.retrieveCalendars(); - expect(result.isSuccess, true); - expect(result.errors, isEmpty); - expect(result.data, isNotNull); - expect(result.data, isNotEmpty); - expect(result.data?[0].name, fakeCalendarName); - }); - - test('RetrieveEvents_CalendarId_IsRequired', () async { - const String? calendarId = null; - const params = RetrieveEventsParams(); - - final result = - await deviceCalendarPlugin.retrieveEvents(calendarId, params); - expect(result.isSuccess, false); - expect(result.errors.length, greaterThan(0)); - expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); - }); - - test('DeleteEvent_CalendarId_IsRequired', () async { - const String? calendarId = null; - const eventId = 'fakeEventId'; - - final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); - expect(result.isSuccess, false); - expect(result.errors.length, greaterThan(0)); - expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); - }); - - test('DeleteEvent_EventId_IsRequired', () async { - const calendarId = 'fakeCalendarId'; - const String? eventId = null; - - final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); - expect(result.isSuccess, false); - expect(result.errors.length, greaterThan(0)); - expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); - }); - - test('DeleteEvent_PassesArguments_Correctly', () async { - const calendarId = 'fakeCalendarId'; - const eventId = 'fakeEventId'; - - await deviceCalendarPlugin.deleteEvent(calendarId, eventId); - expect(log, [ - isMethodCall('deleteEvent', arguments: { - 'calendarId': calendarId, - 'eventId': eventId - }) - ]); - }); - - test('CreateEvent_Arguments_Invalid', () async { - const String? fakeCalendarId = null; - final event = Event(fakeCalendarId); - - final result = await deviceCalendarPlugin.createOrUpdateEvent(event); - expect(result!.isSuccess, false); - expect(result.errors, isNotEmpty); - expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); - }); - - test('CreateEvent_Returns_Successfully', () async { - const fakeNewEventId = 'fakeNewEventId'; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - return fakeNewEventId; - }); - - const fakeCalendarId = 'fakeCalendarId'; - final event = Event(fakeCalendarId); - event.title = 'fakeEventTitle'; - event.start = TZDateTime.now(local); - event.end = event.start!.add(const Duration(hours: 1)); - - final result = await deviceCalendarPlugin.createOrUpdateEvent(event); - expect(result?.isSuccess, true); - expect(result?.errors, isEmpty); - expect(result?.data, isNotEmpty); - expect(result?.data, fakeNewEventId); - }); - - test('UpdateEvent_Returns_Successfully', () async { - const fakeNewEventId = 'fakeNewEventId'; - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) async { - final arguments = methodCall.arguments as Map; - if (!arguments.containsKey('eventId') || arguments['eventId'] == null) { - return null; - } - - return fakeNewEventId; - }); - - const fakeCalendarId = 'fakeCalendarId'; - final event = Event(fakeCalendarId); - event.eventId = 'fakeEventId'; - event.title = 'fakeEventTitle'; - event.start = TZDateTime.now(local); - event.end = event.start!.add(const Duration(hours: 1)); - - final result = await deviceCalendarPlugin.createOrUpdateEvent(event); - expect(result?.isSuccess, true); - expect(result?.errors, isEmpty); - expect(result?.data, isNotEmpty); - expect(result?.data, fakeNewEventId); - }); - - test('Attendee_Serialises_Correctly', () async { - final attendee = Attendee( - name: 'Test Attendee', - emailAddress: 'test@t.com', - role: AttendeeRole.Required, - isOrganiser: true); - final stringAttendee = attendee.toJson(); - expect(stringAttendee, isNotNull); - final newAttendee = Attendee.fromJson(stringAttendee); - expect(newAttendee, isNotNull); - expect(newAttendee.name, equals(attendee.name)); - expect(newAttendee.emailAddress, equals(attendee.emailAddress)); - expect(newAttendee.role, equals(attendee.role)); - expect(newAttendee.isOrganiser, equals(attendee.isOrganiser)); - expect(newAttendee.iosAttendeeDetails, isNull); - expect(newAttendee.androidAttendeeDetails, isNull); - }); - - test('Event_Serializes_Correctly', () async { - final startTime = TZDateTime( - timeZoneDatabase.locations.entries.skip(20).first.value, - 1980, - 10, - 1, - 0, - 0, - 0); - final endTime = TZDateTime( - timeZoneDatabase.locations.entries.skip(21).first.value, - 1980, - 10, - 2, - 0, - 0, - 0); - final attendee = Attendee( - name: 'Test Attendee', - emailAddress: 'test@t.com', - role: AttendeeRole.Required, - isOrganiser: true); - final recurrence = RecurrenceRule(frequency: Frequency.daily); - final reminder = Reminder(minutes: 10); - var event = Event('calendarId', - eventId: 'eventId', - title: 'Test Event', - start: startTime, - location: 'Seattle, Washington', - url: Uri.dataFromString('http://www.example.com'), - end: endTime, - attendees: [attendee], - description: 'Test description', - recurrenceRule: recurrence, - reminders: [reminder], - availability: Availability.Busy, - status: EventStatus.Confirmed); - - final stringEvent = event.toJson(); - expect(stringEvent, isNotNull); - final newEvent = Event.fromJson(stringEvent); - expect(newEvent, isNotNull); - expect(newEvent.calendarId, equals(event.calendarId)); - expect(newEvent.eventId, equals(event.eventId)); - expect(newEvent.title, equals(event.title)); - expect(newEvent.start!.millisecondsSinceEpoch, - equals(event.start!.millisecondsSinceEpoch)); - expect(newEvent.end!.millisecondsSinceEpoch, - equals(event.end!.millisecondsSinceEpoch)); - expect(newEvent.description, equals(event.description)); - expect(newEvent.url, equals(event.url)); - expect(newEvent.location, equals(event.location)); - expect(newEvent.attendees, isNotNull); - expect(newEvent.attendees?.length, equals(1)); - expect(newEvent.recurrenceRule, isNotNull); - expect(newEvent.recurrenceRule?.frequency, - equals(event.recurrenceRule?.frequency)); - expect(newEvent.reminders, isNotNull); - expect(newEvent.reminders?.length, equals(1)); - expect(newEvent.availability, equals(event.availability)); - expect(newEvent.status, equals(event.status)); - }); -} From cd66f8f1c7d16b763d998008c81b64b63afb802f Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Tue, 28 Nov 2023 11:50:37 +0100 Subject: [PATCH 05/11] added event color --- CHANGELOG.md | 200 +++ LICENSE | 27 + README.md | 121 ++ analysis_options.yaml | 30 + android/.gitignore | 8 + android/build.gradle | 58 + android/gradle.properties | 3 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54708 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + android/gradlew | 172 +++ android/gradlew.bat | 84 ++ android/proguard-rules.pro | 1 + android/settings.gradle | 1 + android/src/main/AndroidManifest.xml | 3 + .../devicecalendar/AvailabilitySerializer.kt | 18 + .../devicecalendar/CalendarDelegate.kt | 1268 +++++++++++++++++ .../devicecalendar/DeviceCalendarPlugin.kt | 301 ++++ .../devicecalendar/EventStatusSerializer.kt | 15 + .../devicecalendar/common/Constants.kt | 117 ++ .../devicecalendar/common/ErrorCodes.kt | 11 + .../devicecalendar/common/ErrorMessages.kt | 16 + .../devicecalendar/models/Attendee.kt | 10 + .../devicecalendar/models/Availability.kt | 7 + .../devicecalendar/models/Calendar.kt | 13 + .../CalendarMethodsParametersCacheModel.kt | 16 + .../devicecalendar/models/Event.kt | 23 + .../devicecalendar/models/EventStatus.kt | 7 + .../devicecalendar/models/RecurrenceRule.kt | 17 + .../devicecalendar/models/Reminder.kt | 3 + device_calendar.iml | 19 + device_calendar_android.iml | 30 + example/.gitignore | 9 + example/.metadata | 8 + example/README.md | 195 +++ example/analysis_options.yaml | 30 + example/android/.gitignore | 10 + example/android/app/build.gradle | 60 + example/android/app/proguard-rules.pro | 1 + .../android/app/src/main/AndroidManifest.xml | 44 + .../devicecalendarexample/MainActivity.kt | 6 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values/styles.xml | 8 + example/android/build.gradle | 29 + example/android/gradle.properties | 3 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + example/android/gradlew | 160 +++ example/android/gradlew.bat | 90 ++ example/android/settings.gradle | 15 + example/device_calendar_example.iml | 17 + example/device_calendar_example_android.iml | 27 + example/integration_test/app_test.dart | 89 ++ .../integration_test/integration_test.dart | 8 + .../integration_test_android.dart | 29 + example/integration_test/ios.sh | 24 + example/ios/.gitignore | 45 + example/ios/Flutter/AppFrameworkInfo.plist | 30 + example/ios/Flutter/Debug.xcconfig | 2 + example/ios/Flutter/Release.xcconfig | 2 + example/ios/Podfile | 45 + example/ios/Podfile.lock | 34 + example/ios/Runner.xcodeproj/project.pbxproj | 507 +++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 11112 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + example/ios/Runner/Base.lproj/Main.storyboard | 26 + example/ios/Runner/Info.plist | 55 + example/ios/Runner/Runner-Bridging-Header.h | 1 + example/lib/common/app_routes.dart | 3 + example/lib/main.dart | 29 + .../lib/presentation/date_time_picker.dart | 81 ++ example/lib/presentation/event_item.dart | 342 +++++ example/lib/presentation/input_dropdown.dart | 42 + .../lib/presentation/pages/calendar_add.dart | 164 +++ .../presentation/pages/calendar_event.dart | 1261 ++++++++++++++++ .../presentation/pages/calendar_events.dart | 190 +++ example/lib/presentation/pages/calendars.dart | 161 +++ .../presentation/pages/event_attendee.dart | 174 +++ .../presentation/pages/event_reminders.dart | 102 ++ .../presentation/recurring_event_dialog.dart | 101 ++ example/pubspec.yaml | 27 + ios/.gitignore | 36 + ios/Assets/.gitkeep | 0 ios/Classes/DeviceCalendarPlugin.h | 4 + ios/Classes/DeviceCalendarPlugin.m | 8 + ios/Classes/SwiftDeviceCalendarPlugin.swift | 1136 +++++++++++++++ ios/device_calendar.podspec | 22 + lib/device_calendar.dart | 17 + lib/src/common/calendar_enums.dart | 315 ++++ lib/src/common/channel_constants.dart | 26 + lib/src/common/error_codes.dart | 6 + lib/src/common/error_messages.dart | 27 + lib/src/device_calendar.dart | 443 ++++++ lib/src/models/attendee.dart | 81 ++ lib/src/models/calendar.dart | 56 + lib/src/models/event.dart | 306 ++++ .../android/attendance_status.dart | 15 + .../android/attendee_details.dart | 23 + .../ios/attendance_status.dart | 18 + .../ios/attendee_details.dart | 21 + lib/src/models/reminder.dart | 18 + lib/src/models/result.dart | 33 + lib/src/models/retrieve_events_params.dart | 7 + pubspec.yaml | 29 + test/device_calendar_test.dart | 245 ++++ 135 files changed, 10120 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 analysis_options.yaml create mode 100644 android/.gitignore create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/proguard-rules.pro create mode 100644 android/settings.gradle create mode 100644 android/src/main/AndroidManifest.xml create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt create mode 100644 android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt create mode 100644 device_calendar.iml create mode 100644 device_calendar_android.iml create mode 100644 example/.gitignore create mode 100644 example/.metadata create mode 100644 example/README.md create mode 100644 example/analysis_options.yaml create mode 100644 example/android/.gitignore create mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/proguard-rules.pro create mode 100644 example/android/app/src/main/AndroidManifest.xml create mode 100644 example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt create mode 100644 example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/values/styles.xml create mode 100644 example/android/build.gradle create mode 100644 example/android/gradle.properties create mode 100644 example/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 example/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 example/android/gradlew create mode 100644 example/android/gradlew.bat create mode 100644 example/android/settings.gradle create mode 100644 example/device_calendar_example.iml create mode 100644 example/device_calendar_example_android.iml create mode 100644 example/integration_test/app_test.dart create mode 100644 example/integration_test/integration_test.dart create mode 100644 example/integration_test/integration_test_android.dart create mode 100755 example/integration_test/ios.sh create mode 100755 example/ios/.gitignore create mode 100755 example/ios/Flutter/AppFrameworkInfo.plist create mode 100755 example/ios/Flutter/Debug.xcconfig create mode 100755 example/ios/Flutter/Release.xcconfig create mode 100644 example/ios/Podfile create mode 100755 example/ios/Podfile.lock create mode 100644 example/ios/Runner.xcodeproj/project.pbxproj create mode 100755 example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100755 example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100755 example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 example/ios/Runner/AppDelegate.swift create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100755 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100755 example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100755 example/ios/Runner/Base.lproj/Main.storyboard create mode 100755 example/ios/Runner/Info.plist create mode 100755 example/ios/Runner/Runner-Bridging-Header.h create mode 100644 example/lib/common/app_routes.dart create mode 100644 example/lib/main.dart create mode 100644 example/lib/presentation/date_time_picker.dart create mode 100644 example/lib/presentation/event_item.dart create mode 100644 example/lib/presentation/input_dropdown.dart create mode 100644 example/lib/presentation/pages/calendar_add.dart create mode 100644 example/lib/presentation/pages/calendar_event.dart create mode 100644 example/lib/presentation/pages/calendar_events.dart create mode 100644 example/lib/presentation/pages/calendars.dart create mode 100644 example/lib/presentation/pages/event_attendee.dart create mode 100644 example/lib/presentation/pages/event_reminders.dart create mode 100644 example/lib/presentation/recurring_event_dialog.dart create mode 100644 example/pubspec.yaml create mode 100644 ios/.gitignore create mode 100644 ios/Assets/.gitkeep create mode 100644 ios/Classes/DeviceCalendarPlugin.h create mode 100644 ios/Classes/DeviceCalendarPlugin.m create mode 100644 ios/Classes/SwiftDeviceCalendarPlugin.swift create mode 100644 ios/device_calendar.podspec create mode 100644 lib/device_calendar.dart create mode 100644 lib/src/common/calendar_enums.dart create mode 100644 lib/src/common/channel_constants.dart create mode 100644 lib/src/common/error_codes.dart create mode 100644 lib/src/common/error_messages.dart create mode 100644 lib/src/device_calendar.dart create mode 100644 lib/src/models/attendee.dart create mode 100644 lib/src/models/calendar.dart create mode 100644 lib/src/models/event.dart create mode 100644 lib/src/models/platform_specifics/android/attendance_status.dart create mode 100644 lib/src/models/platform_specifics/android/attendee_details.dart create mode 100644 lib/src/models/platform_specifics/ios/attendance_status.dart create mode 100644 lib/src/models/platform_specifics/ios/attendee_details.dart create mode 100644 lib/src/models/reminder.dart create mode 100644 lib/src/models/result.dart create mode 100644 lib/src/models/retrieve_events_params.dart create mode 100644 pubspec.yaml create mode 100644 test/device_calendar_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..2fa175fa --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,200 @@ +# Changelog + + + +## [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 + - *Note:* `timezone 0.9.0` [removed named database files](https://pub.dev/packages/timezone/changelog#090). If you are only using `device_calendar`, you can ignore this note. +- Added support for all-day multi-day events on iOS +- Fixed iOS issue of adding attendees to events +- Fixed Android issue of the `ownerAccount` being null + +## [4.2.0](https://github.com/builttoroam/device_calendar/releases/tag/4.2.0) + +- Fix: apks can be build correctly now +- Support for viewing and editing attendee status + - iOS needs a specific native view and permissions to edit attendees due to iOS restrictions. See README and example app. + +## [4.1.0](https://github.com/builttoroam/device_calendar/releases/tag/4.1.0) + +- Fix: title, descriptions etc are now retrieved properly. +- Fix: Event JSONs created and are now readable. Previous (mislabeled) JSONs are also readable with warnings. +- Fix: removed depreceated plugins from Example. +- Integration tests are now working. Android instructions are ready. +- Gradle plug-ins are updated. +- Compiles with jvm 1.8, should be compilable for Flutter 2.9+ +- Android: proper support for all day events, and multi-day all day events. + +## [4.0.1](https://github.com/builttoroam/device_calendar/releases/tag/4.0.1) + +- Fix: event time are now properly retrieved + +## [4.0.0](https://github.com/builttoroam/device_calendar/releases/tag/4.0.0) + +- Timezone plugin and logic implemented. All issues related to timezone shoulde be fixed. +- Events.availability defaults to busy when not specified [354](https://github.com/builttoroam/device_calendar/pull/354) +- Events parameter now includes location and url. [319](https://github.com/builttoroam/device_calendar/pull/319) +- Android: Fixed bug where platform exception appeared, when Events.availability was null on Event [241](https://github.com/builttoroam/device_calendar/issues/241) +- Fixed various issues in example [270](https://github.com/builttoroam/device_calendar/issues/270), [268](https://github.com/builttoroam/device_calendar/issues/268) +- Android: deleteEvent code aligned with flutter [258](https://github.com/builttoroam/device_calendar/issues/258) +- Android: Updated to V2 embeddding [326](https://github.com/builttoroam/device_calendar/issues/326) +- iOS: Updated swift versions, possibly improved compability with Obj-C [flutter/flutter#16049 (comment)](https://github.com/flutter/flutter/issues/16049#issuecomment-611192738) + +## [3.9.0](https://github.com/builttoroam/device_calendar/releases/tag/3.9.0) + +- Migrated to null safety +- Updated multiple underlying dependencies +- Rebuilt iOS podfile +- Upgraded to new Android plugins APIs for flutter + +## 3.1.0 25th March 2020 - Bug fixes and new features + +- Boolean variable `isDefault` added for issue [145](https://github.com/builttoroam/device_calendar/issues/145) (**NOTE**: This is not supported Android API 16 or lower, `isDefault` will always be false) +- Events with 'null' title now defaults to 'New Event', issue [126](https://github.com/builttoroam/device_calendar/issues/126) +- Updated property summaries for issues [121](https://github.com/builttoroam/device_calendar/issues/121) and [122](https://github.com/builttoroam/device_calendar/issues/122) +- Updated example documentation for issue [119](https://github.com/builttoroam/device_calendar/issues/119) +- Read-only calendars cannot be edited or deleted for the example app +- Added `DayOfWeekGroup` enum and an extension `getDays` to get corresponding dates of the enum values +- Added to retrieve colour for calendars. Thanks to [nadavfima](https://github.com/nadavfima) for the contribution and PR to add colour support for both Android and iOS +- Added compatibility with a new Flutter plugin for Android. Thanks to the PR submitted by [RohitKumarMishra](https://github.com/RohitKumarMishra) +- [Android] Fixed all day timezone issue [164](https://github.com/builttoroam/device_calendar/issues/164) +- Added support for deleting individual or multiple instances of a recurring event for issue [108](https://github.com/builttoroam/device_calendar/issues/108) +- Ability to add local calendars with a desired colour for issue [115](https://github.com/builttoroam/device_calendar/issues/115) +- Returns account name and type for each calendars for issue [179](https://github.com/builttoroam/device_calendar/issues/179) + +## 3.0.0+3 3rd February 2020 + +- Fixed all day conditional check for issue [162](https://github.com/builttoroam/device_calendar/issues/162) + +## 3.0.0+2 30th January 2020 + +- Updated `event.allDay` property in `createOrUpdateEvent` method to be null-aware + +## 3.0.0+1 28th January 2020 + +- Updated `event.url` property in `createOrUpdateEvent` method to be null-aware for issue [152](https://github.com/builttoroam/device_calendar/issues/152) + +## 3.0.0 21st January 2020 + +- **BREAKING CHANGE** Properties for the attendee model in `attendee.dart` file have been changed: + - Boolean property `isRequired` has been replaced to `AttendeeRole` enum + - New arugment added for `AttendeeRole` property +- **BREAKING CHANGE** Package updates: + - [Android] Updated Gradle plugin to 3.5.2 and Gradle wrapper to 5.4.1 + - [iOS] Updated Swift to 5 +- `name` and `isOrganiser` (read-only) properties have been added +- Attendee UI update for the example app +- Ability to add, modify or remove an attendee + +## 2.0.0 17th January 2020 + +- **BREAKING CHANGE** The recurrence models in `recurrence_rule.dart` file have been chaged +- **BREAKING CHANGE** All articles used in property names or arugments have been removed (i.e. enum `DayOfTheWeek` to `DayOfWeek`) +- Recurrence fix for monthly and yearly frequencies +- UI update for the example app +- Add support for all day events + +## 1.0.0+3 9th January 2020 + +- Flutter upgrade to 1.12.13 +- Added an URL input for calendar events for issue [132](https://github.com/builttoroam/device_calendar/issues/132) + +## 1.0.0+2 30th August 2019 + +- Fix home page URL + +## 1.0.0+1 30th August 2019 + +- Add integration tests to example app. Note that this is more for internal use at the moment as it currently requires an Android device with a calendar that can be written to and assumes that the tests are executed from a Mac. + +## 1.0.0 28th August 2019 + +- **BREAKING CHANGE** `retrieveCalendars` and `retrieveEvents` now return lists that cannot be modified (`UnmodifiableListView`) to address part of issue [113](https://github.com/builttoroam/device_calendar/issues/113) +- Support for more advanced recurrence rules +- Update README to include information about using ProGuard for issue [99](https://github.com/builttoroam/device_calendar/issues/99) +- Made event title optional to fix issue [72](https://github.com/builttoroam/device_calendar/issues/72) +- Return information about the organiser of the event as per issue [73](https://github.com/builttoroam/device_calendar/issues/73) +- Return attendance status of attendees and if they're required for an event. These are details are different across iOS and Android and so are returned within platform-specific objects +- Ability to modify attendees for an event +- Ability to create reminders for events expressed in minutes before the event starts + +## 0.2.2 19th August 2019 + +- Add support for specifying the location of an event. Thanks to [oli06](https://github.com/oli06) and [zemanux](https://github.com/zemanux) for submitting PRs to add the functionality to iOS and Android respectively + +## 0.2.1+1 5th August 2019 + +- Fixing date in changelog for version 0.2.1 + +## 0.2.1 5th August 2019 + +- [Android] Fixes issue [101](https://github.com/builttoroam/device_calendar/issues/101) where plugin results in a crash with headless execution + +## 0.2.0 30th July 2019 + +- **BREAKING CHANGE** [Android] Updated to use Gradle plugin to 3.4.2, Gradle wrapper to 5.1.1, Kotlin version to 1.3.41 and bumped Android dependencies +- Add initial support for recurring events. Note that currently editing or deleting a recurring event will affect all instances of it. Future releases will look at supporting more advanced recurrence rules +- Remove old example app to avoid confusion + +## 0.1.3 5th July 2019 + +- [iOS] Fixes issue [94](https://github.com/builttoroam/device_calendar/issues/94) that occurred on 32-bit iOS devices around date of events. Thanks to the PR submitted by [duzenko](https://github.com/duzenko) + +## 0.1.2+2 28th May 2019 + +- Non-functional release. Minor refactoring in Android code to address issues found in Codefactor and fix build status badge in README + +## 0.1.2+1 17th May 2019 + +- Non-functional release. Fixed formatting in changelog and code comments +- Added more info about potential issues in consuming the plugin within an Objective-C project + +## 0.1.2 - 16th May 2019 + +- [Android] An updated fix to address issue [79](https://github.com/builttoroam/device_calendar/issues/79), thanks to the PR submitted by [Gerry High](https://github.com/gerryhigh) + +## 0.1.1 - 1st March 2019 + +- Fixed issue [79](https://github.com/builttoroam/device_calendar/issues/79) where on Android, the plugin was indicating that it was handling permissions that it shouldn't have + +## 0.1.0 - 26th February 2019 + +- **BREAKING CHANGE** Migrated to the plugin to use AndroidX instead of the deprecated Android support libraries. Please ensure you have migrated your application following the guide [here](https://developer.android.com/jetpack/androidx/migrate) +- **BREAKING CHANGE** Updated Kotlin to version 1.3.21 +- **BREAKING CHANGE** Updated Gradle plugin to 3.3.1 and distribution to 4.10.2 + +## 0.0.8 - 26th February 2019 + +- This was a breaking change that should've been incremented as minor version update instead of a patch version update. See changelog for 0.1.0 for the details of this update + +## 0.0.7 - 16th November 2018 + +- Fixes issue [##67](https://github.com/builttoroam/device_calendar/issues/67) and [##68](https://github.com/builttoroam/device_calendar/issues/68). Thanks to PR submitted by huzhiren. + +## 0.0.6 - 18th June 2018 + +- [iOS] Fix an issue when adding/updating an event with a null description + +## 0.0.5 - 14th June 2018 + +- [Android] Fixed an issue with retrieving events by id only + +## 0.0.4 - 12th June 2018 + +- Reordering changelog +- Creating new example for the Pub Dart Example tab +- Moving existing example to the example_app GitHub folder + +## 0.0.2 - 0.0.3 - 7th June 2018 + +- Fixing incorrect Travis build links + +## 0.0.1 - 7th June 2018 + +- Ability to retrieve device calendars +- CRUD operations on calendar events diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..81f1b5da --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2018 Built to Roam. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Built to Roam nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..2af1e99d --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# Device Calendar Plugin + +[![pub package](https://img.shields.io/pub/v/device_calendar.svg)](https://pub.dartlang.org/packages/device_calendar) ![Pub Version (including pre-releases)](https://img.shields.io/pub/v/device_calendar?include_prereleases&label=Prerelease) [![build](https://github.com/builttoroam/device_calendar/actions/workflows/dart.yml/badge.svg?branch=develop)](https://github.com/builttoroam/device_calendar/actions/workflows/dart.yml) + +A cross platform plugin for modifying calendars on the user's device. + +## Breaking changes at v4 + +* **If you're upgrading from previous versions, your code will need to be modified (slightly), otherwise it will not run after update. See [Timezone support](https://github.com/builttoroam/device_calendar#timezone-support-with-tzdatetime) for more details.** +* **There are some changes to event JSON formats at v4. Pay extra care if you handle event JSONs. Directly calling to and from device calendars should be unaffected.** + +## Features + +* Request permissions to modify calendars on the user's device +* Check if permissions to modify the calendars on the user's device have been granted +* Add or retrieve calendars on the user's device +* Retrieve events associated with a calendar +* Add, update or delete events from a calendar +* Set up, edit or delete recurring events + * **NOTE**: Editing a recurring event will currently edit all instances of it + * **NOTE**: Deleting multiple instances in **Android** takes time to update, you'll see the changes after a few seconds +* Add, modify or remove attendees and receive if an attendee is an organiser for an event +* Setup reminders for an event +* Specify a time zone for event start and end date + * **NOTE**: Due to a limitation of iOS API, single time zone property is used for iOS (`event.startTimeZone`) + * **NOTE**: For the time zone list, please refer to the `TZ database name` column on [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) + +## Timezone support with TZDateTime + +Due to feedback we received, starting from `4.0.0` we will be using the `timezone` package to better handle all timezone data. + +This is already included in this package. However, you need to add this line whenever the package is needed. + +```dart +import 'package:timezone/timezone.dart'; +``` + +If you don't need any timezone specific features in your app, you may use `flutter_native_timezone` to get your devices' current timezone, then convert your previous `DateTime` with it. + +```dart +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; + +initializeTimeZones(); + +// As an example, our default timezone is UTC. +Location _currentLocation = getLocation('Etc/UTC'); + +Future setCurentLocation() async { + String timezone = 'Etc/UTC'; + try { + timezone = await FlutterNativeTimezone.getLocalTimezone(); + } catch (e) { + print('Could not get the local timezone'); + } + _currentLocation = getLocation(timezone); + setLocalLocation(_currentLocation); +} + +... + +event.start = TZDateTime.from(oldDateTime, _currentLocation); +``` + +For other use cases, feedback or future developments on the feature, feel free to open a discussion on GitHub. + +## Null-safety migration + +From `v3.9.0`, device_calendar is null-safe. However, not all workflows have been checked and bugs from older versions still persist. + +You are strongly advised to test your workflow with the new package before shipping. +Better yet, please leave a note for what works and what doesn't, or contribute some bug fixes! + +## Android Integration + +The following will need to be added to the `AndroidManifest.xml` file for your application to indicate permissions to modify calendars are needed + +```xml + + +``` + +### 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()`. + +You may add the following setting to the ProGuard rules file `proguard-rules.pro` (thanks to [Britannio Jarrett](https://github.com/britannio)). Read more about the issue [here](https://github.com/builttoroam/device_calendar/issues/99) + +``` +-keep class com.builttoroam.devicecalendar.** { *; } +``` + +See [here](https://github.com/builttoroam/device_calendar/issues/99#issuecomment-612449677) for an example setup. + +For more information, refer to the guide at [Android Developer](https://developer.android.com/studio/build/shrink-code#keep-code) + +### AndroidX migration + +Since `v.1.0`, this version has migrated to use AndroidX instead of the deprecated Android support libraries. When using `0.10.0` and onwards for this plugin, please ensure your application has been migrated following the guide [here](https://developer.android.com/jetpack/androidx/migrate) + +## iOS Integration + +For iOS 10+ support, you'll need to modify the `Info.plist` to add the following key/value pair + +```xml +NSCalendarsUsageDescription +Access most functions for calendar viewing and editing. + +NSContactsUsageDescription +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/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/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..c6cbe562 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 00000000..1f5ff500 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,58 @@ +group 'com.builttoroam.devicecalendar' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.6.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + defaultConfig { + minSdkVersion 19 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard-rules.pro' + } + lintOptions { + disable 'InvalidPackage' + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + namespace 'com.builttoroam.devicecalendar' +} + +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.12.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..4d3226ab --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 GIT binary patch literal 54708 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

_x$+l{{cH8$W#CT literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..3c9d0852 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 00000000..cccdd3d5 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega 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/android/settings.gradle b/android/settings.gradle new file mode 100644 index 00000000..ef870028 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'device_calendar' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1479c8d6 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt new file mode 100644 index 00000000..5a803a6b --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt @@ -0,0 +1,18 @@ +package com.builttoroam.devicecalendar + +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) { + return JsonPrimitive(src.name) + } + return JsonObject() + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt new file mode 100644 index 00000000..491370e7 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -0,0 +1,1268 @@ +package com.builttoroam.devicecalendar + +import android.Manifest +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.content.Context +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.ErrorMessages +import com.builttoroam.devicecalendar.models.* +import com.builttoroam.devicecalendar.models.Calendar +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +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.RecurrenceRule.WeekdayNum +import java.util.* +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()) + + init { + val gsonBuilder = GsonBuilder() + 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 + + if (!_cachedParametersMap.containsKey(requestCode)) { + // this plugin doesn't handle this request code + return false + } + + 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 + + try { + if (!permissionGranted) { + finishWithError( + EC.NOT_AUTHORIZED, + EM.NOT_AUTHORIZED_MESSAGE, + cachedValues.pendingChannelResult + ) + return false + } + + when (cachedValues.calendarDelegateMethodCode) { + RETRIEVE_CALENDARS_REQUEST_CODE -> { + retrieveCalendars(cachedValues.pendingChannelResult) + } + RETRIEVE_EVENTS_REQUEST_CODE -> { + 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 + ) + } + DELETE_EVENT_REQUEST_CODE -> { + deleteEvent( + cachedValues.calendarId, + cachedValues.eventId, + cachedValues.pendingChannelResult + ) + } + REQUEST_PERMISSIONS_REQUEST_CODE -> { + finishWithSuccess(permissionGranted, cachedValues.pendingChannelResult) + } + DELETE_CALENDAR_REQUEST_CODE -> { + deleteCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) + } + } + + return true + } finally { + _cachedParametersMap.remove(cachedValues.calendarDelegateMethodCode) + } + } + + fun requestPermissions(pendingChannelResult: MethodChannel.Result) { + if (arePermissionsGranted()) { + finishWithSuccess(true, pendingChannelResult) + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + REQUEST_PERMISSIONS_REQUEST_CODE + ) + requestPermissions(parameters) + } + } + + fun hasPermissions(pendingChannelResult: MethodChannel.Result) { + finishWithSuccess(arePermissionsGranted(), pendingChannelResult) + } + + @SuppressLint("MissingPermission") + fun retrieveCalendars(pendingChannelResult: MethodChannel.Result) { + if (arePermissionsGranted()) { + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = CalendarContract.Calendars.CONTENT_URI + val cursor: Cursor? = if (atLeastAPI(17)) { + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION, null, null, null) + } else { + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION_OLDER_API, null, null, null) + } + val calendars: MutableList = mutableListOf() + try { + while (cursor?.moveToNext() == true) { + val calendar = parseCalendarRow(cursor) ?: continue + calendars.add(calendar) + } + + finishWithSuccess(_gson?.toJson(calendars), pendingChannelResult) + } catch (e: Exception) { + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) + } finally { + cursor?.close() + } + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDARS_REQUEST_CODE + ) + requestPermissions(parameters) + } + } + + 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( + EC.INVALID_ARGUMENT, + EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, + pendingChannelResult + ) + } + return null + } + + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = CalendarContract.Calendars.CONTENT_URI + + val cursor: Cursor? = if (atLeastAPI(17)) { + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION, + null, + null, + null + ) + } else { + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION_OLDER_API, + null, + null, + null + ) + } + + try { + if (cursor?.moveToFirst() == true) { + val calendar = parseCalendarRow(cursor) + if (isInternalCall) { + return calendar + } else { + finishWithSuccess(_gson?.toJson(calendar), pendingChannelResult) + } + } else { + if (!isInternalCall) { + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) + } + } + } catch (e: Exception) { + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) + } finally { + cursor?.close() + } + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDAR_REQUEST_CODE, + calendarId + ) + requestPermissions(parameters) + } + + return null + } + + fun deleteCalendar( + calendarId: String, + pendingChannelResult: MethodChannel.Result, + isInternalCall: Boolean = false + ): Calendar? { + if (isInternalCall || arePermissionsGranted()) { + val calendarIdNumber = calendarId.toLongOrNull() + if (calendarIdNumber == null) { + if (!isInternalCall) { + 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 deleteSucceeded = contentResolver?.delete(calenderUriWithId, null, null) ?: 0 + finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) + } else { + if (!isInternalCall) { + 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 + ) + requestPermissions(parameters) + } + + return null + } + + 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() + 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.OWNER_ACCOUNT, localAccountName) + 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 + val calendarId = java.lang.Long.parseLong(result?.lastPathSegment!!) + + finishWithSuccess(calendarId.toString(), pendingChannelResult) + } + + fun retrieveEvents( + calendarId: String, + startDate: Long?, + endDate: Long?, + eventIds: List, + pendingChannelResult: MethodChannel.Result + ) { + if (startDate == null && endDate == null && eventIds.isEmpty()) { + 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( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) + return + } + + val contentResolver: ContentResolver? = _context?.contentResolver + val eventsUriBuilder = CalendarContract.Instances.CONTENT_URI.buildUpon() + ContentUris.appendId(eventsUriBuilder, startDate ?: Date(0).time) + ContentUris.appendId(eventsUriBuilder, endDate ?: Date(Long.MAX_VALUE).time) + + val eventsUri = eventsUriBuilder.build() + val eventsCalendarQuery = "(${Events.CALENDAR_ID} = $calendarId)" + val eventsNotDeletedQuery = "(${Events.DELETED} != 1)" + val eventsIdsQuery = + "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" + + var eventsSelectionQuery = "$eventsCalendarQuery AND $eventsNotDeletedQuery" + if (eventIds.isNotEmpty()) { + eventsSelectionQuery += " AND ($eventsIdsQuery)" + } + val eventsSortOrder = Events.DTSTART + " DESC" + + val eventsCursor = contentResolver?.query( + eventsUri, + Cst.EVENT_PROJECTION, + eventsSelectionQuery, + null, + eventsSortOrder + ) + + val events: MutableList = mutableListOf() + + val exceptionHandler = CoroutineExceptionHandler { _, exception -> + uiThreadHandler.post { + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) + } + } + + GlobalScope.launch(Dispatchers.IO + exceptionHandler) { + while (eventsCursor?.moveToNext() == true) { + val event = parseEvent(calendarId, eventsCursor) ?: continue + events.add(event) + } + for (event in events) { + val attendees = retrieveAttendees(calendar, event.eventId!!, contentResolver) + event.organizer = + attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } + event.attendees = attendees + event.reminders = retrieveReminders(event.eventId!!, contentResolver) + } + }.invokeOnCompletion { cause -> + eventsCursor?.close() + if (cause == null) { + uiThreadHandler.post { + finishWithSuccess(_gson?.toJson(events), pendingChannelResult) + } + } + } + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_EVENTS_REQUEST_CODE, + calendarId, + startDate, + endDate + ) + requestPermissions(parameters) + } + + return + } + + fun createOrUpdateEvent( + calendarId: String, + event: Event?, + pendingChannelResult: MethodChannel.Result + ) { + if (arePermissionsGranted()) { + if (event == null) { + finishWithError( + EC.GENERIC_ERROR, + EM.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, + pendingChannelResult + ) + return + } + + val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) + if (calendar == null) { + finishWithError( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) + return + } + + val contentResolver: ContentResolver? = _context?.contentResolver + val values = buildEventContentValues(event, calendarId) + + val exceptionHandler = CoroutineExceptionHandler { _, exception -> + uiThreadHandler.post { + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) + } + } + + val job: Job + var eventId: Long? = event.eventId?.toLongOrNull() + if (eventId == null) { + val uri = contentResolver?.insert(Events.CONTENT_URI, values) + // get the event ID that is the last element in the Uri + eventId = java.lang.Long.parseLong(uri?.lastPathSegment!!) + job = GlobalScope.launch(Dispatchers.IO + exceptionHandler) { + insertAttendees(event.attendees, eventId, contentResolver) + insertReminders(event.reminders, eventId, contentResolver) + } + } 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 + for (attendeeToDelete in attendeesToDelete) { + deleteAttendee(eventId, attendeeToDelete, contentResolver) + } + + val attendeesToInsert = + event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } + insertAttendees(attendeesToInsert, eventId, contentResolver) + deleteExistingReminders(contentResolver, eventId) + insertReminders(event.reminders, eventId, contentResolver!!) + + val existingSelfAttendee = existingAttendees.firstOrNull { + it.emailAddress == calendar.ownerAccount + } + val newSelfAttendee = event.attendees.firstOrNull { + it.emailAddress == calendar.ownerAccount + } + if (existingSelfAttendee != null && newSelfAttendee != null && + newSelfAttendee.attendanceStatus != null && + existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus + ) { + updateAttendeeStatus(eventId, newSelfAttendee, contentResolver) + } + } + } + job.invokeOnCompletion { cause -> + if (cause == null) { + uiThreadHandler.post { + finishWithSuccess(eventId.toString(), pendingChannelResult) + } + } + } + } else { + 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 + ) + ) + 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) + } + if (reminderUri != null) { + contentResolver?.delete(reminderUri, null, null) + } + } + cursor?.close() + } + + @SuppressLint("MissingPermission") + private fun insertReminders( + reminders: List, + eventId: Long?, + contentResolver: ContentResolver + ) { + if (reminders.isEmpty()) { + return + } + val remindersContentValues = reminders.map { + ContentValues().apply { + put(CalendarContract.Reminders.EVENT_ID, eventId) + put(CalendarContract.Reminders.MINUTES, it.minutes) + put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) + } + }.toTypedArray() + contentResolver.bulkInsert(CalendarContract.Reminders.CONTENT_URI, remindersContentValues) + } + + private fun buildEventContentValues(event: Event, calendarId: String): ContentValues { + val values = ContentValues() + + 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.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.AVAILABILITY, getAvailability(event.availability)) + 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 + } + + private fun getTimeZone(timeZoneString: String?): TimeZone { + val deviceTimeZone: TimeZone = java.util.Calendar.getInstance().timeZone + var timeZone = TimeZone.getTimeZone(timeZoneString ?: deviceTimeZone.id) + + // Invalid time zone names defaults to GMT so update that to be device's time zone + if (timeZone.id == "GMT" && timeZoneString != "GMT") { + timeZone = TimeZone.getTimeZone(deviceTimeZone.id) + } + + return timeZone + } + + private fun getAvailability(availability: Availability?): Int? = when (availability) { + Availability.BUSY -> Events.AVAILABILITY_BUSY + Availability.FREE -> Events.AVAILABILITY_FREE + Availability.TENTATIVE -> Events.AVAILABILITY_TENTATIVE + else -> null + } + + private fun getEventStatus(eventStatus: EventStatus?): Int? = when (eventStatus) { + EventStatus.CONFIRMED -> Events.STATUS_CONFIRMED + EventStatus.TENTATIVE -> Events.STATUS_TENTATIVE + EventStatus.CANCELED -> Events.STATUS_CANCELED + else -> null + } + + @SuppressLint("MissingPermission") + private fun insertAttendees( + attendees: List, + eventId: Long?, + contentResolver: ContentResolver? + ) { + if (attendees.isEmpty()) { + return + } + + val attendeesValues = attendees.map { + ContentValues().apply { + put(CalendarContract.Attendees.ATTENDEE_NAME, it.name) + put(CalendarContract.Attendees.ATTENDEE_EMAIL, it.emailAddress) + put( + CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.RELATIONSHIP_ATTENDEE + ) + put(CalendarContract.Attendees.ATTENDEE_TYPE, it.role) + put( + CalendarContract.Attendees.ATTENDEE_STATUS, + it.attendanceStatus + ) + put(CalendarContract.Attendees.EVENT_ID, eventId) + } + }.toTypedArray() + + contentResolver?.bulkInsert(CalendarContract.Attendees.CONTENT_URI, attendeesValues) + } + + @SuppressLint("MissingPermission") + 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 + " = ?)" + 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 + ) + } + + 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( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) + return + } + + if (existingCal.isReadOnly) { + finishWithError( + EC.NOT_ALLOWED, + "Calendar with ID $calendarId is read-only", + pendingChannelResult + ) + return + } + + val eventIdNumber = eventId.toLongOrNull() + if (eventIdNumber == null) { + finishWithError( + EC.INVALID_ARGUMENT, + EM.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, + pendingChannelResult + ) + return + } + + val contentResolver: ContentResolver? = _context?.contentResolver + if (startDate == null && endDate == null && followingInstances == null) { // Delete all instances + val eventsUriWithId = ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) + val deleteSucceeded = contentResolver?.delete(eventsUriWithId, null, null) ?: 0 + finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) + } else { + if (!followingInstances!!) { // Only this instance + val exceptionUriWithId = + ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) + val values = ContentValues() + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) + + while (instanceCursor.moveToNext()) { + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) + + if (eventIdNumber == foundEventID) { + values.put( + Events.ORIGINAL_INSTANCE_TIME, + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_BEGIN_INDEX) + ) + values.put(Events.STATUS, Events.STATUS_CANCELED) + } + } + + val deleteSucceeded = contentResolver?.insert(exceptionUriWithId, values) + instanceCursor.close() + finishWithSuccess(deleteSucceeded != null, pendingChannelResult) + } else { // This and following instances + val eventsUriWithId = + ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) + val values = ContentValues() + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) + + while (instanceCursor.moveToNext()) { + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) + + if (eventIdNumber == foundEventID) { + 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, + Cst.EVENT_INSTANCE_DELETION, + startDate, + lastDate + ) + while (cursor.moveToNext()) { + 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, + Cst.EVENT_INSTANCE_DELETION, + startDate - DateUtils.YEAR_IN_MILLIS, + startDate - 1 + ) + var lastRecurrenceDate: Long? = null + + while (cursor.moveToNext()) { + if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { + lastRecurrenceDate = + cursor.getLong(Cst.EVENT_INSTANCE_DELETION_END_INDEX) + } + } + + if (lastRecurrenceDate != null) { + newRule.until = DateTime(lastRecurrenceDate) + } else { + newRule.until = DateTime(startDate - 1) + } + cursor.close() + } + + values.put(Events.RRULE, newRule.toString()) + contentResolver?.update(eventsUriWithId, values, null, null) + finishWithSuccess(true, pendingChannelResult) + } + } + instanceCursor.close() + } + } + } else { + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + DELETE_EVENT_REQUEST_CODE, + calendarId + ) + parameters.eventId = eventId + requestPermissions(parameters) + } + } + + private fun arePermissionsGranted(): Boolean { + if (atLeastAPI(23) && _binding != null) { + val writeCalendarPermissionGranted = _binding!!.activity.checkSelfPermission(Manifest.permission.WRITE_CALENDAR) == PackageManager.PERMISSION_GRANTED + val readCalendarPermissionGranted = _binding!!.activity.checkSelfPermission(Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_GRANTED + return writeCalendarPermissionGranted && readCalendarPermissionGranted + } + + return true + } + + private fun requestPermissions(parameters: CalendarMethodsParametersCacheModel) { + val requestCode: Int = generateUniqueRequestCodeAndCacheParameters(parameters) + requestPermissions(requestCode) + } + + private fun requestPermissions(requestCode: Int) { + if (atLeastAPI(23)) { + _binding!!.activity.requestPermissions( + arrayOf( + Manifest.permission.WRITE_CALENDAR, + Manifest.permission.READ_CALENDAR + ), requestCode + ) + } + } + + private fun parseCalendarRow(cursor: Cursor?): Calendar? { + if (cursor == null) { + return null + } + + 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(), + displayName, + calendarColor, + accountName, + accountType, + ownerAccount + ) + + calendar.isReadOnly = isCalendarReadOnly(accessLevel) + if (atLeastAPI(17)) { + val isPrimary = cursor.getString(Cst.CALENDAR_PROJECTION_IS_PRIMARY_INDEX) + calendar.isDefault = isPrimary == "1" + } else { + calendar.isDefault = false + } + return calendar + } + + private fun parseEvent(calendarId: String, cursor: Cursor?): Event? { + if (cursor == null) { + return null + } + 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 eventColor = cursor.getLong(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) + + val event = Event() + event.eventTitle = title ?: "New Event" + event.eventId = eventId.toString() + event.calendarId = calendarId + event.eventDescription = description + event.eventStartDate = begin + event.eventEndDate = end + event.eventAllDay = allDay + event.eventLocation = location + event.eventURL = url + event.recurrenceRule = parseRecurrenceRuleString(recurringRule) + event.eventStartTimeZone = startTimeZone + event.eventEndTimeZone = endTimeZone + event.availability = availability + event.eventStatus = eventStatus + event.eventColor = eventColor + + return event + } + + private fun parseRecurrenceRuleString(recurrenceRuleString: String?): RecurrenceRule? { + if (recurrenceRuleString == null) { + return null + } + val rfcRecurrenceRule = Rrule(recurrenceRuleString) + val frequency = when (rfcRecurrenceRule.freq) { + 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) + + recurrenceRule.count = rfcRecurrenceRule.count + recurrenceRule.interval = rfcRecurrenceRule.interval + + val until = rfcRecurrenceRule.until + if (until != null) { + recurrenceRule.until = formatDateTime(dateTime = until) + } + + 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) + } + + recurrenceRule.bysetpos = rfcRecurrenceRule.getByPart(Rrule.Part.BYSETPOS) + + return recurrenceRule + } + + private fun formatDateTime(dateTime: DateTime): String { + assert(dateTime.year in 0..9999) + + fun twoDigits(n: Int): String { + return if (n < 10) "0$n" else "$n" + } + + 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" + } + + 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? { + if (cursor == null) { + return null + } + + val emailAddress = cursor.getString(Cst.ATTENDEE_EMAIL_INDEX) + + return Attendee( + emailAddress, + 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 + ) + } + + private fun parseReminderRow(cursor: Cursor?): Reminder? { + if (cursor == null) { + return null + } + + return Reminder(cursor.getInt(Cst.REMINDER_MINUTES_INDEX)) + } + + private fun isCalendarReadOnly(accessLevel: Int): Boolean { + return when (accessLevel) { + Events.CAL_ACCESS_CONTRIBUTOR, + Events.CAL_ACCESS_ROOT, + Events.CAL_ACCESS_OWNER, + Events.CAL_ACCESS_EDITOR + -> false + else -> true + } + } + + @SuppressLint("MissingPermission") + 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, + Cst.ATTENDEE_PROJECTION, + attendeesQuery, + null, + null + ) + attendeesCursor.use { cursor -> + if (cursor?.moveToFirst() == true) { + do { + val attendee = parseAttendeeRow(calendar, attendeesCursor) ?: continue + attendees.add(attendee) + } while (cursor.moveToNext()) + } + } + + return attendees + } + + @SuppressLint("MissingPermission") + 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, + Cst.REMINDER_PROJECTION, + remindersQuery, + null, + null + ) + remindersCursor.use { cursor -> + if (cursor?.moveToFirst() == true) { + do { + val reminder = parseReminderRow(remindersCursor) ?: continue + reminders.add(reminder) + } while (cursor.moveToNext()) + } + } + + return reminders + } + + @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 + val uniqueRequestCode: Int = (_cachedParametersMap.keys.maxOrNull() ?: 0) + 1 + parameters.ownCacheKey = uniqueRequestCode + _cachedParametersMap[uniqueRequestCode] = parameters + + return uniqueRequestCode + } + + private fun finishWithSuccess(result: T, pendingChannelResult: MethodChannel.Result) { + pendingChannelResult.success(result) + clearCachedParameters(pendingChannelResult) + } + + 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() + for (cachedParameter in cachedParameters) { + if (_cachedParametersMap.containsKey(cachedParameter.ownCacheKey)) { + _cachedParametersMap.remove(cachedParameter.ownCacheKey) + } + } + } + + private fun atLeastAPI(api: Int): Boolean { + return api <= Build.VERSION.SDK_INT + } + + 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.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.wkst != null) { + rr.weekStart = Weekday.valueOf(recurrenceRule.wkst!!) + } + + if (recurrenceRule.byday != null) { + rr.byDayPart = recurrenceRule.byday?.mapNotNull { + WeekdayNum.valueOf(it) + }?.toMutableList() + } + + if (recurrenceRule.bymonthday != null) { + rr.setByPart(Rrule.Part.BYMONTHDAY, recurrenceRule.bymonthday!!) + } + + if (recurrenceRule.byyearday != null) { + rr.setByPart(Rrule.Part.BYYEARDAY, recurrenceRule.byyearday!!) + } + + 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) + } + rr.setByPart(Rrule.Part.BYMONTH, newMonth) + } + + if (recurrenceRule.bysetpos != null) { + rr.setByPart(Rrule.Part.BYSETPOS, recurrenceRule.bysetpos!!) + } + return rr.toString() + } + + 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) { + Events.AVAILABILITY_BUSY -> Availability.BUSY + Events.AVAILABILITY_FREE -> Availability.FREE + Events.AVAILABILITY_TENTATIVE -> Availability.TENTATIVE + else -> null + } + + private fun parseEventStatus(status: Int): EventStatus? = when(status) { + Events.STATUS_CONFIRMED -> EventStatus.CONFIRMED + Events.STATUS_CANCELED -> EventStatus.CANCELED + Events.STATUS_TENTATIVE -> EventStatus.TENTATIVE + else -> null + } +} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt new file mode 100644 index 00000000..c1f14533 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -0,0 +1,301 @@ +package com.builttoroam.devicecalendar + +import android.app.Activity +import android.content.Context +import androidx.annotation.NonNull +import com.builttoroam.devicecalendar.common.Constants +import com.builttoroam.devicecalendar.models.* +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +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 org.dmfs.rfc5545.recur.Freq + +const val CHANNEL_NAME = "plugins.builttoroam.com/device_calendar" + +// 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 + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + private var context: Context? = null + private var activity: Activity? = null + + private lateinit var _calendarDelegate: CalendarDelegate + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + context = flutterPluginBinding.applicationContext + channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME) + channel.setMethodCallHandler(this) + _calendarDelegate = CalendarDelegate(null, context!!) + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + _calendarDelegate = CalendarDelegate(binding, context!!) + binding.addRequestPermissionsResultListener(_calendarDelegate) + } + + override fun onDetachedFromActivityForConfigChanges() { + activity = null + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity + _calendarDelegate = CalendarDelegate(binding, context!!) + binding.addRequestPermissionsResultListener(_calendarDelegate) + } + + override fun onDetachedFromActivity() { + activity = null + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + REQUEST_PERMISSIONS_METHOD -> { + _calendarDelegate.requestPermissions(result) + } + HAS_PERMISSIONS_METHOD -> { + _calendarDelegate.hasPermissions(result) + } + RETRIEVE_CALENDARS_METHOD -> { + _calendarDelegate.retrieveCalendars(result) + } + RETRIEVE_EVENTS_METHOD -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT) + 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 -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT) + val eventId = call.argument(EVENT_ID_ARGUMENT) + + _calendarDelegate.deleteEvent(calendarId!!, eventId!!, result) + } + DELETE_EVENT_INSTANCE_METHOD -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT) + val eventId = call.argument(EVENT_ID_ARGUMENT) + val startDate = call.argument(EVENT_START_DATE_ARGUMENT) + val endDate = call.argument(EVENT_END_DATE_ARGUMENT) + val followingInstances = call.argument(FOLLOWING_INSTANCES) + + _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 + ) + } + DELETE_CALENDAR_METHOD -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT) + _calendarDelegate.deleteCalendar(calendarId!!, result) + } + else -> { + result.notImplemented() + } + } + } + + private fun parseEventArgs(call: MethodCall, calendarId: String?): Event { + val event = Event() + event.eventTitle = call.argument(EVENT_TITLE_ARGUMENT) + event.calendarId = calendarId + event.eventId = call.argument(EVENT_ID_ARGUMENT) + event.eventDescription = call.argument(EVENT_DESCRIPTION_ARGUMENT) + event.eventAllDay = call.argument(EVENT_ALL_DAY_ARGUMENT) ?: false + event.eventStartDate = call.argument(EVENT_START_DATE_ARGUMENT)!! + event.eventEndDate = call.argument(EVENT_END_DATE_ARGUMENT)!! + event.eventStartTimeZone = call.argument(EVENT_START_TIMEZONE_ARGUMENT) + event.eventEndTimeZone = call.argument(EVENT_END_TIMEZONE_ARGUMENT) + event.eventLocation = call.argument(EVENT_LOCATION_ARGUMENT) + event.eventURL = call.argument(EVENT_URL_ARGUMENT) + 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 + ) { + val recurrenceRule = parseRecurrenceRuleArgs(call) + event.recurrenceRule = recurrenceRule + } + + 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( + attendeeArgs[EMAIL_ADDRESS_ARGUMENT] as String, + attendeeArgs[NAME_ARGUMENT] as String?, + attendeeArgs[ROLE_ARGUMENT] as Int, + attendeeArgs[ATTENDANCE_STATUS_ARGUMENT] as Int?, + null, 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 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(UNTIL_ARGUMENT)) { + recurrenceRule.until = recurrenceRuleArgs[UNTIL_ARGUMENT] as String? + } + + if (recurrenceRuleArgs.containsKey(BY_WEEK_DAYS_ARGUMENT)) { + recurrenceRule.byday = + recurrenceRuleArgs[BY_WEEK_DAYS_ARGUMENT].toListOf()?.toMutableList() + } + + if (recurrenceRuleArgs.containsKey(BY_MONTH_DAYS_ARGUMENT)) { + recurrenceRule.bymonthday = + recurrenceRuleArgs[BY_MONTH_DAYS_ARGUMENT] as MutableList? + } + + if (recurrenceRuleArgs.containsKey(BY_YEAR_DAYS_ARGUMENT)) { + recurrenceRule.byyearday = + recurrenceRuleArgs[BY_YEAR_DAYS_ARGUMENT] as MutableList? + } + + 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 + } + + private inline fun Any?.toListOf(): List? { + return (this as List<*>?)?.filterIsInstance()?.toList() + } + + private fun parseAvailability(value: String?): Availability? = + if (value == null || value == Constants.AVAILABILITY_UNAVAILABLE) { + null + } else { + Availability.valueOf(value) + } + + private fun parseEventStatus(value: String?): EventStatus? = + if (value == null || value == Constants.EVENT_STATUS_NONE) { + null + } else { + EventStatus.valueOf(value) + } +} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt new file mode 100644 index 00000000..4883b7a7 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/EventStatusSerializer.kt @@ -0,0 +1,15 @@ +package com.builttoroam.devicecalendar + +import com.builttoroam.devicecalendar.models.EventStatus +import com.google.gson.* +import java.lang.reflect.Type + +class EventStatusSerializer: JsonSerializer { + override fun serialize(src: EventStatus?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { + if(src != null) { + return JsonPrimitive(src.name) + } + 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 new file mode 100644 index 00000000..052c7278 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -0,0 +1,117 @@ +package com.builttoroam.devicecalendar.common + +import android.provider.CalendarContract + +class Constants { + companion object { + const val CALENDAR_PROJECTION_ID_INDEX: Int = 0 + const val CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX: Int = 1 + const val CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX: Int = 2 + const val CALENDAR_PROJECTION_DISPLAY_NAME_INDEX: Int = 3 + const val CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX: Int = 4 + const val CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX: Int = 5 + const val CALENDAR_PROJECTION_COLOR_INDEX: Int = 6 + const val CALENDAR_PROJECTION_IS_PRIMARY_INDEX: Int = 7 + + // 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 + + ) + + // 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 + ) + + const val EVENT_PROJECTION_ID_INDEX: Int = 0 + const val EVENT_PROJECTION_TITLE_INDEX: Int = 1 + const val EVENT_PROJECTION_DESCRIPTION_INDEX: Int = 2 + const val EVENT_PROJECTION_BEGIN_INDEX: Int = 3 + const val EVENT_PROJECTION_END_INDEX: Int = 4 + const val EVENT_PROJECTION_RECURRING_RULE_INDEX: Int = 7 + const val EVENT_PROJECTION_ALL_DAY_INDEX: Int = 8 + const val EVENT_PROJECTION_EVENT_LOCATION_INDEX: Int = 9 + const val EVENT_PROJECTION_CUSTOM_APP_URI_INDEX: Int = 10 + const val EVENT_PROJECTION_START_TIMEZONE_INDEX: Int = 11 + const val EVENT_PROJECTION_END_TIMEZONE_INDEX: Int = 12 + 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, + CalendarContract.Events.TITLE, + CalendarContract.Events.DESCRIPTION, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END, + CalendarContract.Instances.DURATION, + CalendarContract.Events.RDATE, + CalendarContract.Events.RRULE, + CalendarContract.Events.ALL_DAY, + CalendarContract.Events.EVENT_LOCATION, + CalendarContract.Events.CUSTOM_APP_URI, + CalendarContract.Events.EVENT_TIMEZONE, + CalendarContract.Events.EVENT_END_TIMEZONE, + CalendarContract.Events.AVAILABILITY, + CalendarContract.Events.STATUS, + CalendarContract.Events.EVENT_COLOR, + ) + + const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 + const val EVENT_INSTANCE_DELETION_RRULE_INDEX: Int = 1 + const val EVENT_INSTANCE_DELETION_LAST_DATE_INDEX: Int = 2 + const val EVENT_INSTANCE_DELETION_BEGIN_INDEX: Int = 3 + 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 + ) + + const val ATTENDEE_ID_INDEX: Int = 0 + const val ATTENDEE_EVENT_ID_INDEX: Int = 1 + const val ATTENDEE_NAME_INDEX: Int = 2 + const val ATTENDEE_EMAIL_INDEX: Int = 3 + const val ATTENDEE_TYPE_INDEX: Int = 4 + const val ATTENDEE_RELATIONSHIP_INDEX: Int = 5 + 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 + ) + + const val REMINDER_MINUTES_INDEX = 1 + val REMINDER_PROJECTION: Array = arrayOf( + CalendarContract.Reminders.EVENT_ID, + CalendarContract.Reminders.MINUTES + ) + + const val AVAILABILITY_UNAVAILABLE = "UNAVAILABLE" + + const val EVENT_STATUS_NONE = "NONE" + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt new file mode 100644 index 00000000..3509ad11 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorCodes.kt @@ -0,0 +1,11 @@ +package com.builttoroam.devicecalendar.common + +class ErrorCodes { + companion object { + const val INVALID_ARGUMENT: String = "400" + const val NOT_FOUND: String = "404" + const val NOT_ALLOWED: String = "405" + const val NOT_AUTHORIZED: String = "401" + const val GENERIC_ERROR: String = "500" + } +} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt new file mode 100644 index 00000000..e8486baa --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt @@ -0,0 +1,16 @@ +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)" + } +} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt new file mode 100644 index 00000000..825ca964 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt @@ -0,0 +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 diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt new file mode 100644 index 00000000..0ac7faa1 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Availability.kt @@ -0,0 +1,7 @@ +package com.builttoroam.devicecalendar.models + +enum class Availability { + BUSY, + FREE, + TENTATIVE +} \ 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 new file mode 100644 index 00000000..6e10b7fe --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -0,0 +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? +) { + var isReadOnly: Boolean = false + var isDefault: Boolean = false +} diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt new file mode 100644 index 00000000..22bb4c4b --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt @@ -0,0 +1,16 @@ +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 +) { + 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 new file mode 100644 index 00000000..a3918d75 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt @@ -0,0 +1,23 @@ +package com.builttoroam.devicecalendar.models + +class Event { + var eventTitle: String? = null + var eventId: String? = null + var calendarId: String? = null + var eventDescription: String? = null + var eventStartDate: Long? = null + var eventEndDate: Long? = null + var eventStartTimeZone: String? = null + var eventEndTimeZone: String? = null + var eventAllDay: Boolean = false + var eventLocation: String? = null + var eventURL: String? = null + var attendees: MutableList = mutableListOf() + var recurrenceRule: RecurrenceRule? = null + var organizer: Attendee? = null + var reminders: MutableList = mutableListOf() + var availability: Availability? = null + var eventStatus: EventStatus? = null + var eventColor: Long? = null + +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt new file mode 100644 index 00000000..c8422795 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/EventStatus.kt @@ -0,0 +1,7 @@ +package com.builttoroam.devicecalendar.models + +enum class EventStatus { + CONFIRMED, + CANCELED, + TENTATIVE +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt new file mode 100644 index 00000000..1da83111 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt @@ -0,0 +1,17 @@ +package com.builttoroam.devicecalendar.models + +import org.dmfs.rfc5545.recur.Freq + +class RecurrenceRule(val freq: Freq) { + var count: Int? = null + var interval: 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/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt new file mode 100644 index 00000000..c9695796 --- /dev/null +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Reminder.kt @@ -0,0 +1,3 @@ +package com.builttoroam.devicecalendar.models + +class Reminder(val minutes: Int) \ No newline at end of file diff --git a/device_calendar.iml b/device_calendar.iml new file mode 100644 index 00000000..73e7ebd0 --- /dev/null +++ b/device_calendar.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/device_calendar_android.iml b/device_calendar_android.iml new file mode 100644 index 00000000..ac5d744d --- /dev/null +++ b/device_calendar_android.iml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 00000000..dee655cc --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ + +.flutter-plugins diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 00000000..8cab361b --- /dev/null +++ b/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 44b7e7d3f42f050a79712daab253af06e9daf530 + channel: beta diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..a24543bc --- /dev/null +++ b/example/README.md @@ -0,0 +1,195 @@ +# Examples + +Most of the APIs are covered in [calendar_event.dart](https://github.com/builttoroam/device_calendar/blob/master/example/lib/presentation/pages/calendar_event.dart) or [calendar_events.dart](https://github.com/builttoroam/device_calendar/blob/master/example/lib/presentation/pages/calendar_events.dart) files in the example app. +You'll be able to get a reference of how the APIs are used. + +For a full API reference, the documentation can be found at [pub.dev](https://pub.dev/documentation/device_calendar/latest/device_calendar/device_calendar-library.html). + +## DayOfWeekGroup Enum + +`DayOfWeekGroup` enum allows to explicitly choose and return a list of `DayOfWeek` enum values by using an extension `getDays`: + +* `DayOfWeekGroup.Weekday.getDays` will return: + + ```dart + [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday]; + ``` + +* `DayOfWeekGroup.Weekend.getDays` will return: + + ```dart + [DayOfWeek.Saturday, DayOfWeek.Sunday]; + ``` + +* `DayOfWeekGroup.Alldays.getDays` will return: + + ```dart + [DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday]; + ``` + +## Attendee Examples + +Examples below present on how to initialise an `Attendee` model in Dart: + +* A required attendee: + + ```dart + Attendee( + name: 'Test User 1', + emailAddress: 'test1@example.com', + role: AttendeeRole.Required); + ``` + +* An optional attendee: + + ```dart + Attendee( + name: 'Test User 2', + emailAddress: 'test2@example.com', + role: AttendeeRole.Optional); + ``` + +## Reminder Examples + +Examples below present on how to initialise a `Reminder` model in Dart: + +* 30 minutes + + ```dart + Reminder(minutes: 30); + ``` + +* 1 day + + ```dart + Reminder(minutes: 1440); + ``` + +## Recurrence Rule Examples + +Examples below present sample parameters of recurrence rules received by each platform and required properties for the `RecurrenceRule` model in Dart.\ +**Please note**: Receiving monthly and yearly recurrence parameters are slightly different for the two platforms. + +You can find more standard examples at [iCalendar.org](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html). + +### **Daily Rule** + +Daily every 5 days and end after 3 occurrences + +* Recurrence parameter example (Android and iOS):\ +`FREQ=DAILY;INTERVAL=5;COUNT=3` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Daily, + interval: 5, + totalOccurrences: 3); + ``` + +### **Weekly Rule** + +Weekly on Monday, Tuesday and Saturday every 2 weeks and end on 31 Jan 2020 + +* Recurrence parameter example (Android and iOS):\ +`FREQ=WEEKLY;BYDAY=MO,TU,SA;INTERVAL=2;UNTIL=20200130T130000Z` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Weekly, + interval: 2, + endDate: DateTime(2020, 1, 31), + daysOfWeek: [ DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Saturday ]); + ``` + +### **Monthly/Yearly SetPosition (Week Number) Rule** + +Monthly on third Thursday + +* Recurrence parameter example (Android):\ +`FREQ=MONTHLY;INTERVAL=1;BYDAY=3TH` +* Recurrence parameter example (iOS):\ +`FREQ=MONTHLY;INTERVAL=1;BYDAY=TH;BYSETPOS=3` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Monthly, + interval: 1, + daysOfWeek: [ DayOfWeek.Thursday ], + weekOfMonth: WeekNumber.Third); + ``` + +Monthly on last Thursday + +* Recurrence parameter example (Android and iOS):\ +`FREQ=MONTHLY;INTERVAL=1;BYDAY=-1TH` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Monthly, + interval: 1, + daysOfWeek: [ DayOfWeek.Thursday ], + weekOfMonth: WeekNumber.Last); + ``` + +Yearly on third Thursday of January + +* Recurrence parameter example (Android and iOS):\ +`FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYDAY=3TH` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Yearly, + interval: 1, + monthOfYear: MonthOfYear.January, + weekOfMonth: WeekNumber.Third); + ``` + +Yearly on last Thursday of January + +* Recurrence parameter example (Android and iOS):\ +`FREQ=YEARLY;INTERVAL=1;BYMONTH=1;BYDAY=-1TH` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Yearly, + interval: 1, + monthOfYear: MonthOfYear.January, + weekOfMonth: WeekNumber.Last); + ``` + +### **Monthly/Yearly By Day of a Month Rule** + +Monthly on 8th + +* Recurrence parameter example (Android and iOS):\ +`FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=8` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Monthly, + interval: 1, + dayOfMonth: 8); + ``` + +Yearly on 8th of February + +* Recurrence parameter example (Android):\ +`FREQ=YEARLY;INTERVAL=1;BYMONTHDAY=8;BYMONTH=2` +* Recurrence parameter example (iOS):\ +`FREQ=YEARLY;INTERVAL=1` +* Dart example: + + ```dart + RecurrenceRule( + RecurrenceFrequency.Yearly, + interval: 1, + monthOfYear: MonthOfYear.February, + dayOfMonth: 8); + ``` diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 00000000..3e1200f9 --- /dev/null +++ b/example/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 diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 00000000..65b7315a --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,10 @@ +*.iml +*.class +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +GeneratedPluginRegistrant.java diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 00000000..776dc817 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,60 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 32 + ndkVersion '22.1.7171670' + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.builttoroam.devicecalendarexample" + minSdkVersion 19 + targetSdkVersion 31 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} diff --git a/example/android/app/proguard-rules.pro b/example/android/app/proguard-rules.pro new file mode 100644 index 00000000..77023c9c --- /dev/null +++ b/example/android/app/proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.builttoroam.devicecalendar.** { *; } \ No newline at end of file diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..630265e9 --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt b/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt new file mode 100644 index 00000000..2d71b1d4 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/builttoroam/devicecalendarexample/MainActivity.kt @@ -0,0 +1,6 @@ +package com.builttoroam.devicecalendarexample + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..00fa4417 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 00000000..d3f65307 --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + ext.kotlin_version = '1.6.0' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.1.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 00000000..946d709d --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1536M \ No newline at end of file diff --git a/example/android/gradle/wrapper/gradle-wrapper.jar b/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..6f5b3ec5 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jun 16 16:20:15 AEST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip diff --git a/example/android/gradlew b/example/android/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/example/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/example/android/gradlew.bat b/example/android/gradlew.bat new file mode 100644 index 00000000..8a0b282a --- /dev/null +++ b/example/android/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 00000000..5a2f14fb --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/example/device_calendar_example.iml b/example/device_calendar_example.iml new file mode 100644 index 00000000..c92516a5 --- /dev/null +++ b/example/device_calendar_example.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/example/device_calendar_example_android.iml b/example/device_calendar_example_android.iml new file mode 100644 index 00000000..b050030a --- /dev/null +++ b/example/device_calendar_example_android.iml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + diff --git a/example/integration_test/app_test.dart b/example/integration_test/app_test.dart new file mode 100644 index 00000000..6e4a1908 --- /dev/null +++ b/example/integration_test/app_test.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:uuid/uuid.dart'; + +import 'package:device_calendar_example/main.dart' as app; + +/// NOTE: These integration tests are currently made to be run on a physical device where there is at least a calendar that can be written to. +/// Calendar permissions are needed. See example/test_driver/integration_test.dart for how to run this on Android +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('Calendar plugin example', () { + final eventTitle = const Uuid().v1(); + final saveEventButtonFinder = find.byKey(const Key('saveEventButton')); + final eventTitleFinder = find.text(eventTitle); + final firstWritableCalendarFinder = + find.byKey(const Key('writableCalendar0')); + final addEventButtonFinder = find.byKey(const Key('addEventButton')); + final titleFieldFinder = find.byKey(const Key('titleField')); + final deleteButtonFinder = find.byKey(const Key('deleteEventButton')); +//TODO: remove redundant restarts. Currently needed because the first screen is always "test starting..." + testWidgets('starts on calendars page', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('calendarsPage')), findsOneWidget); + }); + testWidgets('select first writable calendar', (WidgetTester tester) async { + app.main(); + + await tester.pumpAndSettle(const Duration(milliseconds: 500)); + expect(firstWritableCalendarFinder, findsOneWidget); + }); + testWidgets('go to add event page', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(firstWritableCalendarFinder); + + await tester.pumpAndSettle(); + expect(addEventButtonFinder, findsOneWidget); + print('found add event button'); + await tester.tap(addEventButtonFinder); + await tester.pumpAndSettle(); + expect(saveEventButtonFinder, findsOneWidget); + }); + testWidgets('try to save event without entering mandatory fields', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(firstWritableCalendarFinder); + await tester.pumpAndSettle(); + await tester.tap(addEventButtonFinder); + + await tester.pumpAndSettle(); + await tester.tap(saveEventButtonFinder); + await tester.pumpAndSettle(); + expect(find.text('Please fix the errors in red before submitting.'), + findsOneWidget); + }); + testWidgets('save event with title $eventTitle', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(firstWritableCalendarFinder); + await tester.pumpAndSettle(); + await tester.tap(addEventButtonFinder); + + await tester.pumpAndSettle(); + await tester.tap(titleFieldFinder); + + await tester.enterText(titleFieldFinder, eventTitle); + await tester.tap(saveEventButtonFinder); + await tester.pumpAndSettle(); + expect(eventTitleFinder, findsOneWidget); + }); + testWidgets('delete event with title $eventTitle', + (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + await tester.tap(firstWritableCalendarFinder); + await tester.pumpAndSettle(); + await tester.tap(eventTitleFinder); + + await tester.scrollUntilVisible(deleteButtonFinder, -5); + await tester.tap(deleteButtonFinder); + await tester.pumpAndSettle(); + expect(eventTitleFinder, findsNothing); + }); + }); +} diff --git a/example/integration_test/integration_test.dart b/example/integration_test/integration_test.dart new file mode 100644 index 00000000..ca6e9ef3 --- /dev/null +++ b/example/integration_test/integration_test.dart @@ -0,0 +1,8 @@ +import 'package:integration_test/integration_test_driver.dart'; + +/// Instruction for iOS: +/// See `ios.sh` +/// Instruction for android: +/// See `integration_test_android.dart` + +Future main() => integrationDriver(); diff --git a/example/integration_test/integration_test_android.dart b/example/integration_test/integration_test_android.dart new file mode 100644 index 00000000..2c840da5 --- /dev/null +++ b/example/integration_test/integration_test_android.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:integration_test/integration_test_driver.dart'; + +// make sure 'adb devices' works on your local machine, then from the root of the plugin, run the following: +/* +1. +cd example +2. +flutter drive --driver=integration_test/integration_test.dart --target=integration_test/app_test.dart + */ + +Future main() async { + await Process.run('adb', [ + 'shell', + 'pm', + 'grant', + 'com.builttoroam.devicecalendarexample', + 'android.permission.READ_CALENDAR' + ]); + await Process.run('adb', [ + 'shell', + 'pm', + 'grant', + 'com.builttoroam.devicecalendarexample', + 'android.permission.WRITE_CALENDAR' + ]); + await integrationDriver(); +} diff --git a/example/integration_test/ios.sh b/example/integration_test/ios.sh new file mode 100755 index 00000000..8f8eede4 --- /dev/null +++ b/example/integration_test/ios.sh @@ -0,0 +1,24 @@ +# Use: integration_test/ios.sh +# +# Executes the device_calendar integration test for iOS +# This script creates and starts a new iOS simulator, grants calendar permission +# to the app then runs the integration tests and finally deletes the simulator. +# +# Prerequisites: Xcode, Xcode Command Line Tools, Xcode iOS Simulator +# +# To run an integration test, make sure the script has execute permission +# example: `chmod +x example/integration_test/ios.sh` then: +# 1. cd example +# 2. integration_test/ios.sh +# 3. You should see `All tests passed` +# +# Success - "All tests passed." is printed to the console +# +deviceId=$(xcrun simctl create builtToRoamCalendarTest "iPhone 13" 2> /dev/null | tail -1) +echo "Created device: $deviceId, booting..." +xcrun simctl boot $deviceId +xcrun simctl privacy $deviceId grant calendar com.builttoroam.deviceCalendarExample00 +echo "Running tests..." +flutter drive --driver=integration_test/integration_test.dart --target=integration_test/app_test.dart -d $deviceId +echo "Removing device: $deviceId" +xcrun simctl delete $deviceId diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100755 index 00000000..1c202be0 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,45 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/app.flx +/Flutter/app.zip +/Flutter/flutter_assets/ +/Flutter/App.framework +/Flutter/Flutter.framework +/Flutter/Generated.xcconfig +/ServiceDefinitions.json + +**/.symlinks/ +Pods/ diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100755 index 00000000..9b41e7d8 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 11.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100755 index 00000000..e8efba11 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100755 index 00000000..399e9340 --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 00000000..997d1cb3 --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,45 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + flutter_additional_ios_build_settings(target) + config.build_settings['SWIFT_VERSION'] = '5.0' + # Or whatever Swift version your app is using that works with your plugins + end + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100755 index 00000000..cb8f159b --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,34 @@ +PODS: + - device_calendar (0.0.1): + - Flutter + - Flutter (1.0.0) + - flutter_native_timezone (0.0.1): + - Flutter + - integration_test (0.0.1): + - Flutter + +DEPENDENCIES: + - device_calendar (from `.symlinks/plugins/device_calendar/ios`) + - Flutter (from `Flutter`) + - flutter_native_timezone (from `.symlinks/plugins/flutter_native_timezone/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + +EXTERNAL SOURCES: + device_calendar: + :path: ".symlinks/plugins/device_calendar/ios" + Flutter: + :path: Flutter + flutter_native_timezone: + :path: ".symlinks/plugins/flutter_native_timezone/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + +SPEC CHECKSUMS: + device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 + integration_test: 13825b8a9334a850581300559b8839134b124670 + +PODFILE CHECKSUM: 10625bdc9b9ef8574174815aabd5b048e6e29bff + +COCOAPODS: 1.11.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..160e1d14 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,507 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 763C5C3662C48FEF7F2B0120 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E098C60D243A71853922C094 /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2AD784C0989B63BAA46EFDFD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E098C60D243A71853922C094 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F504DA6DD95FB326B0725EEE /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 763C5C3662C48FEF7F2B0120 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0C6DE7144DB7716571BF5210 /* Pods */ = { + isa = PBXGroup; + children = ( + F504DA6DD95FB326B0725EEE /* Pods-Runner.debug.xcconfig */, + 2AD784C0989B63BAA46EFDFD /* Pods-Runner.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 6400E78A7B626A4B08303DA0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E098C60D243A71853922C094 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 0C6DE7144DB7716571BF5210 /* Pods */, + 6400E78A7B626A4B08303DA0 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + A6EB42BBC2C9005F5277AB4F /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 122A90E1AB6EBE6FCB241A06 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = PG8Q9ZR89L; + LastSwiftMigration = 1130; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 122A90E1AB6EBE6FCB241A06 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/device_calendar/device_calendar.framework", + "${BUILT_PRODUCTS_DIR}/flutter_native_timezone/flutter_native_timezone.framework", + "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_calendar.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_timezone.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + A6EB42BBC2C9005F5277AB4F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PG8Q9ZR89L; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = PG8Q9ZR89L; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.builttoroam.deviceCalendarExample00; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_SWIFT3_OBJC_INFERENCE = On; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100755 index 00000000..919434a6 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100755 index 00000000..14d255fd --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100755 index 00000000..21a3cc14 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 00000000..18d98100 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100755 index 00000000..70693e4a --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 00000000..d36b1fab --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100755 index 0000000000000000000000000000000000000000..3d43d11e66f4de3da27ed045ca4fe38ad8b48094 GIT binary patch literal 11112 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 P-1+_A+?hGkL;m(J7kc&K literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100755 index 0000000000000000000000000000000000000000..28c6bf03016f6c994b70f38d1b7346e5831b531f GIT binary patch literal 564 zcmV-40?Yl0P)Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 zcmVjJBgitF5mAp-i>4+KS_oR{|13AP->1TD4=w)g|)JHOx|a2Wk1Va z!k)vP$UcQ#mdj%wNQoaJ!w>jv_6&JPyutpQps?s5dmDQ>`%?Bvj>o<%kYG!YW6H-z zu`g$@mp`;qDR!51QaS}|ZToSuAGcJ7$2HF0z`ln4t!#Yg46>;vGG9N9{V@9z#}6v* zfP?}r6b{*-C*)(S>NECI_E~{QYzN5SXRmVnP<=gzP+_Sp(Aza_hKlZ{C1D&l*(7IKXxQC1Z9#6wx}YrGcn~g%;icdw>T0Rf^w0{ z$_wn1J+C0@!jCV<%Go5LA45e{5gY9PvZp8uM$=1}XDI+9m7!A95L>q>>oe0$nC->i zeexUIvq%Uk<-$>DiDb?!In)lAmtuMWxvWlk`2>4lNuhSsjAf2*2tjT`y;@d}($o)S zn(+W&hJ1p0xy@oxP%AM15->wPLp{H!k)BdBD$toBpJh+crWdsNV)qsHaqLg2_s|Ih z`8E9z{E3sA!}5aKu?T!#enD(wLw?IT?k-yWVHZ8Akz4k5(TZJN^zZgm&zM28sfTD2BYJ|Fde3Xzh;;S` z=GXTnY4Xc)8nYoz6&vF;P7{xRF-{|2Xs5>a5)@BrnQ}I(_x7Cgpx#5&Td^4Q9_FnQ zX5so*;#8-J8#c$OlA&JyPp$LKUhC~-e~Ij!L%uSMu!-VZG7Hx-L{m2DVR2i=GR(_% zCVD!4N`I)&Q5S`?P&fQZ=4#Dgt_v2-DzkT}K(9gF0L(owe-Id$Rc2qZVLqI_M_DyO z9@LC#U28_LU{;wGZ&))}0R2P4MhajKCd^K#D+JJ&JIXZ_p#@+7J9A&P<0kdRujtQ_ zOy>3=C$kgi6$0pW06KaLz!21oOryKM3ZUOWqppndxfH}QpgjEJ`j7Tzn5bk6K&@RA?vl##y z$?V~1E(!wB5rH`>3nc&@)|#<1dN2cMzzm=PGhQ|Yppne(C-Vlt450IXc`J4R0W@I7 zd1e5uW6juvO%ni(WX7BsKx3MLngO7rHO;^R5I~0^nE^9^E_eYLgiR9&KnJ)pBbfno zSVnW$0R+&6jOOsZ82}nJ126+c|%svPo;TeUku<2G7%?$oft zyaO;tVo}(W)VsTUhq^XmFi#2z%-W9a{7mXn{uzivYQ_d6b7VJG{77naW(vHt-uhnY zVN#d!JTqVh(7r-lhtXVU6o})aZbDt_;&wJVGl2FKYFBFpU-#9U)z#(A%=IVnqytR$SY-sO( z($oNE09{D^@OuYPz&w~?9>Fl5`g9u&ecFGhqX=^#fmR=we0CJw+5xna*@oHnkahk+ z9aWeE3v|An+O5%?4fA&$Fgu~H_YmqR!yIU!bFCk4!#pAj%(lI(A5n)n@Id#M)O9Yx zJU9oKy{sRAIV3=5>(s8n{8ryJ!;ho}%pn6hZKTKbqk=&m=f*UnK$zW3YQP*)pw$O* zIfLA^!-bmBl6%d_n$#tP8Zd_(XdA*z*WH|E_yILwjtI~;jK#v-6jMl^?<%Y%`gvpwv&cFb$||^v4D&V=aNy?NGo620jL3VZnA%s zH~I|qPzB~e(;p;b^gJr7Ure#7?8%F0m4vzzPy^^(q4q1OdthF}Fi*RmVZN1OwTsAP zn9CZP`FazX3^kG(KodIZ=Kty8DLTy--UKfa1$6XugS zk%6v$Kmxt6U!YMx0JQ)0qX*{CXwZZk$vEROidEc7=J-1;peNat!vS<3P-FT5po>iE z!l3R+<`#x|+_hw!HjQGV=8!q|76y8L7N8gP3$%0kfush|u0uU^?dKBaeRSBUpOZ0c z62;D&Mdn2}N}xHRFTRI?zRv=>=AjHgH}`2k4WK=#AHB)UFrR-J87GgX*x5fL^W2#d z=(%K8-oZfMO=i{aWRDg=FX}UubM4eotRDcn;OR#{3q=*?3mE3_oJ-~prjhxh%PgQT zyn)Qozaq0@o&|LEgS{Ind4Swsr;b`u185hZPOBLL<`d2%^Yp1?oL)=jnLi;Zo0ZDliTtQ^b5SmfIMe{T==zZkbvn$KTQGlbG8w}s@M3TZnde;1Am46P3juKb zl9GU&3F=q`>j!`?SyH#r@O59%@aMX^rx}Nxe<>NqpUp5=lX1ojGDIR*-D^SDuvCKF z?3$xG(gVUsBERef_YjPFl^rU9EtD{pt z0CXwpN7BN3!8>hajGaTVk-wl=9rxmfWtIhC{mheHgStLi^+Nz12a?4r(fz)?3A%at zMlvQmL<2-R)-@G1wJ0^zQK%mR=r4d{Y3fHp){nWXUL#|CqXl(+v+qDh>FkF9`eWrW zfr^D%LNfOcTNvtx0JXR35J0~Jpi2#P3Q&80w+nqNfc}&G0A~*)lGHKv=^FE+b(37|)zL;KLF>oiGfb(?&1 zV3XRu!Sw>@quKiab%g6jun#oZ%!>V#A%+lNc?q>6+VvyAn=kf_6z^(TZUa4Eelh{{ zqFX-#dY(EV@7l$NE&kv9u9BR8&Ojd#ZGJ6l8_BW}^r?DIS_rU2(XaGOK z225E@kH5Opf+CgD^{y29jD4gHbGf{1MD6ggQ&%>UG4WyPh5q_tb`{@_34B?xfSO*| zZv8!)q;^o-bz`MuxXk*G^}(6)ACb@=Lfs`Hxoh>`Y0NE8QRQ!*p|SH@{r8=%RKd4p z+#Ty^-0kb=-H-O`nAA3_6>2z(D=~Tbs(n8LHxD0`R0_ATFqp-SdY3(bZ3;VUM?J=O zKCNsxsgt@|&nKMC=*+ZqmLHhX1KHbAJs{nGVMs6~TiF%Q)P@>!koa$%oS zjXa=!5>P`vC-a}ln!uH1ooeI&v?=?v7?1n~P(wZ~0>xWxd_Aw;+}9#eULM7M8&E?Y zC-ZLhi3RoM92SXUb-5i-Lmt5_rfjE{6y^+24`y$1lywLyHO!)Boa7438K4#iLe?rh z2O~YGSgFUBH?og*6=r9rme=peP~ah`(8Zt7V)j5!V0KPFf_mebo3z95U8(up$-+EA^9dTRLq>Yl)YMBuch9%=e5B`Vnb>o zt03=kq;k2TgGe4|lGne&zJa~h(UGutjP_zr?a7~#b)@15XNA>Dj(m=gg2Q5V4-$)D|Q9}R#002ovPDHLkV1o7DH3k3x literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100755 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100755 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100755 index 00000000..89c2725b --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100755 index 00000000..f2e259c7 --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100755 index 00000000..f3c28516 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100755 index 00000000..ad25cf2b --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + NSContactsUsageDescription + Contacts are used for event attendee editing. + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + device_calendar_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSCalendarsUsageDescription + We need access to your calendar to help you track events + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100755 index 00000000..7335fdf9 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/example/lib/common/app_routes.dart b/example/lib/common/app_routes.dart new file mode 100644 index 00000000..991a9d70 --- /dev/null +++ b/example/lib/common/app_routes.dart @@ -0,0 +1,3 @@ +class AppRoutes { + static const calendars = '/'; +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 00000000..3b5d61ee --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +import 'common/app_routes.dart'; +import 'presentation/pages/calendars.dart'; + +void main() => runApp(const MyApp()); + +class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData(), + themeMode: ThemeMode.system, + darkTheme: ThemeData.dark(), + routes: { + AppRoutes.calendars: (context) { + return const CalendarsPage(key: Key('calendarsPage')); + } + }, + ); + } +} diff --git a/example/lib/presentation/date_time_picker.dart b/example/lib/presentation/date_time_picker.dart new file mode 100644 index 00000000..dc11e8d9 --- /dev/null +++ b/example/lib/presentation/date_time_picker.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import 'input_dropdown.dart'; + +class DateTimePicker extends StatelessWidget { + const DateTimePicker( + {Key? key, + this.labelText, + this.selectedDate, + this.selectedTime, + this.selectDate, + this.selectTime, + this.enableTime = true}) + : super(key: key); + + final String? labelText; + final DateTime? selectedDate; + final TimeOfDay? selectedTime; + final ValueChanged? selectDate; + final ValueChanged? selectTime; + final bool enableTime; + + Future _selectDate(BuildContext context) async { + final picked = await showDatePicker( + context: context, + initialDate: selectedDate != null + ? DateTime.parse(selectedDate.toString()) + : DateTime.now(), + firstDate: DateTime(2015, 8), + lastDate: DateTime(2101)); + if (picked != null && picked != selectedDate && selectDate != null) { + selectDate!(picked); + } + } + + Future _selectTime(BuildContext context) async { + if (selectedTime == null) return; + final picked = + await showTimePicker(context: context, initialTime: selectedTime!); + if (picked != null && picked != selectedTime) selectTime!(picked); + } + + @override + Widget build(BuildContext context) { + final valueStyle = Theme.of(context).textTheme.headline6; + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + flex: 4, + child: InputDropdown( + labelText: labelText, + valueText: selectedDate == null + ? '' + : DateFormat.yMMMd().format(selectedDate as DateTime), + valueStyle: valueStyle, + onPressed: () { + _selectDate(context); + }, + ), + ), + if (enableTime) ...[ + const SizedBox(width: 12.0), + Expanded( + flex: 3, + child: InputDropdown( + valueText: selectedTime?.format(context) ?? '', + valueStyle: valueStyle, + onPressed: () { + _selectTime(context); + }, + ), + ), + ] + ], + ); + } +} diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart new file mode 100644 index 00000000..f91bb7de --- /dev/null +++ b/example/lib/presentation/event_item.dart @@ -0,0 +1,342 @@ +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 'recurring_event_dialog.dart'; + +class EventItem extends StatefulWidget { + final Event? _calendarEvent; + final DeviceCalendarPlugin _deviceCalendarPlugin; + final bool _isReadOnly; + + final Function(Event) _onTapped; + final VoidCallback _onLoadingStarted; + final Function(bool) _onDeleteFinished; + + const EventItem( + this._calendarEvent, + this._deviceCalendarPlugin, + this._onLoadingStarted, + this._onDeleteFinished, + this._onTapped, + this._isReadOnly, + {Key? key}) + : super(key: key); + + @override + State createState() { + return _EventItemState(); + } +} + +class _EventItemState extends State { + final double _eventFieldNameWidth = 75.0; + Location? _currentLocation; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => setCurentLocation()); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (widget._calendarEvent != null) { + widget._onTapped(widget._calendarEvent as Event); + } + }, + child: Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 10.0), + child: FlutterLogo(), + ), + ListTile( + title: Text(widget._calendarEvent?.title ?? ''), + subtitle: Text(widget._calendarEvent?.description ?? '')), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + if (_currentLocation != null) + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Starts'), + ), + Text( + widget._calendarEvent == null + ? '' + : _formatDateTime( + dateTime: widget._calendarEvent!.start!, + ), + ) + ], + ), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 5.0), + ), + if (_currentLocation != null) + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Ends'), + ), + Text( + widget._calendarEvent?.end == null + ? '' + : _formatDateTime( + dateTime: widget._calendarEvent!.end!, + ), + ), + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('All day?'), + ), + Text(widget._calendarEvent?.allDay != null && + widget._calendarEvent?.allDay == true + ? 'Yes' + : 'No') + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Location'), + ), + Expanded( + child: Text( + widget._calendarEvent?.location ?? '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('URL'), + ), + Expanded( + child: Text( + widget._calendarEvent?.url?.data?.contentText ?? '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Attendees'), + ), + Expanded( + child: Text( + widget._calendarEvent?.attendees + ?.where((a) => a?.name?.isNotEmpty ?? false) + .map((a) => a?.name) + .join(', ') ?? + '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Availability'), + ), + Expanded( + child: Text( + widget._calendarEvent?.availability.enumToString ?? + '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + const SizedBox( + height: 10.0, + ), + Align( + alignment: Alignment.topLeft, + child: Row( + children: [ + SizedBox( + width: _eventFieldNameWidth, + child: const Text('Status'), + ), + Expanded( + child: Text( + widget._calendarEvent?.status?.enumToString ?? '', + overflow: TextOverflow.ellipsis, + ), + ) + ], + ), + ), + ], + ), + ), + ButtonBar( + children: [ + if (!widget._isReadOnly) ...[ + IconButton( + onPressed: () { + if (widget._calendarEvent != null) { + widget._onTapped(widget._calendarEvent as Event); + } + }, + icon: const Icon(Icons.edit), + ), + IconButton( + onPressed: () async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + if (widget._calendarEvent?.recurrenceRule == null) { + return AlertDialog( + title: const Text( + 'Are you sure you want to delete this event?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () async { + Navigator.of(context).pop(); + widget._onLoadingStarted(); + final deleteResult = await widget + ._deviceCalendarPlugin + .deleteEvent( + widget._calendarEvent?.calendarId, + widget._calendarEvent?.eventId); + widget._onDeleteFinished( + deleteResult.isSuccess && + deleteResult.data != null); + }, + child: const Text('Delete'), + ), + ], + ); + } else { + if (widget._calendarEvent == null) { + return const SizedBox(); + } + return RecurringEventDialog( + widget._deviceCalendarPlugin, + widget._calendarEvent!, + widget._onLoadingStarted, + widget._onDeleteFinished); + } + }, + ); + }, + icon: const Icon(Icons.delete), + ), + ] else ...[ + IconButton( + onPressed: () { + if (widget._calendarEvent != null) { + widget._onTapped(widget._calendarEvent!); + } + }, + icon: const Icon(Icons.remove_red_eye), + ), + ] + ], + ) + ], + ), + ), + ); + } + + void setCurentLocation() async { + String? timezone; + try { + timezone = await FlutterNativeTimezone.getLocalTimezone(); + } catch (e) { + debugPrint('Could not get the local timezone'); + } + timezone ??= 'Etc/UTC'; + _currentLocation = timeZoneDatabase.locations[timezone]; + setState(() {}); + } + + /// Formats [dateTime] into a human-readable string. + /// If [_calendarEvent] is an Android allDay event, then the output will + /// omit the time. + String _formatDateTime({DateTime? dateTime}) { + if (dateTime == null) { + return 'Error'; + } + var output = ''; + if (Platform.isAndroid && widget._calendarEvent?.allDay == true) { + // just the dates, no times + output = DateFormat.yMd().format(dateTime); + } else { + output = DateFormat('yyyy-MM-dd HH:mm:ss') + .format(TZDateTime.from(dateTime, _currentLocation!)); + } + return output; + } +} diff --git a/example/lib/presentation/input_dropdown.dart b/example/lib/presentation/input_dropdown.dart new file mode 100644 index 00000000..a6c19820 --- /dev/null +++ b/example/lib/presentation/input_dropdown.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class InputDropdown extends StatelessWidget { + const InputDropdown( + {Key? key, + this.child, + this.labelText, + this.valueText, + this.valueStyle, + this.onPressed}) + : super(key: key); + + final String? labelText; + final String? valueText; + final TextStyle? valueStyle; + final VoidCallback? onPressed; + final Widget? child; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + child: InputDecorator( + decoration: InputDecoration( + labelText: labelText, + ), + baseStyle: valueStyle, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + if (valueText != null) Text(valueText!, style: valueStyle), + Icon(Icons.arrow_drop_down, + color: Theme.of(context).brightness == Brightness.light + ? Colors.grey.shade700 + : Colors.white70), + ], + ), + ), + ); + } +} diff --git a/example/lib/presentation/pages/calendar_add.dart b/example/lib/presentation/pages/calendar_add.dart new file mode 100644 index 00000000..7d6d8820 --- /dev/null +++ b/example/lib/presentation/pages/calendar_add.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +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(); + } +} + +class _CalendarAddPageState extends State { + final GlobalKey _formKey = GlobalKey(); + final GlobalKey _scaffoldKey = GlobalKey(); + late DeviceCalendarPlugin _deviceCalendarPlugin; + + AutovalidateMode _autovalidate = AutovalidateMode.disabled; + String _calendarName = ''; + ColorChoice? _colorChoice; + String _localAccountName = ''; + + _CalendarAddPageState() { + _deviceCalendarPlugin = DeviceCalendarPlugin(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Create Calendar'), + ), + body: Form( + autovalidateMode: _autovalidate, + key: _formKey, + child: Container( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + TextFormField( + decoration: const InputDecoration( + labelText: 'Calendar Name', + hintText: 'My New Calendar', + ), + validator: _validateCalendarName, + onSaved: (String? value) => _calendarName = value ?? '', + ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Calendar Color'), + DropdownButton( + onChanged: (selectedColor) { + setState(() => _colorChoice = selectedColor); + }, + value: _colorChoice, + items: ColorChoice.values + .map((color) => DropdownMenuItem( + value: color, + child: Text(color.toString().split('.').last), + )) + .toList(), + ), + ], + ), + if (Platform.isAndroid) + TextFormField( + decoration: const InputDecoration( + labelText: 'Local Account Name', + hintText: 'Device Calendar', + ), + onSaved: (String? value) => _localAccountName = value ?? '', + ), + ], + ), + ), + ), + floatingActionButton: FloatingActionButton( + 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.'); + } else { + form?.save(); + var result = await _deviceCalendarPlugin.createCalendar( + _calendarName, + calendarColor: _colorChoice?.value, + localAccountName: _localAccountName, + ); + + if (result.isSuccess) { + Navigator.pop(context, true); + } else { + showInSnackBar(result.errors + .map((err) => '[${err.errorCode}] ${err.errorMessage}') + .join(' | ')); + } + } + }, + child: const Icon(Icons.check), + ), + ); + } + + String? _validateCalendarName(String? value) { + if (value == null) return null; + if (value.isEmpty) { + return 'Calendar name is required.'; + } + + return null; + } + + void showInSnackBar(String value) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); + } +} + +enum ColorChoice { + Red, + Orange, + Yellow, + Green, + Blue, + Purple, + Brown, + Black, + White +} + +extension ColorChoiceExtension on ColorChoice { + static Color _value(ColorChoice val) { + switch (val) { + case ColorChoice.Red: + return Colors.red; + case ColorChoice.Orange: + return Colors.orange; + case ColorChoice.Yellow: + return Colors.yellow; + case ColorChoice.Green: + return Colors.green; + case ColorChoice.Blue: + return Colors.blue; + case ColorChoice.Purple: + return Colors.purple; + case ColorChoice.Brown: + return Colors.brown; + case ColorChoice.Black: + return Colors.black; + case ColorChoice.White: + return Colors.white; + default: + return Colors.red; + } + } + + Color get value => _value(this); +} diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart new file mode 100644 index 00000000..72c4cb5b --- /dev/null +++ b/example/lib/presentation/pages/calendar_event.dart @@ -0,0 +1,1261 @@ +import 'dart:io'; + +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 '../date_time_picker.dart'; +import '../recurring_event_dialog.dart'; +import 'event_attendee.dart'; +import 'event_reminders.dart'; + +enum RecurrenceRuleEndType { Indefinite, MaxOccurrences, SpecifiedEndDate } + +class CalendarEventPage extends StatefulWidget { + final Calendar _calendar; + final Event? _event; + final RecurringEventDialog? _recurringEventDialog; + + const CalendarEventPage(this._calendar, + [this._event, this._recurringEventDialog, Key? key]) + : super(key: key); + + @override + _CalendarEventPageState createState() { + return _CalendarEventPageState(_calendar, _event, _recurringEventDialog); + } +} + +class _CalendarEventPageState extends State { + final GlobalKey _formKey = GlobalKey(); + final GlobalKey _scaffoldKey = GlobalKey(); + final Calendar _calendar; + + Event? _event; + 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; + + TZDateTime? _endDate; + TimeOfDay? _endTime; + + AutovalidateMode _autovalidate = AutovalidateMode.disabled; + DayOfWeekGroup _dayOfWeekGroup = DayOfWeekGroup.None; + + RecurrenceRuleEndType _recurrenceRuleEndType = + RecurrenceRuleEndType.Indefinite; + RecurrenceRule? _rrule; + + final List _validDaysOfMonth = []; + + Availability _availability = Availability.Busy; + EventStatus? _eventStatus; + List? _attendees; + List? _reminders; + String _timezone = 'Etc/UTC'; + + _CalendarEventPageState( + this._calendar, this._event, this._recurringEventDialog) { + getCurentLocation(); + } + + void getCurentLocation() async { + try { + _timezone = await FlutterNativeTimezone.getLocalTimezone(); + } catch (e) { + debugPrint('Could not get the local timezone'); + } + + _deviceCalendarPlugin = DeviceCalendarPlugin(); + + final event = _event; + if (event == null) { + debugPrint( + 'calendar_event _timezone ------------------------- $_timezone'); + final currentLocation = timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + 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']; + 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, availability: _availability); + + debugPrint('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); + + _eventStatus = EventStatus.None; + } else { + 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); + } + + final attendees = event.attendees; + if (attendees != null && attendees.isNotEmpty) { + _attendees = []; + _attendees?.addAll(attendees as Iterable); + } + + final reminders = event.reminders; + if (reminders != null && reminders.isNotEmpty) { + _reminders = []; + _reminders?.addAll(reminders); + } + + final rrule = event.recurrenceRule; + if (rrule != null) { + // debugPrint('OLD_RRULE: ${rrule.toString()}'); + _rrule = rrule; + if (rrule.count != null) { + _recurrenceRuleEndType = RecurrenceRuleEndType.MaxOccurrences; + } + if (rrule.until != null) { + _recurrenceRuleEndType = RecurrenceRuleEndType.SpecifiedEndDate; + } + } + + _availability = event.availability; + _eventStatus = event.status; + } + + // Getting days of the current month (or a selected month for the yearly recurrence) as a default + _getValidDaysOfMonth(_rrule?.frequency); + setState(() {}); + } + + void printAttendeeDetails(Attendee attendee) { + debugPrint( + 'attendee name: ${attendee.name}, email address: ${attendee.emailAddress}, type: ${attendee.role?.enumToString}'); + debugPrint( + 'ios specifics - status: ${attendee.iosAttendeeDetails?.attendanceStatus}, type: ${attendee.iosAttendeeDetails?.attendanceStatus?.enumToString}'); + debugPrint( + 'android specifics - status ${attendee.androidAttendeeDetails?.attendanceStatus}, type: ${attendee.androidAttendeeDetails?.attendanceStatus?.enumToString}'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: Text(_event?.eventId?.isEmpty ?? true + ? 'Create event' + : _calendar.isReadOnly == true + ? 'View event ${_event?.title}' + : 'Edit event ${_event?.title}'), + ), + 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?.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; + }, + ), + ), + 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; + } + }, + ), + ), + ListTile( + leading: const Text( + 'Availability', + style: TextStyle(fontSize: 16), + ), + trailing: DropdownButton( + value: _availability, + onChanged: (Availability? newValue) { + setState(() { + if (newValue != null) { + _availability = newValue; + _event?.availability = newValue; + } + }); + }, + items: Availability.values + .map>( + (Availability value) { + return DropdownMenuItem( + value: value, + child: Text(value.enumToString), + ); + }).toList(), + ), + ), + if (Platform.isAndroid) + ListTile( + leading: const 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: const Text('All Day'), + ), + if (_startDate != null) + Padding( + padding: const EdgeInsets.all(10.0), + child: DateTimePicker( + labelText: 'From', + enableTime: _event?.allDay == false, + selectedDate: _startDate, + selectedTime: _startTime, + selectDate: (DateTime date) { + setState(() { + var currentLocation = + timeZoneDatabase.locations[_timezone]; + if (currentLocation != null) { + _startDate = + TZDateTime.from(date, currentLocation); + _event?.start = _combineDateWithTime( + _startDate, _startTime); + } + }); + }, + selectTime: (TimeOfDay time) { + setState( + () { + _startTime = time; + _event?.start = _combineDateWithTime( + _startDate, _startTime); + }, + ); + }, + ), + ), + 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); + _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: const 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) => + const EventAttendeePage())); + if (result != null) { + _attendees ??= []; + setState(() { + _attendees?.add(result); + }); + } + } + : null, + leading: const Icon(Icons.people), + title: Text(_calendar.isReadOnly == false + ? 'Add Attendees' + : 'Attendees'), + ), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: _attendees?.length ?? 0, + itemBuilder: (context, index) { + return Container( + color: (_attendees?[index].isOrganiser ?? false) + ? MediaQuery.of(context).platformBrightness == + Brightness.dark + ? Colors.black26 + : 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), + 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 ?? + 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: const 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: const 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: const 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 ?? true) + Text(_calendar.isReadOnly == false + ? 'Add reminders' + : 'Reminders'), + for (var reminder in _reminders ?? []) + Text('${reminder.minutes} minutes before; ') + ], + ), + ), + ), + ), + CheckboxListTile( + value: _rrule != null, + title: const Text('Is recurring'), + onChanged: (isChecked) { + if (isChecked != null) { + setState(() { + if (isChecked) { + _rrule = + RecurrenceRule(frequency: Frequency.daily); + } else { + _rrule = null; + } + }); + } + }, + ), + if (_rrule != null) ...[ + ListTile( + leading: const Text('Select a Recurrence Type'), + trailing: DropdownButton( + onChanged: (selectedFrequency) { + setState(() { + _onFrequencyChange( + selectedFrequency ?? Frequency.daily); + _getValidDaysOfMonth(selectedFrequency); + }); + }, + 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(), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 10), + child: Row( + children: [ + const Text('Repeat Every '), + Flexible( + child: TextFormField( + initialValue: '${_rrule?.interval ?? 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) { + _rrule = _rrule?.copyWith( + interval: int.tryParse(value)); + } + }, + ), + ), + _recurrenceFrequencyToIntervalText( + _rrule?.frequency), + ], + ), + ), + 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(() { + 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: 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: const Text('Delete'), + ), + ], + ], + ), + ), + ), + ), + floatingActionButton: Visibility( + visible: _calendar.isReadOnly == false, + child: FloatingActionButton( + key: const Key('saveEventButton'), + onPressed: () async { + final form = _formKey.currentState; + if (form?.validate() == false) { + _autovalidate = + AutovalidateMode.always; // Start validating on every change. + showInSnackBar( + context, 'Please fix the errors in red before submitting.'); + return; + } else { + form?.save(); + _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: const Icon(Icons.check), + ), + ), + ); + } + + 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(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 const Text('Indefinitely'); + case RecurrenceRuleEndType.MaxOccurrences: + return const Text('After a set number of times'); + case RecurrenceRuleEndType.SpecifiedEndDate: + return const Text('Continues until a specified date'); + default: + return const Text(''); + } + } + + // Get total days of a month + void _getValidDaysOfMonth(Frequency? frequency) { + _validDaysOfMonth.clear(); + var totalDays = 0; + + // Year frequency: Get total days of the selected month + if (frequency == Frequency.yearly) { + totalDays = DateTime(DateTime.now().year, + (_rrule?.hasByMonths ?? false) ? _rrule!.byMonths.first : 1, 0) + .day; + } else { + // Otherwise, get total days of the current month + var now = DateTime.now(); + totalDays = DateTime(now.year, now.month + 1, 0).day; + } + + for (var i = 1; i <= totalDays; i++) { + _validDaysOfMonth.add(i); + } + } + + void _updateDaysOfWeek() { + 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: + _rrule = _rrule?.copyWith(byWeekDays: { + ByWeekDayEntry(1), + ByWeekDayEntry(2), + ByWeekDayEntry(3), + ByWeekDayEntry(4), + ByWeekDayEntry(5), + ByWeekDayEntry(6), + ByWeekDayEntry(7), + }); + break; + case DayOfWeekGroup.None: + default: + _rrule?.byWeekDays.clear(); + break; + } + // () => setState(() => {}); + } + + void _updateDaysOfWeekGroup({DayOfWeek? selectedDay}) { + 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; + } + } + } + + int _weekNumFromWeekDayOccurence(Set weekdays) { + final weekNum = weekdays.first.occurrence; + if (weekNum != null) { + return weekNum - 1; + } else { + return 0; + } + } + + 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); + } + } + } + } + + /// 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; + } + } + } + + String? _validateTotalOccurrences(String? value) { + if (value == null) return null; + if (value.isNotEmpty && int.tryParse(value) == null) { + return 'Total occurrences needs to be a valid number'; + } + return null; + } + + String? _validateInterval(String? value) { + if (value == null) return null; + if (value.isNotEmpty && int.tryParse(value) == null) { + return 'Interval needs to be a valid number'; + } + return null; + } + + String? _validateTitle(String? value) { + if (value == null) return null; + if (value.isEmpty) { + return 'Name is required.'; + } + return null; + } + + TZDateTime? _combineDateWithTime(TZDateTime? date, TimeOfDay? time) { + if (date == null) return null; + var currentLocation = timeZoneDatabase.locations[_timezone]; + + final dateWithoutTime = TZDateTime.from( + DateTime.parse(DateFormat('y-MM-dd 00:00:00').format(date)), + currentLocation!); + + if (time == null) return dateWithoutTime; + if (Platform.isAndroid && _event?.allDay == true) return dateWithoutTime; + + return dateWithoutTime + .add(Duration(hours: time.hour, minutes: time.minute)); + } + + 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 new file mode 100644 index 00000000..a8d4b2b2 --- /dev/null +++ b/example/lib/presentation/pages/calendar_events.dart @@ -0,0 +1,190 @@ +import 'dart:async'; + +import 'package:device_calendar/device_calendar.dart'; +import 'package:flutter/material.dart'; + +import '../event_item.dart'; +import '../recurring_event_dialog.dart'; +import 'calendar_event.dart'; + +class CalendarEventsPage extends StatefulWidget { + final Calendar _calendar; + + const CalendarEventsPage(this._calendar, {Key? key}) : super(key: key); + + @override + _CalendarEventsPageState createState() { + return _CalendarEventsPageState(_calendar); + } +} + +class _CalendarEventsPageState extends State { + final Calendar _calendar; + final GlobalKey _scaffoldstate = GlobalKey(); + + late DeviceCalendarPlugin _deviceCalendarPlugin; + List _calendarEvents = []; + bool _isLoading = true; + + _CalendarEventsPageState(this._calendar) { + _deviceCalendarPlugin = DeviceCalendarPlugin(); + } + + @override + void initState() { + super.initState(); + _retrieveCalendarEvents(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldstate, + appBar: AppBar( + title: Text('${_calendar.name} events'), + actions: [_getDeleteButton()], + ), + body: (_calendarEvents.isNotEmpty || _isLoading) + ? Stack( + children: [ + ListView.builder( + itemCount: _calendarEvents.length, + itemBuilder: (BuildContext context, int index) { + return EventItem( + _calendarEvents[index], + _deviceCalendarPlugin, + _onLoading, + _onDeletedFinished, + _onTapped, + _calendar.isReadOnly != null && + _calendar.isReadOnly as bool); + }, + ), + if (_isLoading) + const Center( + child: CircularProgressIndicator(), + ) + ], + ) + : const Center(child: Text('No events found')), + floatingActionButton: _getAddEventButton(context)); + } + + Widget? _getAddEventButton(BuildContext context) { + if (_calendar.isReadOnly == false || _calendar.isReadOnly == null) { + return FloatingActionButton( + key: const Key('addEventButton'), + onPressed: () async { + final refreshEvents = await Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) { + return CalendarEventPage(_calendar); + })); + if (refreshEvents == true) { + await _retrieveCalendarEvents(); + } + }, + child: const Icon(Icons.add), + ); + } else { + return null; + } + } + + void _onLoading() { + setState(() { + _isLoading = true; + }); + } + + Future _onDeletedFinished(bool deleteSucceeded) async { + if (deleteSucceeded) { + await _retrieveCalendarEvents(); + } else { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Oops, we ran into an issue deleting the event'), + backgroundColor: Colors.red, + duration: Duration(seconds: 5), + )); + setState(() { + _isLoading = false; + }); + } + } + + Future _onTapped(Event event) async { + final refreshEvents = await Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) { + return CalendarEventPage( + _calendar, + event, + RecurringEventDialog( + _deviceCalendarPlugin, + event, + _onLoading, + _onDeletedFinished, + ), + ); + })); + if (refreshEvents != null && refreshEvents) { + await _retrieveCalendarEvents(); + } + } + + Future _retrieveCalendarEvents() async { + final startDate = DateTime.now().add(const Duration(days: -30)); + final endDate = DateTime.now().add(const Duration(days: 365 * 10)); + var calendarEventsResult = await _deviceCalendarPlugin.retrieveEvents( + _calendar.id, + RetrieveEventsParams(startDate: startDate, endDate: endDate)); + setState(() { + _calendarEvents = calendarEventsResult.data ?? []; + _isLoading = false; + }); + } + + Widget _getDeleteButton() { + return IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + await _showDeleteDialog(); + }); + } + + Future _showDeleteDialog() async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Warning'), + content: SingleChildScrollView( + child: ListBody( + children: const [ + Text('This will delete this calendar'), + Text('Are you sure?'), + ], + ), + ), + actions: [ + TextButton( + onPressed: () async { + var returnValue = + await _deviceCalendarPlugin.deleteCalendar(_calendar.id!); + debugPrint( + 'returnValue: ${returnValue.data}, ${returnValue.errors}'); + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + child: const Text('Delete!'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + ], + ); + }, + ); + } +} diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart new file mode 100644 index 00000000..71c47ea5 --- /dev/null +++ b/example/lib/presentation/pages/calendars.dart @@ -0,0 +1,161 @@ +import 'package:device_calendar/device_calendar.dart'; +import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'calendar_events.dart'; + +class CalendarsPage extends StatefulWidget { + const CalendarsPage({Key? key}) : super(key: key); + + @override + _CalendarsPageState createState() { + return _CalendarsPageState(); + } +} + +class _CalendarsPageState extends State { + late DeviceCalendarPlugin _deviceCalendarPlugin; + List _calendars = []; + List get _writableCalendars => + _calendars.where((c) => c.isReadOnly == false).toList(); + + List get _readOnlyCalendars => + _calendars.where((c) => c.isReadOnly == true).toList(); + + _CalendarsPageState() { + _deviceCalendarPlugin = DeviceCalendarPlugin(); + } + + @override + void initState() { + super.initState(); + _retrieveCalendars(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Calendars'), + actions: [_getRefreshButton()], + ), + body: Column( + children: [ + Padding( + 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, + ), + ), + Expanded( + flex: 1, + child: ListView.builder( + 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)}'), + onTap: () async { + await Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) { + return CalendarEventsPage(_calendars[index], + key: const Key('calendarEventsPage')); + })); + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + children: [ + Expanded( + 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, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(_calendars[index].color!)), + ), + 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: const Text('Default'), + ), + Icon(_calendars[index].isReadOnly == true + ? Icons.lock + : Icons.lock_open) + ], + ), + ), + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + final createCalendar = await Navigator.push(context, + MaterialPageRoute(builder: (BuildContext context) { + return const CalendarAddPage(); + })); + + if (createCalendar == true) { + _retrieveCalendars(); + } + }, + child: const Icon(Icons.add), + ), + ); + } + + void _retrieveCalendars() async { + try { + var permissionsGranted = await _deviceCalendarPlugin.hasPermissions(); + if (permissionsGranted.isSuccess && + (permissionsGranted.data == null || + permissionsGranted.data == false)) { + permissionsGranted = await _deviceCalendarPlugin.requestPermissions(); + if (!permissionsGranted.isSuccess || + permissionsGranted.data == null || + permissionsGranted.data == false) { + return; + } + } + + final calendarsResult = await _deviceCalendarPlugin.retrieveCalendars(); + setState(() { + _calendars = calendarsResult.data as List; + }); + } on PlatformException catch (e, s) { + debugPrint('RETRIEVE_CALENDARS: $e, $s'); + } + } + + Widget _getRefreshButton() { + return IconButton( + 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 new file mode 100644 index 00000000..2fff734e --- /dev/null +++ b/example/lib/presentation/pages/event_attendee.dart @@ -0,0 +1,174 @@ +import 'dart:io'; + +import 'package:device_calendar/device_calendar.dart'; +import 'package:device_calendar_example/common/app_routes.dart'; +import 'package:flutter/material.dart'; + +late DeviceCalendarPlugin _deviceCalendarPlugin; + +class EventAttendeePage extends StatefulWidget { + final Attendee? attendee; + final String? eventId; + const EventAttendeePage({Key? key, this.attendee, this.eventId}) + : super(key: key); + + @override + _EventAttendeePageState createState() => + _EventAttendeePageState(attendee, eventId ?? ''); +} + +class _EventAttendeePageState extends State { + Attendee? _attendee; + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _emailAddressController = TextEditingController(); + var _role = AttendeeRole.None; + var _status = AndroidAttendanceStatus.None; + String _eventId = ''; + + _EventAttendeePageState(Attendee? attendee, eventId) { + if (attendee != null) { + _attendee = attendee; + _nameController.text = _attendee!.name!; + _emailAddressController.text = _attendee!.emailAddress!; + _role = _attendee!.role!; + _status = _attendee!.androidAttendeeDetails?.attendanceStatus ?? + AndroidAttendanceStatus.None; + } + _eventId = eventId; + } + + @override + void dispose() { + _nameController.dispose(); + _emailAddressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_attendee != null + ? 'Edit attendee ${_attendee!.name}' + : 'Add an Attendee'), + ), + body: Column( + children: [ + Form( + key: _formKey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + controller: _nameController, + validator: (value) { + if (_attendee?.isCurrentUser == false && + (value == null || value.isEmpty)) { + return 'Please enter a name'; + } + return null; + }, + decoration: const InputDecoration(labelText: 'Name'), + ), + ), + Padding( + padding: const EdgeInsets.all(10.0), + child: TextFormField( + controller: _emailAddressController, + validator: (value) { + if (value == null || + value.isEmpty || + !value.contains('@')) { + return 'Please enter a valid email address'; + } + return null; + }, + decoration: + const InputDecoration(labelText: 'Email Address'), + ), + ), + ListTile( + leading: const Text('Role'), + trailing: DropdownButton( + onChanged: (value) { + setState(() { + _role = value as AttendeeRole; + }); + }, + value: _role, + items: AttendeeRole.values + .map((role) => DropdownMenuItem( + value: role, + child: Text(role.enumToString), + )) + .toList(), + ), + ), + Visibility( + visible: Platform.isIOS, + child: ListTile( + onTap: () async { + _deviceCalendarPlugin = DeviceCalendarPlugin(); + + await _deviceCalendarPlugin + .showiOSEventModal(_eventId); + Navigator.popUntil( + context, ModalRoute.withName(AppRoutes.calendars)); + //TODO: finish calling and getting attendee details from iOS + }, + leading: const Icon(Icons.edit), + title: const Text('View / edit iOS attendance details'), + ), + ), + Visibility( + visible: Platform.isAndroid, + child: ListTile( + leading: const Text('Android attendee status'), + trailing: DropdownButton( + onChanged: (value) { + setState(() { + _status = value as AndroidAttendanceStatus; + }); + }, + value: _status, + items: AndroidAttendanceStatus.values + .map((status) => DropdownMenuItem( + value: status, + child: Text(status.enumToString), + )) + .toList(), + ), + ), + ) + ], + ), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + setState(() { + _attendee = Attendee( + name: _nameController.text, + emailAddress: _emailAddressController.text, + role: _role, + isOrganiser: _attendee?.isOrganiser ?? false, + isCurrentUser: _attendee?.isCurrentUser ?? false, + iosAttendeeDetails: _attendee?.iosAttendeeDetails, + androidAttendeeDetails: AndroidAttendeeDetails.fromJson( + {'attendanceStatus': _status.index})); + + _emailAddressController.clear(); + }); + + Navigator.pop(context, _attendee); + } + }, + child: Text(_attendee != null ? 'Update' : 'Add'), + ) + ], + ), + ); + } +} diff --git a/example/lib/presentation/pages/event_reminders.dart b/example/lib/presentation/pages/event_reminders.dart new file mode 100644 index 00000000..4b0a11f3 --- /dev/null +++ b/example/lib/presentation/pages/event_reminders.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:device_calendar/device_calendar.dart'; + +class EventRemindersPage extends StatefulWidget { + final List _reminders; + const EventRemindersPage(this._reminders, {Key? key}) : super(key: key); + + @override + _EventRemindersPageState createState() => + _EventRemindersPageState(_reminders); +} + +class _EventRemindersPageState extends State { + List _reminders = []; + final _formKey = GlobalKey(); + final _minutesController = TextEditingController(); + + _EventRemindersPageState(List reminders) { + _reminders = [...reminders]; + } + + @override + void dispose() { + _minutesController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Reminders'), + ), + body: Column( + children: [ + Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Row( + children: [ + Expanded( + child: TextFormField( + controller: _minutesController, + validator: (value) { + if (value == null || + value.isEmpty || + int.tryParse(value) == null) { + return 'Please enter a reminder time in minutes'; + } + return null; + }, + decoration: const InputDecoration( + labelText: 'Minutes before start'), + ), + ), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + setState(() { + _reminders.add(Reminder( + minutes: int.parse(_minutesController.text))); + _minutesController.clear(); + }); + } + }, + child: const Text('Add'), + ), + ], + ), + ), + ), + Expanded( + child: ListView.builder( + itemCount: _reminders.length, + itemBuilder: (context, index) { + return ListTile( + title: Text('${_reminders[index].minutes} minutes'), + trailing: ElevatedButton( + onPressed: () { + setState(() { + _reminders.removeWhere( + (a) => a.minutes == _reminders[index].minutes); + }); + }, + child: const Text('Delete'), + ), + ); + }, + ), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context, _reminders); + }, + child: const Text('Done'), + ) + ], + ), + ); + } +} diff --git a/example/lib/presentation/recurring_event_dialog.dart b/example/lib/presentation/recurring_event_dialog.dart new file mode 100644 index 00000000..c8b8ff35 --- /dev/null +++ b/example/lib/presentation/recurring_event_dialog.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:device_calendar/device_calendar.dart'; + +class RecurringEventDialog extends StatefulWidget { + final DeviceCalendarPlugin _deviceCalendarPlugin; + final Event _calendarEvent; + + final VoidCallback _onLoadingStarted; + final Function(bool) _onDeleteFinished; + + const RecurringEventDialog(this._deviceCalendarPlugin, this._calendarEvent, + this._onLoadingStarted, this._onDeleteFinished, + {Key? key}) + : super(key: key); + + @override + _RecurringEventDialogState createState() => + _RecurringEventDialogState(_deviceCalendarPlugin, _calendarEvent, + onLoadingStarted: _onLoadingStarted, + onDeleteFinished: _onDeleteFinished); +} + +class _RecurringEventDialogState extends State { + late DeviceCalendarPlugin _deviceCalendarPlugin; + late Event _calendarEvent; + VoidCallback? _onLoadingStarted; + Function(bool)? _onDeleteFinished; + + _RecurringEventDialogState( + DeviceCalendarPlugin deviceCalendarPlugin, Event calendarEvent, + {VoidCallback? onLoadingStarted, Function(bool)? onDeleteFinished}) { + _deviceCalendarPlugin = deviceCalendarPlugin; + _calendarEvent = calendarEvent; + _onLoadingStarted = onLoadingStarted; + _onDeleteFinished = onDeleteFinished; + } + + @override + Widget build(BuildContext context) { + return SimpleDialog( + title: const Text('Are you sure you want to delete this event?'), + children: [ + SimpleDialogOption( + onPressed: () async { + Navigator.of(context).pop(true); + if (_onLoadingStarted != null) _onLoadingStarted!(); + final deleteResult = + await _deviceCalendarPlugin.deleteEventInstance( + _calendarEvent.calendarId, + _calendarEvent.eventId, + _calendarEvent.start?.millisecondsSinceEpoch, + _calendarEvent.end?.millisecondsSinceEpoch, + false); + if (_onDeleteFinished != null) { + _onDeleteFinished!( + deleteResult.isSuccess && deleteResult.data != null); + } + }, + child: const Text('This instance only'), + ), + SimpleDialogOption( + onPressed: () async { + Navigator.of(context).pop(true); + if (_onLoadingStarted != null) _onLoadingStarted!(); + final deleteResult = + await _deviceCalendarPlugin.deleteEventInstance( + _calendarEvent.calendarId, + _calendarEvent.eventId, + _calendarEvent.start?.millisecondsSinceEpoch, + _calendarEvent.end?.millisecondsSinceEpoch, + true); + if (_onDeleteFinished != null) { + _onDeleteFinished!( + deleteResult.isSuccess && deleteResult.data != null); + } + }, + child: const Text('This and following instances'), + ), + SimpleDialogOption( + onPressed: () async { + Navigator.of(context).pop(true); + if (_onLoadingStarted != null) _onLoadingStarted!(); + final deleteResult = await _deviceCalendarPlugin.deleteEvent( + _calendarEvent.calendarId, _calendarEvent.eventId); + if (_onDeleteFinished != null) { + _onDeleteFinished!( + deleteResult.isSuccess && deleteResult.data != null); + } + }, + child: const Text('All instances'), + ), + SimpleDialogOption( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ) + ], + ); + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 00000000..b32d2fb0 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,27 @@ +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" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + intl: ^0.17.0 + uuid: ^3.0.6 + flutter_native_timezone: ^2.0.0 + device_calendar: + path: ../ + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +flutter: + uses-material-design: true \ No newline at end of file diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 00000000..710ec6cf --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,36 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/ios/Classes/DeviceCalendarPlugin.h b/ios/Classes/DeviceCalendarPlugin.h new file mode 100644 index 00000000..0d5ad0b9 --- /dev/null +++ b/ios/Classes/DeviceCalendarPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface DeviceCalendarPlugin : NSObject +@end diff --git a/ios/Classes/DeviceCalendarPlugin.m b/ios/Classes/DeviceCalendarPlugin.m new file mode 100644 index 00000000..774b46d3 --- /dev/null +++ b/ios/Classes/DeviceCalendarPlugin.m @@ -0,0 +1,8 @@ +#import "DeviceCalendarPlugin.h" +#import + +@implementation DeviceCalendarPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftDeviceCalendarPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift new file mode 100644 index 00000000..f37d1a5a --- /dev/null +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -0,0 +1,1136 @@ +import EventKit +import EventKitUI +import Flutter +import Foundation +import UIKit + +extension Date { + var millisecondsSinceEpoch: Double { return self.timeIntervalSince1970 * 1000.0 } +} + +extension EKParticipant { + var emailAddress: String? { + return self.value(forKey: "emailAddress") as? String + } +} + +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" } + + if (!iCloudSources.isEmpty) { + return iCloudSources.first + } + + return nil + } + + 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 + } + + 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)) + } + } + + 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 { + 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"; + } + } + + 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 { + 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(fourYearsTimeInterval) + while currentEndDate <= endDate { + 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 [fourYearsTimeInterval] + currentStartDate = currentEndDate + currentEndDate = currentStartDate.addingTimeInterval(fourYearsTimeInterval) + } + + // If the cycle doesn't end exactly on the end date + if currentStartDate <= endDate { + 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) + } + } + } + + 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 { + for ekParticipant in ekEvent.attendees! { + let attendee = convertEkParticipantToAttendee(ekParticipant: ekParticipant) + 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, + calendarId: calendarId, + eventTitle: ekEvent.title ?? "New Event", + eventDescription: ekEvent.notes, + eventStartDate: Int64(ekEvent.startDate.millisecondsSinceEpoch), + eventEndDate: Int64(ekEvent.endDate.millisecondsSinceEpoch), + eventStartTimeZone: ekEvent.timeZone?.identifier, + eventAllDay: ekEvent.isAllDay, + attendees: attendees, + eventLocation: ekEvent.location, + eventURL: ekEvent.url?.absoluteString, + recurrenceRule: recurrenceRule, + organizer: convertEkParticipantToAttendee(ekParticipant: ekEvent.organizer), + reminders: reminders, + availability: convertEkEventAvailability(ekEventAvailability: ekEvent.availability), + eventStatus: convertEkEventStatus(ekEventStatus: ekEvent.status) + ) + + return event + } + + 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 + } + + private func convertEkEventAvailability(ekEventAvailability: EKEventAvailability?) -> Availability? { + switch ekEventAvailability { + case .busy: + return Availability.BUSY + case .free: + return Availability.FREE + case .tentative: + return Availability.TENTATIVE + case .unavailable: + return Availability.UNAVAILABLE + default: + return nil + } + } + + private func convertEkEventStatus(ekEventStatus: EKEventStatus?) -> EventStatus? { + switch ekEventStatus { + case .confirmed: + return EventStatus.CONFIRMED + case .tentative: + return EventStatus.TENTATIVE + case .canceled: + return EventStatus.CANCELED + case .none?: + return EventStatus.NONE + default: + return nil + } + } + + private func parseEKRecurrenceRules(_ ekEvent: EKEvent) -> RecurrenceRule? { + var recurrenceRule: RecurrenceRule? + if ekEvent.hasRecurrenceRules { + let ekRecurrenceRule = ekEvent.recurrenceRules![0] + var frequency: String + switch ekRecurrenceRule.frequency { + case EKRecurrenceFrequency.daily: + frequency = "DAILY" + case EKRecurrenceFrequency.weekly: + frequency = "WEEKLY" + case EKRecurrenceFrequency.monthly: + frequency = "MONTHLY" + case EKRecurrenceFrequency.yearly: + frequency = "YEARLY" + default: + frequency = "DAILY" + } + + var count: Int? + var endDate: String? + if(ekRecurrenceRule.recurrenceEnd?.occurrenceCount != nil && ekRecurrenceRule.recurrenceEnd?.occurrenceCount != 0) { + count = ekRecurrenceRule.recurrenceEnd?.occurrenceCount + } + + let endDateRaw = ekRecurrenceRule.recurrenceEnd?.endDate + if(endDateRaw != nil) { + endDate = formateDateTime(dateTime: endDateRaw!) + } + + 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( + freq: frequency, + count: count, + interval: ekRecurrenceRule.interval, + 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 recurrenceFrequency = recurrenceRuleArguments![recurrenceFrequencyArgument] as? String + let totalOccurrences = recurrenceRuleArguments![countArgument] as? NSInteger + let interval = recurrenceRuleArguments![intervalArgument] as? NSInteger + var recurrenceInterval = 1 + 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 { + 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 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!)} + } + } + + 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: 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 + } + if existingAttendee != nil && ekEvent!.organizer?.emailAddress != existingAttendee?.emailAddress{ + attendees.append(existingAttendee!) + 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 + } + + switch availabilityValue.uppercased() { + case Availability.BUSY.rawValue: + return .busy + case Availability.FREE.rawValue: + return .free + 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 + let calendarId = arguments[calendarIdArgument] as! String + let eventId = arguments[eventIdArgument] as? String + let isAllDay = arguments[eventAllDayArgument] as! Bool + let startDateMillisecondsSinceEpoch = arguments[eventStartDateArgument] as! NSNumber + let endDateDateMillisecondsSinceEpoch = arguments[eventEndDateArgument] as! NSNumber + let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch.doubleValue / 1000.0) + let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch.doubleValue / 1000.0) + let startTimeZoneString = arguments[eventStartTimeZoneArgument] as? String + let title = arguments[self.eventTitleArgument] as? String + let description = arguments[self.eventDescriptionArgument] as? String + let location = arguments[self.eventLocationArgument] as? String + let url = arguments[self.eventURLArgument] 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 + } + + var ekEvent: EKEvent? + if eventId == nil { + ekEvent = EKEvent.init(eventStore: self.eventStore) + } else { + ekEvent = self.eventStore.event(withIdentifier: eventId!) + if(ekEvent == nil) { + self.finishWithEventNotFoundError(result: result, eventId: eventId!) + return + } + } + + ekEvent!.title = title ?? "" + ekEvent!.notes = description + ekEvent!.isAllDay = isAllDay + ekEvent!.startDate = startDate + ekEvent!.endDate = endDate + + if (!isAllDay) { + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current + ekEvent!.timeZone = timeZone + } + + ekEvent!.calendar = ekCalendar! + ekEvent!.location = location + + // Create and add URL object only when if the input string is not empty or nil + if let urlCheck = url, !urlCheck.isEmpty { + let iosUrl = URL(string: url ?? "") + ekEvent!.url = iosUrl + } + 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) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + }, result: result) + } + + private func createParticipant(name: String, emailAddress: String, role: Int) -> EKParticipant? { + 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") + return participant as? EKParticipant + } + return nil + } + + private func deleteEvent(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { + checkPermissionsThenExecute(permissionsGrantedAction: { + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + let eventId = arguments[eventIdArgument] as! String + 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) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } + 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) + } + else { + try self.eventStore.remove(ekEvent!, span: .futureEvents, commit: true) + } + + result(true) + } catch { + self.eventStore.reset() + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } + }, 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 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() + let jsonData = try jsonEncoder.encode(codable) + let jsonString = String(data: jsonData, encoding: .utf8) + result(jsonString) + } catch { + result(FlutterError(code: genericError, message: error.localizedDescription, details: nil)) + } + } + + private func checkPermissionsThenExecute(permissionsGrantedAction: () -> Void, result: @escaping FlutterResult) { + if hasEventPermissions() { + permissionsGrantedAction() + return + } + self.finishWithUnauthorizedError(result: result) + } + + private func requestPermissions(_ completion: @escaping (Bool) -> Void) { + if hasEventPermissions() { + completion(true) + return + } + 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) + if #available(iOS 17, *) { + return status == EKAuthorizationStatus.fullAccess + } else { + return status == EKAuthorizationStatus.authorized + } + } +} + +extension Date { + func convert(from initTimeZone: TimeZone, to targetTimeZone: TimeZone) -> Date { + let delta = TimeInterval(initTimeZone.secondsFromGMT() - targetTimeZone.secondsFromGMT()) + return addingTimeInterval(delta) + } +} + +extension UIColor { + func rgb() -> Int? { + var fRed : CGFloat = 0 + var fGreen : CGFloat = 0 + var fBlue : CGFloat = 0 + var fAlpha: CGFloat = 0 + if self.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha) { + let iRed = Int(fRed * 255.0) + let iGreen = Int(fGreen * 255.0) + let iBlue = Int(fBlue * 255.0) + let iAlpha = Int(fAlpha * 255.0) + + // (Bits 24-31 are alpha, 16-23 are red, 8-15 are green, 0-7 are blue). + let rgb = (iAlpha << 24) + (iRed << 16) + (iGreen << 8) + iBlue + return rgb + } else { + // Could not extract RGBA components: + return nil + } + } + + public convenience init?(hex: String) { + let r, g, b, a: CGFloat + + if hex.hasPrefix("0x") { + let start = hex.index(hex.startIndex, offsetBy: 2) + let hexColor = String(hex[start...]) + + if hexColor.count == 8 { + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + + if scanner.scanHexInt64(&hexNumber) { + a = CGFloat((hexNumber & 0xff000000) >> 24) / 255 + 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 + } + } + } + + return nil + } + +} diff --git a/ios/device_calendar.podspec b/ios/device_calendar.podspec new file mode 100644 index 00000000..3230bcf3 --- /dev/null +++ b/ios/device_calendar.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'device_calendar' + s.version = '0.0.1' + s.summary = 'A new flutter plugin project.' + s.description = <<-DESC +A new flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.swift_version = '5.0' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.ios.deployment_target = '8.0' +end + diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart new file mode 100644 index 00000000..3566d5df --- /dev/null +++ b/lib/device_calendar.dart @@ -0,0 +1,17 @@ +library device_calendar; + +export 'src/common/calendar_enums.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 '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'; +export 'src/models/platform_specifics/android/attendance_status.dart'; +export 'src/device_calendar.dart'; +export 'package:timezone/timezone.dart'; diff --git a/lib/src/common/calendar_enums.dart b/lib/src/common/calendar_enums.dart new file mode 100644 index 00000000..aa77ec02 --- /dev/null +++ b/lib/src/common/calendar_enums.dart @@ -0,0 +1,315 @@ +enum DayOfWeek { + Monday, + Tuesday, + Wednesday, + Thursday, + Friday, + Saturday, + Sunday, +} + +enum DayOfWeekGroup { + None, + Weekday, + Weekend, + AllDays, +} + +enum MonthOfYear { + January, + Feburary, + March, + April, + May, + June, + July, + August, + September, + October, + November, + December, +} + +enum WeekNumber { + First, + Second, + Third, + Fourth, + Last, +} + +enum AttendeeRole { + None, + Required, + Optional, + Resource, +} + +enum Availability { + Free, + Busy, + Tentative, + Unavailable, +} + +enum EventStatus { + None, + Confirmed, + Canceled, + Tentative, +} + +extension DayOfWeekExtension on DayOfWeek { + static int _value(DayOfWeek val) { + switch (val) { + case DayOfWeek.Monday: + return 1; + case DayOfWeek.Tuesday: + return 2; + case DayOfWeek.Wednesday: + return 3; + case DayOfWeek.Thursday: + return 4; + case DayOfWeek.Friday: + return 5; + case DayOfWeek.Saturday: + return 6; + case DayOfWeek.Sunday: + return 0; + default: + return 1; + } + } + + String _enumToString(DayOfWeek enumValue) { + return enumValue.toString().split('.').last; + } + + int get value => _value(this); + + String get enumToString => _enumToString(this); +} + +extension DaysOfWeekGroupExtension on DayOfWeekGroup { + static List _getDays(DayOfWeekGroup val) { + switch (val) { + case DayOfWeekGroup.Weekday: + return [ + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday + ]; + case DayOfWeekGroup.Weekend: + return [DayOfWeek.Saturday, DayOfWeek.Sunday]; + case DayOfWeekGroup.AllDays: + return [ + DayOfWeek.Monday, + DayOfWeek.Tuesday, + DayOfWeek.Wednesday, + DayOfWeek.Thursday, + DayOfWeek.Friday, + DayOfWeek.Saturday, + DayOfWeek.Sunday + ]; + default: + return []; + } + } + + String _enumToString(DayOfWeekGroup enumValue) { + return enumValue.toString().split('.').last; + } + + List get getDays => _getDays(this); + + String get enumToString => _enumToString(this); +} + +extension MonthOfYearExtension on MonthOfYear { + static int _value(MonthOfYear val) { + switch (val) { + case MonthOfYear.January: + return 1; + case MonthOfYear.Feburary: + return 2; + case MonthOfYear.March: + return 3; + case MonthOfYear.April: + return 4; + case MonthOfYear.May: + return 5; + case MonthOfYear.June: + return 6; + case MonthOfYear.July: + return 7; + case MonthOfYear.August: + return 8; + case MonthOfYear.September: + return 9; + case MonthOfYear.October: + return 10; + case MonthOfYear.November: + return 11; + case MonthOfYear.December: + return 12; + default: + return 1; + } + } + + String _enumToString(MonthOfYear enumValue) { + return enumValue.toString().split('.').last; + } + + int get value => _value(this); + + String get enumToString => _enumToString(this); +} + +extension WeekNumberExtension on WeekNumber { + static int _value(WeekNumber val) { + switch (val) { + case WeekNumber.First: + return 1; + case WeekNumber.Second: + return 2; + case WeekNumber.Third: + return 3; + case WeekNumber.Fourth: + return 4; + case WeekNumber.Last: + return -1; + default: + return 1; + } + } + + String _enumToString(WeekNumber enumValue) { + return enumValue.toString().split('.').last; + } + + int get value => _value(this); + + String get enumToString => _enumToString(this); +} + +extension IntExtensions on int { + static DayOfWeek _getDayOfWeekEnumValue(int val) { + switch (val) { + case 1: + return DayOfWeek.Monday; + case 2: + return DayOfWeek.Tuesday; + case 3: + return DayOfWeek.Wednesday; + case 4: + return DayOfWeek.Thursday; + case 5: + return DayOfWeek.Friday; + case 6: + return DayOfWeek.Saturday; + case 0: + return DayOfWeek.Sunday; + default: + return DayOfWeek.Monday; + } + } + + static MonthOfYear _getMonthOfYearEnumValue(int val) { + switch (val) { + case 1: + return MonthOfYear.January; + case 2: + return MonthOfYear.Feburary; + case 3: + return MonthOfYear.March; + case 4: + return MonthOfYear.April; + case 5: + return MonthOfYear.May; + case 6: + return MonthOfYear.June; + case 7: + return MonthOfYear.July; + case 8: + return MonthOfYear.August; + case 9: + return MonthOfYear.September; + case 10: + return MonthOfYear.October; + case 11: + return MonthOfYear.November; + case 12: + return MonthOfYear.December; + default: + return MonthOfYear.January; + } + } + + static WeekNumber _getWeekNumberEnumValue(int val) { + switch (val) { + case 1: + return WeekNumber.First; + case 2: + return WeekNumber.Second; + case 3: + return WeekNumber.Third; + case 4: + return WeekNumber.Fourth; + case -1: + return WeekNumber.Last; + default: + return WeekNumber.First; + } + } + + DayOfWeek get getDayOfWeekEnumValue => _getDayOfWeekEnumValue(this); + + MonthOfYear get getMonthOfYearEnumValue => _getMonthOfYearEnumValue(this); + + WeekNumber get getWeekNumberEnumValue => _getWeekNumberEnumValue(this); +} + +extension RoleExtensions on AttendeeRole { + String _enumToString(AttendeeRole enumValue) { + return enumValue.toString().split('.').last; + } + + String get enumToString => _enumToString(this); +} + +extension AvailabilityExtensions on Availability { + String _enumToString(Availability enumValue) { + switch (enumValue) { + case Availability.Busy: + return 'BUSY'; + case Availability.Free: + return 'FREE'; + case Availability.Tentative: + return 'TENTATIVE'; + case Availability.Unavailable: + return 'UNAVAILABLE'; + } + } + + String get enumToString => _enumToString(this); +} + +extension EventStatusExtensions on EventStatus { + String _enumToString(EventStatus enumValue) { + switch (enumValue) { + case EventStatus.Confirmed: + return 'CONFIRMED'; + case EventStatus.Tentative: + return 'TENTATIVE'; + case EventStatus.Canceled: + return 'CANCELED'; + case EventStatus.None: + return 'NONE'; + } + } + + String get enumToString => _enumToString(this); +} diff --git a/lib/src/common/channel_constants.dart b/lib/src/common/channel_constants.dart new file mode 100644 index 00000000..2eef3d2d --- /dev/null +++ b/lib/src/common/channel_constants.dart @@ -0,0 +1,26 @@ +class ChannelConstants { + static const String channelName = 'plugins.builttoroam.com/device_calendar'; + + static const String methodNameRequestPermissions = 'requestPermissions'; + static const String methodNameHasPermissions = 'hasPermissions'; + static const String methodNameRetrieveCalendars = 'retrieveCalendars'; + static const String methodNameRetrieveEvents = 'retrieveEvents'; + static const String methodNameDeleteEvent = 'deleteEvent'; + static const String methodNameDeleteEventInstance = 'deleteEventInstance'; + static const String methodNameCreateOrUpdateEvent = 'createOrUpdateEvent'; + static const String methodNameCreateCalendar = 'createCalendar'; + static const String methodNameDeleteCalendar = 'deleteCalendar'; + static const String methodNameShowiOSEventModal = 'showiOSEventModal'; + + static const String parameterNameCalendarId = 'calendarId'; + static const String parameterNameStartDate = 'startDate'; + static const String parameterNameEndDate = 'endDate'; + static const String parameterNameEventId = 'eventId'; + static const String parameterNameEventIds = 'eventIds'; + static const String parameterNameEventStartDate = 'eventStartDate'; + static const String parameterNameEventEndDate = 'eventEndDate'; + static const String parameterNameFollowingInstances = 'followingInstances'; + static const String parameterNameCalendarName = 'calendarName'; + static const String parameterNameCalendarColor = 'calendarColor'; + static const String parameterNameLocalAccountName = 'localAccountName'; +} diff --git a/lib/src/common/error_codes.dart b/lib/src/common/error_codes.dart new file mode 100644 index 00000000..e75b29de --- /dev/null +++ b/lib/src/common/error_codes.dart @@ -0,0 +1,6 @@ +class ErrorCodes { + static const int invalidArguments = 400; + static const int platformSpecific = 599; + static const int generic = 500; + static const int unknown = 502; +} diff --git a/lib/src/common/error_messages.dart b/lib/src/common/error_messages.dart new file mode 100644 index 00000000..f2126006 --- /dev/null +++ b/lib/src/common/error_messages.dart @@ -0,0 +1,27 @@ +class ErrorMessages { + static const String fromJsonMapIsNull = 'The json object is null'; + + static const String invalidMissingCalendarId = + 'Calendar ID is missing or invalid'; + + static const String invalidRetrieveEventsParams = + 'A valid instance of the RetrieveEventsParams class is required. Must the event ids to filter by or the start and end date to filter by or a combination of these'; + static const String deleteEventInvalidArgumentsMessage = + 'Calendar ID and/or Event ID argument(s) have not been specified or are invalid'; + static const String createOrUpdateEventInvalidArgumentsMessageAllDay = + "To create or update an all day event you must provide calendar ID, event with a title and event's start date"; + static const String createOrUpdateEventInvalidArgumentsMessage = + "To create or update an event you must provide calendar ID, event with a title and event's start date and end date (where start date must be before end date)"; + static const String createCalendarInvalidCalendarNameMessage = + 'Calendar name must not be null or empty'; + + static const String invalidRecurrencyFrequency = + 'Invalid recurrency frequency'; + + 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.'; + static const String unknownDeviceGenericExceptionTemplate = + 'Device calendar plugin ran into an issue, with message "%s"'; +} diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart new file mode 100644 index 00000000..4c1d12f1 --- /dev/null +++ b/lib/src/device_calendar.dart @@ -0,0 +1,443 @@ +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; + +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 { + static const MethodChannel channel = + MethodChannel(ChannelConstants.channelName); + + static final DeviceCalendarPlugin _instance = DeviceCalendarPlugin.private(); + + factory DeviceCalendarPlugin({bool shouldInitTimezone = true}) { + if (shouldInitTimezone) { + tz.initializeTimeZones(); + } + return _instance; + } + + @visibleForTesting + DeviceCalendarPlugin.private(); + + /// Requests permissions to modify the calendars on the device + /// + /// Returns a [Result] indicating if calendar READ and WRITE permissions + /// have (true) or have not (false) been granted + Future> requestPermissions() async { + return _invokeChannelMethod( + ChannelConstants.methodNameRequestPermissions, + ); + } + + /// Checks if permissions for modifying the device calendars have been granted + /// + /// Returns a [Result] indicating if calendar READ and WRITE permissions + /// have (true) or have not (false) been granted + Future> hasPermissions() async { + return _invokeChannelMethod( + ChannelConstants.methodNameHasPermissions, + ); + } + + /// Retrieves all of the device defined calendars + /// + /// Returns a [Result] containing a list of device [Calendar] + Future>> retrieveCalendars() async { + return _invokeChannelMethod( + ChannelConstants.methodNameRetrieveCalendars, + evaluateResponse: (rawData) => UnmodifiableListView( + json.decode(rawData).map( + (decodedCalendar) => Calendar.fromJson(decodedCalendar), + ), + ), + ); + } + + /// Retrieves the events from the specified calendar + /// + /// The `calendarId` paramter is the id of the calendar that plugin will return events for + /// The `retrieveEventsParams` parameter combines multiple properties that + /// specifies conditions of the events retrieval. For instance, defining [RetrieveEventsParams.startDate] + /// and [RetrieveEventsParams.endDate] will return events only happening in that time range + /// + /// Returns a [Result] containing a list [Event], that fall + /// into the specified parameters + Future>> retrieveEvents( + 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( + 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.\ + /// To delete individual instance of a recurring event, please use [deleteEventInstance()] + /// + /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\ + /// The `eventId` parameter is the id of the event that plugin will try to delete + /// + /// Returns a [Result] indicating if the event has (true) or has not (false) been deleted from the calendar + Future> deleteEvent( + String? calendarId, + String? eventId, + ) async { + return _invokeChannelMethod( + ChannelConstants.methodNameDeleteEvent, + assertParameters: (result) { + _validateCalendarIdParameter( + result, + calendarId, + ); + + _assertParameter( + result, + eventId?.isNotEmpty ?? false, + ErrorCodes.invalidArguments, + ErrorMessages.deleteEventInvalidArgumentsMessage, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarId: calendarId, + ChannelConstants.parameterNameEventId: eventId, + }, + ); + } + + /// Deletes an instance of a recurring event from a calendar. This should be used for a recurring event only.\ + /// If `startDate`, `endDate` or `deleteFollowingInstances` is not valid or null, then all instances of the event will be deleted. + /// + /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\ + /// The `eventId` parameter is the id of the event that plugin will try to delete\ + /// The `startDate` parameter is the start date of the instance to delete\ + /// The `endDate` parameter is the end date of the instance to delete\ + /// The `deleteFollowingInstances` parameter will also delete the following instances if set to true + /// + /// Returns a [Result] indicating if the instance of the event has (true) or has not (false) been deleted from the calendar + Future> deleteEventInstance( + String? calendarId, + String? eventId, + int? startDate, + int? endDate, + bool deleteFollowingInstances, + ) async { + return _invokeChannelMethod( + ChannelConstants.methodNameDeleteEventInstance, + assertParameters: (result) { + _validateCalendarIdParameter( + result, + calendarId, + ); + + _assertParameter( + result, + eventId?.isNotEmpty ?? false, + ErrorCodes.invalidArguments, + ErrorMessages.deleteEventInvalidArgumentsMessage, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarId: calendarId, + ChannelConstants.parameterNameEventId: eventId, + ChannelConstants.parameterNameEventStartDate: startDate, + ChannelConstants.parameterNameEventEndDate: endDate, + ChannelConstants.parameterNameFollowingInstances: + deleteFollowingInstances, + }, + ); + } + + /// Creates or updates an event + /// + /// The `event` paramter specifies how event data should be saved into the calendar + /// Always specify the [Event.calendarId], to inform the plugin in which calendar + /// it should create or update the event. + /// + /// Returns a [Result] with the newly created or updated [Event.eventId] + Future?> createOrUpdateEvent(Event? event) async { + if (event == null) return null; + return _invokeChannelMethod( + ChannelConstants.methodNameCreateOrUpdateEvent, + assertParameters: (result) { + // Setting time to 0 for all day events + if (event.allDay == true) { + if (event.start != null) { + var dateStart = DateTime(event.start!.year, event.start!.month, + event.start!.day, 0, 0, 0); + // 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) + : TZDateTime.from(dateStart, + timeZoneDatabase.locations[event.start!.location.name]!); + } + if (event.end != null) { + var dateEnd = DateTime( + event.end!.year, event.end!.month, event.end!.day, 0, 0, 0); + // allDay events on Android need to be at midnight UTC on the + // day after the last day. For example, a 2-day allDay event on + // 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(const Duration(days: 1)) + : TZDateTime.from(dateEnd, + timeZoneDatabase.locations[event.end!.location.name]!); + } + } + + _assertParameter( + result, + !(event.allDay == true && (event.calendarId?.isEmpty ?? true) || + event.start == null || + event.end == null), + ErrorCodes.invalidArguments, + ErrorMessages.createOrUpdateEventInvalidArgumentsMessageAllDay, + ); + + _assertParameter( + result, + !(event.allDay != true && + ((event.calendarId?.isEmpty ?? true) || + event.start == null || + event.end == null || + (event.start != null && + event.end != null && + event.start!.isAfter(event.end!)))), + ErrorCodes.invalidArguments, + ErrorMessages.createOrUpdateEventInvalidArgumentsMessage, + ); + }, + arguments: () => event.toJson(), + ); + } + + /// Creates a new local calendar for the current device. + /// + /// The `calendarName` parameter is the name of the new calendar\ + /// The `calendarColor` parameter is the color of the calendar. If null, + /// a default color (red) will be used\ + /// The `localAccountName` parameter is the name of the local account: + /// - [Android] Required. If `localAccountName` parameter is null or empty, it will default to 'Device Calendar'. + /// If the account name already exists in the device, it will add another calendar under the account, + /// otherwise a new local account and a new calendar will be created. + /// - [iOS] Not used. A local account will be picked up automatically, if not found, an error will be thrown. + /// + /// Returns a [Result] with the newly created [Calendar.id] + Future> createCalendar( + String? calendarName, { + Color? calendarColor, + String? localAccountName, + }) async { + return _invokeChannelMethod( + ChannelConstants.methodNameCreateCalendar, + assertParameters: (result) { + calendarColor ??= Colors.red; + + _assertParameter( + result, + calendarName?.isNotEmpty == true, + ErrorCodes.invalidArguments, + ErrorMessages.createCalendarInvalidCalendarNameMessage, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarName: calendarName, + ChannelConstants.parameterNameCalendarColor: + '0x${calendarColor?.value.toRadixString(16)}', + ChannelConstants.parameterNameLocalAccountName: + localAccountName?.isEmpty ?? true + ? 'Device Calendar' + : localAccountName + }, + ); + } + + /// Deletes a calendar. + /// The `calendarId` parameter is the id of the calendar that plugin will try to delete the event from\/// + /// Returns a [Result] indicating if the instance of the calendar has (true) or has not (false) been deleted + Future> deleteCalendar( + String calendarId, + ) async { + return _invokeChannelMethod( + ChannelConstants.methodNameDeleteCalendar, + assertParameters: (result) { + _validateCalendarIdParameter( + result, + calendarId, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarId: calendarId, + }, + ); + } + + /// 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 + Future> showiOSEventModal( + String eventId, + ) { + return _invokeChannelMethod( + ChannelConstants.methodNameShowiOSEventModal, + arguments: () => { + ChannelConstants.parameterNameEventId: eventId, + }, + ); + } + + Future> _invokeChannelMethod( + String channelMethodName, { + Function(Result)? assertParameters, + Map Function()? arguments, + T Function(dynamic)? evaluateResponse, + }) async { + final result = Result(); + + try { + if (assertParameters != null) { + assertParameters(result); + if (result.hasErrors) { + return result; + } + } + + var rawData = await channel.invokeMethod( + channelMethodName, + arguments != null ? arguments() : null, + ); + + if (evaluateResponse != null) { + result.data = evaluateResponse(rawData); + } else { + result.data = rawData; + } + } 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; + } + + void _parsePlatformExceptionAndUpdateResult( + Exception? exception, Result result) { + if (exception == null) { + result.errors.add( + const ResultError( + ErrorCodes.unknown, + ErrorMessages.unknownDeviceIssue, + ), + ); + return; + } + + debugPrint('$exception'); + + if (exception is PlatformException) { + result.errors.add( + ResultError( + ErrorCodes.platformSpecific, + '${ErrorMessages.unknownDeviceExceptionTemplate}, Code: ${exception.code}, Exception: ${exception.message}', + ), + ); + } else { + result.errors.add( + ResultError( + ErrorCodes.generic, + '${ErrorMessages.unknownDeviceGenericExceptionTemplate} ${exception.toString}', + ), + ); + } + } + + void _assertParameter( + Result result, + bool predicate, + int errorCode, + String errorMessage, + ) { + if (result.data != null) { + debugPrint("RESULT of _assertParameter: ${result.data}"); + } + if (!predicate) { + result.errors.add( + ResultError(errorCode, errorMessage), + ); + } + } + + void _validateCalendarIdParameter( + Result result, + String? calendarId, + ) { + _assertParameter( + result, + calendarId?.isNotEmpty ?? false, + ErrorCodes.invalidArguments, + ErrorMessages.invalidMissingCalendarId, + ); + } +} diff --git a/lib/src/models/attendee.dart b/lib/src/models/attendee.dart new file mode 100644 index 00000000..2e8e4cb9 --- /dev/null +++ b/lib/src/models/attendee.dart @@ -0,0 +1,81 @@ +import 'dart:io' show Platform; + +import '../common/calendar_enums.dart'; +import '../common/error_messages.dart'; +import 'platform_specifics/android/attendee_details.dart'; +import 'platform_specifics/ios/attendee_details.dart'; + +/// A person attending an event +class Attendee { + /// The name of the attendee + String? name; + + /// The email address of the attendee + String? emailAddress; + + /// An attendee role: None, Optional, Required or Resource + AttendeeRole? role; + + /// Read-only. Returns true if the attendee is an organiser, else false + bool isOrganiser = false; + + /// Read-only. Returns true if the attendee is the current user, else false + bool isCurrentUser = false; + + /// Details about the attendee that are specific to iOS. + /// When reading details for an existing event, this will only be populated on iOS devices. + IosAttendeeDetails? iosAttendeeDetails; + + /// Details about the attendee that are specific to Android. + /// When reading details for an existing event, this will only be populated on Android devices. + AndroidAttendeeDetails? androidAttendeeDetails; + + Attendee({ + this.name, + this.emailAddress, + this.role, + this.isOrganiser = false, + this.isCurrentUser = false, + this.iosAttendeeDetails, + this.androidAttendeeDetails, + }); + + Attendee.fromJson(Map? json) { + if (json == null) { + throw ArgumentError(ErrorMessages.fromJsonMapIsNull); + } + + name = json['name']; + emailAddress = json['emailAddress']; + role = AttendeeRole.values[json['role'] ?? 0]; + isOrganiser = json['isOrganizer'] ?? + false; // Getting and setting an organiser for Android + isCurrentUser = json['isCurrentUser'] ?? false; + + if (Platform.isAndroid) { + androidAttendeeDetails = AndroidAttendeeDetails.fromJson(json); + } + + if (Platform.isIOS) { + iosAttendeeDetails = IosAttendeeDetails.fromJson(json); + } + } + + Map toJson() { + final data = { + 'name': name, + 'emailAddress': emailAddress, + 'role': role?.index, + 'isOrganizer': isOrganiser, + }; + + if (iosAttendeeDetails != null) { + data.addEntries(iosAttendeeDetails!.toJson().entries); + } + if (androidAttendeeDetails != null) { + data.addEntries(androidAttendeeDetails!.toJson().entries); + } + + return data; + } +} diff --git a/lib/src/models/calendar.dart b/lib/src/models/calendar.dart new file mode 100644 index 00000000..c04ab405 --- /dev/null +++ b/lib/src/models/calendar.dart @@ -0,0 +1,56 @@ +/// A calendar on the user's device +class Calendar { + /// Read-only. The unique identifier for this calendar + String? id; + + /// The name of this calendar + String? name; + + /// Read-only. If the calendar is read-only + bool? isReadOnly; + + /// Read-only. If the calendar is the default + bool? isDefault; + + /// Read-only. Color of the calendar + int? color; + + // Read-only. Account name associated with the calendar + String? accountName; + + // Read-only. Account type associated with the calendar + String? accountType; + + Calendar( + {this.id, + this.name, + this.isReadOnly, + this.isDefault, + this.color, + this.accountName, + this.accountType}); + + Calendar.fromJson(Map json) { + id = json['id']; + name = json['name']; + isReadOnly = json['isReadOnly']; + isDefault = json['isDefault']; + color = json['color']; + accountName = json['accountName']; + accountType = json['accountType']; + } + + Map toJson() { + final data = { + 'id': id, + 'name': name, + 'isReadOnly': isReadOnly, + 'isDefault': isDefault, + 'color': color, + 'accountName': accountName, + 'accountType': accountType + }; + + return data; + } +} diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart new file mode 100644 index 00000000..00ebfa52 --- /dev/null +++ b/lib/src/models/event.dart @@ -0,0 +1,306 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; + +import '../../device_calendar.dart'; +import '../common/error_messages.dart'; + +/// An event associated with a calendar +class Event { + /// Read-only. The unique identifier for this event. This is auto-generated when a new event is created + String? eventId; + + /// Read-only. The identifier of the calendar that this event is associated with + String? calendarId; + + /// The title of this event + String? title; + + /// The description for this event + String? description; + + /// Indicates when the event starts + TZDateTime? start; + + /// Indicates when the event ends + TZDateTime? end; + + /// Indicates if this is an all-day event + bool? allDay; + + /// The location of this event + String? location; + + /// An URL for this event + Uri? url; + + /// A list of attendees for this event + List? attendees; + + /// The recurrence rule for this event + RecurrenceRule? recurrenceRule; + + /// A list of reminders (by minutes) for this event + List? reminders; + + /// Indicates if this event counts as busy time, tentative, unavaiable or is still free time + late Availability availability; + + /// Indicates if this event is of confirmed, canceled, tentative or none status + EventStatus? status; + + //##### + /// Read-only. Color of the event + int? color; + + ///Note for development: + /// + ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. + ///Make sure all locations are updated if changes needed to be made. + ///Swift: + ///`ios/Classes/SwiftDeviceCalendarPlugin.swift` + ///Kotlin: + ///`android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt` + ///`android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt` + ///`android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt` + Event( + this.calendarId, { + this.eventId, + this.title, + this.start, + this.end, + this.description, + this.attendees, + this.recurrenceRule, + this.reminders, + this.availability = Availability.Busy, + this.location, + this.url, + this.allDay = false, + this.status, + this.color, + }); + + ///Get Event from JSON. + /// + ///Sample JSON: + ///{calendarId: 00, eventId: 0000, eventTitle: Sample Event, eventDescription: This is a sample event, eventStartDate: 1563719400000, eventStartTimeZone: Asia/Hong_Kong, eventEndDate: 1640532600000, eventEndTimeZone: Asia/Hong_Kong, eventAllDay: false, eventLocation: Yuenlong Station, eventURL: null, availability: BUSY, attendees: [{name: commonfolk, emailAddress: total.loss@hong.com, role: 1, isOrganizer: false, attendanceStatus: 3}], reminders: [{minutes: 39}]} + Event.fromJson(Map? json) { + if (json == null) { + throw ArgumentError(ErrorMessages.fromJsonMapIsNull); + } + String? foundUrl; + String? startLocationName; + String? endLocationName; + int? startTimestamp; + int? endTimestamp; + bool legacyJSON = false; + var legacyName = { + title: 'title', + description: 'description', + startTimestamp: 'start', + endTimestamp: 'end', + startLocationName: 'startTimeZone', + endLocationName: 'endTimeZone', + allDay: 'allDay', + location: 'location', + foundUrl: 'url', + }; + legacyName.forEach((key, value) { + if (json[value] != null) { + key = json[value]; + legacyJSON = true; + } + }); + + eventId = json['eventId']; + calendarId = json['calendarId']; + title = json['eventTitle']; + description = json['eventDescription']; + color = json['eventColor']; //##### + + startTimestamp = json['eventStartDate']; + startLocationName = json['eventStartTimeZone']; + var startTimeZone = timeZoneDatabase.locations[startLocationName]; + startTimeZone ??= local; + start = startTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(startTimeZone, startTimestamp) : TZDateTime.now(local); + + endTimestamp = json['eventEndDate']; + endLocationName = json['eventEndTimeZone']; + var endLocation = timeZoneDatabase.locations[endLocationName]; + endLocation ??= startTimeZone; + end = endTimestamp != null ? TZDateTime.fromMillisecondsSinceEpoch(endLocation, endTimestamp) : TZDateTime.now(local); + allDay = json['eventAllDay'] ?? false; + if (Platform.isAndroid && (allDay ?? false)) { + // On Android, the datetime in an allDay event is adjusted to local + // timezone, which can result in the wrong day, so we need to bring the + // date back to midnight UTC to get the correct date + var startOffset = start?.timeZoneOffset.inMilliseconds ?? 0; + var endOffset = end?.timeZoneOffset.inMilliseconds ?? 0; + // subtract the offset to get back to midnight on the correct date + start = start?.subtract(Duration(milliseconds: startOffset)); + 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(const Duration(days: 1)); + } + location = json['eventLocation']; + availability = parseStringToAvailability(json['availability']); + status = parseStringToEventStatus(json['eventStatus']); + + foundUrl = json['eventURL']?.toString(); + if (foundUrl?.isEmpty ?? true) { + url = null; + } else { + url = Uri.dataFromString(foundUrl as String); + } + + if (json['attendees'] != null) { + attendees = json['attendees'].map((decodedAttendee) { + return Attendee.fromJson(decodedAttendee); + }).toList(); + } + + if (json['organizer'] != null) { + // Getting and setting an organiser for iOS + var organiser = Attendee.fromJson(json['organizer']); + + var attendee = attendees?.firstWhereOrNull((at) => at?.name == organiser.name && at?.emailAddress == organiser.emailAddress); + if (attendee != null) { + attendee.isOrganiser = true; + } + } + + 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) { + reminders = json['reminders'].map((decodedReminder) { + return Reminder.fromJson(decodedReminder); + }).toList(); + } + if (legacyJSON) { + throw const FormatException('legacy JSON detected. Please update your current JSONs as they may not be supported later on.'); + } + } + + Map toJson() { + final data = {}; + + data['calendarId'] = calendarId; + data['eventId'] = eventId; + data['eventTitle'] = title; + data['eventDescription'] = description; + data['eventStartDate'] = start?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; + data['eventStartTimeZone'] = start?.location.name; + data['eventEndDate'] = end?.millisecondsSinceEpoch ?? TZDateTime.now(local).millisecondsSinceEpoch; + data['eventEndTimeZone'] = end?.location.name; + data['eventAllDay'] = allDay; + data['eventLocation'] = location; + data['eventURL'] = url?.data?.contentText; + data['availability'] = availability.enumToString; + data['eventStatus'] = status?.enumToString; + data['eventColor'] = color; //##### + + if (attendees != null) { + data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); + } + + if (attendees != null) { + data['organizer'] = attendees?.firstWhereOrNull((a) => a!.isOrganiser)?.toJson(); + } + + 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; + } + + Availability parseStringToAvailability(String? value) { + var testValue = value?.toUpperCase(); + switch (testValue) { + case 'BUSY': + return Availability.Busy; + case 'FREE': + return Availability.Free; + case 'TENTATIVE': + return Availability.Tentative; + case 'UNAVAILABLE': + return Availability.Unavailable; + } + return Availability.Busy; + } + + EventStatus? parseStringToEventStatus(String? value) { + var testValue = value?.toUpperCase(); + switch (testValue) { + case 'CONFIRMED': + return EventStatus.Confirmed; + case 'TENTATIVE': + return EventStatus.Tentative; + case 'CANCELED': + return EventStatus.Canceled; + case 'NONE': + return EventStatus.None; + } + return null; + } + + bool updateStartLocation(String? newStartLocation) { + if (newStartLocation == null) return false; + try { + var location = timeZoneDatabase.get(newStartLocation); + start = TZDateTime.from(start as TZDateTime, location); + return true; + } on LocationNotFoundException { + return false; + } + } + + bool updateEndLocation(String? newEndLocation) { + if (newEndLocation == null) return false; + try { + var location = timeZoneDatabase.get(newEndLocation); + end = TZDateTime.from(end as TZDateTime, location); + return true; + } on LocationNotFoundException { + return false; + } + } +} diff --git a/lib/src/models/platform_specifics/android/attendance_status.dart b/lib/src/models/platform_specifics/android/attendance_status.dart new file mode 100644 index 00000000..d895877f --- /dev/null +++ b/lib/src/models/platform_specifics/android/attendance_status.dart @@ -0,0 +1,15 @@ +enum AndroidAttendanceStatus { + None, + Accepted, + Declined, + Invited, + Tentative, +} + +extension AndroidAttendanceStatusExtensions on AndroidAttendanceStatus { + String _enumToString(AndroidAttendanceStatus enumValue) { + return enumValue.toString().split('.').last; + } + + String get enumToString => _enumToString(this); +} diff --git a/lib/src/models/platform_specifics/android/attendee_details.dart b/lib/src/models/platform_specifics/android/attendee_details.dart new file mode 100644 index 00000000..ba6f3b7a --- /dev/null +++ b/lib/src/models/platform_specifics/android/attendee_details.dart @@ -0,0 +1,23 @@ +import '../../../common/error_messages.dart'; +import 'attendance_status.dart'; + +class AndroidAttendeeDetails { + AndroidAttendanceStatus? attendanceStatus; + + AndroidAttendeeDetails({this.attendanceStatus}); + + AndroidAttendeeDetails.fromJson(Map? json) { + if (json == null) { + throw ArgumentError(ErrorMessages.fromJsonMapIsNull); + } + + if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { + attendanceStatus = + AndroidAttendanceStatus.values[json['attendanceStatus']]; + } + } + + Map toJson() { + return {'attendanceStatus': attendanceStatus?.index}; + } +} diff --git a/lib/src/models/platform_specifics/ios/attendance_status.dart b/lib/src/models/platform_specifics/ios/attendance_status.dart new file mode 100644 index 00000000..bd958391 --- /dev/null +++ b/lib/src/models/platform_specifics/ios/attendance_status.dart @@ -0,0 +1,18 @@ +enum IosAttendanceStatus { + Unknown, + Pending, + Accepted, + Declined, + Tentative, + Delegated, + Completed, + InProcess, +} + +extension IosAttendanceStatusExtensions on IosAttendanceStatus { + String _enumToString(IosAttendanceStatus enumValue) { + return enumValue.toString().split('.').last; + } + + String get enumToString => _enumToString(this); +} diff --git a/lib/src/models/platform_specifics/ios/attendee_details.dart b/lib/src/models/platform_specifics/ios/attendee_details.dart new file mode 100644 index 00000000..305a1b42 --- /dev/null +++ b/lib/src/models/platform_specifics/ios/attendee_details.dart @@ -0,0 +1,21 @@ +import '../../../common/error_messages.dart'; +import 'attendance_status.dart'; + +class IosAttendeeDetails { + IosAttendanceStatus? attendanceStatus; + IosAttendeeDetails({this.attendanceStatus}); + + IosAttendeeDetails.fromJson(Map? json) { + if (json == null) { + throw ArgumentError(ErrorMessages.fromJsonMapIsNull); + } + + if (json['attendanceStatus'] != null && json['attendanceStatus'] is int) { + attendanceStatus = IosAttendanceStatus.values[json['attendanceStatus']]; + } + } + + Map toJson() { + return {'attendanceStatus': attendanceStatus?.index}; + } +} diff --git a/lib/src/models/reminder.dart b/lib/src/models/reminder.dart new file mode 100644 index 00000000..761ab676 --- /dev/null +++ b/lib/src/models/reminder.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; + +class Reminder { + /// The time when the reminder should be triggered expressed in terms of minutes before the start of the event + int? minutes; + + Reminder({@required this.minutes}) + : assert(minutes != null && minutes >= 0, + 'Minutes must be greater than or equal than zero'); + + Reminder.fromJson(Map json) { + minutes = json['minutes'] as int; + } + + Map toJson() { + return {'minutes': minutes}; + } +} diff --git a/lib/src/models/result.dart b/lib/src/models/result.dart new file mode 100644 index 00000000..fff23c71 --- /dev/null +++ b/lib/src/models/result.dart @@ -0,0 +1,33 @@ +class Result { + /// Indicates if the request was successfull or not + /// + /// Returns true if data is not null and there're no error messages, otherwise returns false + bool get isSuccess { + var res = data != null && errors.isEmpty; + if (res) { + if (data is String) { + res = (data as String).isNotEmpty; + } + } + + return res; + } + + /// Indicates if there are errors. This isn't exactly the same as !isSuccess since + /// it doesn't look at the state of the data. + /// + /// Returns true if there are error messages, otherwise false + bool get hasErrors { + return errors.isNotEmpty; + } + + T? data; + List errors = []; +} + +class ResultError { + final int errorCode; + final String errorMessage; + + const ResultError(this.errorCode, this.errorMessage); +} diff --git a/lib/src/models/retrieve_events_params.dart b/lib/src/models/retrieve_events_params.dart new file mode 100644 index 00000000..7965b0e1 --- /dev/null +++ b/lib/src/models/retrieve_events_params.dart @@ -0,0 +1,7 @@ +class RetrieveEventsParams { + final List? eventIds; + final DateTime? startDate; + final DateTime? endDate; + + const RetrieveEventsParams({this.eventIds, this.startDate, this.endDate}); +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..7c99a9c5 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,29 @@ +name: device_calendar +description: A cross platform plugin for modifying calendars on the user's device. +version: 4.3.1 +homepage: https://github.com/builttoroam/device_calendar/tree/master + +dependencies: + flutter: + sdk: flutter + collection: ^1.16.0 + timezone: ^0.9.0 + rrule: ^0.2.10 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.1 + +flutter: + plugin: + platforms: + android: + package: com.builttoroam.devicecalendar + pluginClass: DeviceCalendarPlugin + ios: + pluginClass: DeviceCalendarPlugin + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart new file mode 100644 index 00000000..132aad61 --- /dev/null +++ b/test/device_calendar_test.dart @@ -0,0 +1,245 @@ +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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channel = MethodChannel('plugins.builttoroam.com/device_calendar'); + var deviceCalendarPlugin = DeviceCalendarPlugin(); + + final log = []; + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + print('Calling channel method ${methodCall.method}'); + log.add(methodCall); + + return null; + }); + + log.clear(); + }); + + test('HasPermissions_Returns_Successfully', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return true; + }); + + final result = await deviceCalendarPlugin.hasPermissions(); + expect(result.isSuccess, true); + expect(result.errors, isEmpty); + expect(result.data, true); + }); + + test('RequestPermissions_Returns_Successfully', () async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return true; + }); + + final result = await deviceCalendarPlugin.requestPermissions(); + expect(result.isSuccess, true); + expect(result.errors, isEmpty); + expect(result.data, true); + }); + + test('RetrieveCalendars_Returns_Successfully', () async { + const fakeCalendarName = 'fakeCalendarName'; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return '[{"id":"1","isReadOnly":false,"name":"$fakeCalendarName"}]'; + }); + + final result = await deviceCalendarPlugin.retrieveCalendars(); + expect(result.isSuccess, true); + expect(result.errors, isEmpty); + expect(result.data, isNotNull); + expect(result.data, isNotEmpty); + expect(result.data?[0].name, fakeCalendarName); + }); + + test('RetrieveEvents_CalendarId_IsRequired', () async { + const String? calendarId = null; + const params = RetrieveEventsParams(); + + final result = + await deviceCalendarPlugin.retrieveEvents(calendarId, params); + expect(result.isSuccess, false); + expect(result.errors.length, greaterThan(0)); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); + }); + + test('DeleteEvent_CalendarId_IsRequired', () async { + const String? calendarId = null; + const eventId = 'fakeEventId'; + + final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); + expect(result.isSuccess, false); + expect(result.errors.length, greaterThan(0)); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); + }); + + test('DeleteEvent_EventId_IsRequired', () async { + const calendarId = 'fakeCalendarId'; + const String? eventId = null; + + final result = await deviceCalendarPlugin.deleteEvent(calendarId, eventId); + expect(result.isSuccess, false); + expect(result.errors.length, greaterThan(0)); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); + }); + + test('DeleteEvent_PassesArguments_Correctly', () async { + const calendarId = 'fakeCalendarId'; + const eventId = 'fakeEventId'; + + await deviceCalendarPlugin.deleteEvent(calendarId, eventId); + expect(log, [ + isMethodCall('deleteEvent', arguments: { + 'calendarId': calendarId, + 'eventId': eventId + }) + ]); + }); + + test('CreateEvent_Arguments_Invalid', () async { + const String? fakeCalendarId = null; + final event = Event(fakeCalendarId); + + final result = await deviceCalendarPlugin.createOrUpdateEvent(event); + expect(result!.isSuccess, false); + expect(result.errors, isNotEmpty); + expect(result.errors[0].errorCode, equals(ErrorCodes.invalidArguments)); + }); + + test('CreateEvent_Returns_Successfully', () async { + const fakeNewEventId = 'fakeNewEventId'; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return fakeNewEventId; + }); + + const fakeCalendarId = 'fakeCalendarId'; + final event = Event(fakeCalendarId); + event.title = 'fakeEventTitle'; + event.start = TZDateTime.now(local); + event.end = event.start!.add(const Duration(hours: 1)); + + final result = await deviceCalendarPlugin.createOrUpdateEvent(event); + expect(result?.isSuccess, true); + expect(result?.errors, isEmpty); + expect(result?.data, isNotEmpty); + expect(result?.data, fakeNewEventId); + }); + + test('UpdateEvent_Returns_Successfully', () async { + const fakeNewEventId = 'fakeNewEventId'; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + final arguments = methodCall.arguments as Map; + if (!arguments.containsKey('eventId') || arguments['eventId'] == null) { + return null; + } + + return fakeNewEventId; + }); + + const fakeCalendarId = 'fakeCalendarId'; + final event = Event(fakeCalendarId); + event.eventId = 'fakeEventId'; + event.title = 'fakeEventTitle'; + event.start = TZDateTime.now(local); + event.end = event.start!.add(const Duration(hours: 1)); + + final result = await deviceCalendarPlugin.createOrUpdateEvent(event); + expect(result?.isSuccess, true); + expect(result?.errors, isEmpty); + expect(result?.data, isNotEmpty); + expect(result?.data, fakeNewEventId); + }); + + test('Attendee_Serialises_Correctly', () async { + final attendee = Attendee( + name: 'Test Attendee', + emailAddress: 'test@t.com', + role: AttendeeRole.Required, + isOrganiser: true); + final stringAttendee = attendee.toJson(); + expect(stringAttendee, isNotNull); + final newAttendee = Attendee.fromJson(stringAttendee); + expect(newAttendee, isNotNull); + expect(newAttendee.name, equals(attendee.name)); + expect(newAttendee.emailAddress, equals(attendee.emailAddress)); + expect(newAttendee.role, equals(attendee.role)); + expect(newAttendee.isOrganiser, equals(attendee.isOrganiser)); + expect(newAttendee.iosAttendeeDetails, isNull); + expect(newAttendee.androidAttendeeDetails, isNull); + }); + + test('Event_Serializes_Correctly', () async { + final startTime = TZDateTime( + timeZoneDatabase.locations.entries.skip(20).first.value, + 1980, + 10, + 1, + 0, + 0, + 0); + final endTime = TZDateTime( + timeZoneDatabase.locations.entries.skip(21).first.value, + 1980, + 10, + 2, + 0, + 0, + 0); + final attendee = Attendee( + name: 'Test Attendee', + emailAddress: 'test@t.com', + role: AttendeeRole.Required, + isOrganiser: true); + final recurrence = RecurrenceRule(frequency: Frequency.daily); + final reminder = Reminder(minutes: 10); + var event = Event('calendarId', + eventId: 'eventId', + title: 'Test Event', + start: startTime, + location: 'Seattle, Washington', + url: Uri.dataFromString('http://www.example.com'), + end: endTime, + attendees: [attendee], + description: 'Test description', + recurrenceRule: recurrence, + reminders: [reminder], + availability: Availability.Busy, + status: EventStatus.Confirmed); + + final stringEvent = event.toJson(); + expect(stringEvent, isNotNull); + final newEvent = Event.fromJson(stringEvent); + expect(newEvent, isNotNull); + expect(newEvent.calendarId, equals(event.calendarId)); + expect(newEvent.eventId, equals(event.eventId)); + expect(newEvent.title, equals(event.title)); + expect(newEvent.start!.millisecondsSinceEpoch, + equals(event.start!.millisecondsSinceEpoch)); + expect(newEvent.end!.millisecondsSinceEpoch, + equals(event.end!.millisecondsSinceEpoch)); + expect(newEvent.description, equals(event.description)); + expect(newEvent.url, equals(event.url)); + expect(newEvent.location, equals(event.location)); + expect(newEvent.attendees, isNotNull); + expect(newEvent.attendees?.length, equals(1)); + expect(newEvent.recurrenceRule, isNotNull); + expect(newEvent.recurrenceRule?.frequency, + equals(event.recurrenceRule?.frequency)); + expect(newEvent.reminders, isNotNull); + expect(newEvent.reminders?.length, equals(1)); + expect(newEvent.availability, equals(event.availability)); + expect(newEvent.status, equals(event.status)); + }); +} From 933d963000e7488566016dbd7b8a81e6a8dc36a8 Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Wed, 6 Dec 2023 12:45:05 +0100 Subject: [PATCH 06/11] added name to calendar --- .../devicecalendar/CalendarDelegate.kt | 5 +++- .../devicecalendar/common/Constants.kt | 6 +++-- .../devicecalendar/models/Calendar.kt | 1 + lib/src/models/calendar.dart | 25 ++++++++++++------- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 491370e7..8517a9f5 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -901,6 +901,8 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : 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 name = cursor.getString(Cst.CALENDAR_PROJECTION_NAME_INDEX) + val calendar = Calendar( calId.toString(), @@ -908,7 +910,8 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : calendarColor, accountName, accountType, - ownerAccount + ownerAccount, + name, ) calendar.isReadOnly = isCalendarReadOnly(accessLevel) 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 052c7278..912c0b19 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -12,6 +12,8 @@ class Constants { const val CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX: Int = 5 const val CALENDAR_PROJECTION_COLOR_INDEX: Int = 6 const val CALENDAR_PROJECTION_IS_PRIMARY_INDEX: Int = 7 + const val CALENDAR_PROJECTION_NAME_INDEX: Int = 8 + // API 17 or higher val CALENDAR_PROJECTION: Array = arrayOf( @@ -22,8 +24,8 @@ class Constants { CalendarContract.Calendars.OWNER_ACCOUNT, // 4 CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 CalendarContract.Calendars.CALENDAR_COLOR, // 6 - CalendarContract.Calendars.IS_PRIMARY // 7 - + CalendarContract.Calendars.IS_PRIMARY, // 7 + NAME // 8 ) // API 16 or lower 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 6e10b7fe..d70a113e 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -7,6 +7,7 @@ class Calendar( val accountName: String, val accountType: String, val ownerAccount: String? + val name_: String ) { var isReadOnly: Boolean = false var isDefault: Boolean = false diff --git a/lib/src/models/calendar.dart b/lib/src/models/calendar.dart index c04ab405..bbdee800 100644 --- a/lib/src/models/calendar.dart +++ b/lib/src/models/calendar.dart @@ -21,14 +21,19 @@ class Calendar { // Read-only. Account type associated with the calendar String? accountType; - Calendar( - {this.id, - this.name, - this.isReadOnly, - this.isDefault, - this.color, - this.accountName, - this.accountType}); + /// The name of this calendar (the other one up is the "display name") + String? name_; + + Calendar({ + this.id, + this.name, + this.isReadOnly, + this.isDefault, + this.color, + this.accountName, + this.accountType, + this.name_, + }); Calendar.fromJson(Map json) { id = json['id']; @@ -38,6 +43,7 @@ class Calendar { color = json['color']; accountName = json['accountName']; accountType = json['accountType']; + name_ = json['name_']; } Map toJson() { @@ -48,7 +54,8 @@ class Calendar { 'isDefault': isDefault, 'color': color, 'accountName': accountName, - 'accountType': accountType + 'accountType': accountType, + 'name_': name_ }; return data; From 9f0ae7dcfd16e22b62e93b6b29554b5671121b22 Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Wed, 6 Dec 2023 12:51:57 +0100 Subject: [PATCH 07/11] added name to calendar --- .../kotlin/com/builttoroam/devicecalendar/common/Constants.kt | 2 +- .../kotlin/com/builttoroam/devicecalendar/models/Calendar.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 912c0b19..43123b4c 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -25,7 +25,7 @@ class Constants { CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 CalendarContract.Calendars.CALENDAR_COLOR, // 6 CalendarContract.Calendars.IS_PRIMARY, // 7 - NAME // 8 + "name" // 8 ) // API 16 or lower 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 d70a113e..9bcacf29 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -6,7 +6,7 @@ class Calendar( val color: Int, val accountName: String, val accountType: String, - val ownerAccount: String? + val ownerAccount: String?, val name_: String ) { var isReadOnly: Boolean = false From 5c1c843ddaf21e563ed17dc212f9291c1dded64d Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Wed, 6 Dec 2023 13:29:29 +0100 Subject: [PATCH 08/11] added name to calendar --- .../com/builttoroam/devicecalendar/CalendarDelegate.kt | 4 ++-- .../com/builttoroam/devicecalendar/common/Constants.kt | 4 ++-- .../com/builttoroam/devicecalendar/models/Calendar.kt | 2 +- lib/src/models/calendar.dart | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 8517a9f5..7164223d 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -901,7 +901,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : 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 name = cursor.getString(Cst.CALENDAR_PROJECTION_NAME_INDEX) +// val name = cursor.getString(Cst.CALENDAR_PROJECTION_NAME_INDEX) val calendar = Calendar( @@ -911,7 +911,7 @@ class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : accountName, accountType, ownerAccount, - name, +// name, ) calendar.isReadOnly = isCalendarReadOnly(accessLevel) 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 43123b4c..f5420b13 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -12,7 +12,7 @@ class Constants { const val CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX: Int = 5 const val CALENDAR_PROJECTION_COLOR_INDEX: Int = 6 const val CALENDAR_PROJECTION_IS_PRIMARY_INDEX: Int = 7 - const val CALENDAR_PROJECTION_NAME_INDEX: Int = 8 +// const val CALENDAR_PROJECTION_NAME_INDEX: Int = 8 // API 17 or higher @@ -25,7 +25,7 @@ class Constants { CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 CalendarContract.Calendars.CALENDAR_COLOR, // 6 CalendarContract.Calendars.IS_PRIMARY, // 7 - "name" // 8 +// CalendarContract.Calendars.NAME // 8 ) // API 16 or lower 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 9bcacf29..59d2972e 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Calendar.kt @@ -7,7 +7,7 @@ class Calendar( val accountName: String, val accountType: String, val ownerAccount: String?, - val name_: String +// val name_: String ) { var isReadOnly: Boolean = false var isDefault: Boolean = false diff --git a/lib/src/models/calendar.dart b/lib/src/models/calendar.dart index bbdee800..3b26465a 100644 --- a/lib/src/models/calendar.dart +++ b/lib/src/models/calendar.dart @@ -32,7 +32,7 @@ class Calendar { this.color, this.accountName, this.accountType, - this.name_, + // this.name_, }); Calendar.fromJson(Map json) { @@ -43,7 +43,7 @@ class Calendar { color = json['color']; accountName = json['accountName']; accountType = json['accountType']; - name_ = json['name_']; + // name_ = json['name_']; } Map toJson() { @@ -55,7 +55,7 @@ class Calendar { 'color': color, 'accountName': accountName, 'accountType': accountType, - 'name_': name_ + // 'name_': name_ }; return data; From 07df224c0006f4e1dc24b4be8cdef1809eaf9e08 Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Wed, 6 Dec 2023 13:33:46 +0100 Subject: [PATCH 09/11] added name to calendar --- lib/src/models/calendar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/models/calendar.dart b/lib/src/models/calendar.dart index 3b26465a..f8ee5863 100644 --- a/lib/src/models/calendar.dart +++ b/lib/src/models/calendar.dart @@ -22,7 +22,7 @@ class Calendar { String? accountType; /// The name of this calendar (the other one up is the "display name") - String? name_; + // String? name_; Calendar({ this.id, From e08b1fe8f8fc0b3dc410e486bd5e8915d8b9b350 Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Tue, 2 Apr 2024 20:14:31 +0200 Subject: [PATCH 10/11] We allow minus values for reminders, because all day event reminders on that day are set with "minus minutes" (eg. -300) --- lib/src/models/reminder.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/src/models/reminder.dart b/lib/src/models/reminder.dart index 761ab676..2a6c595e 100644 --- a/lib/src/models/reminder.dart +++ b/lib/src/models/reminder.dart @@ -4,9 +4,8 @@ class Reminder { /// The time when the reminder should be triggered expressed in terms of minutes before the start of the event int? minutes; - Reminder({@required this.minutes}) - : assert(minutes != null && minutes >= 0, - 'Minutes must be greater than or equal than zero'); + //2.4.24 We allow minus values for reminders, because all day event reminders on that day are set with "minus minutes" (eg. -300) + Reminder({@required this.minutes}) : assert(minutes != null, 'Minutes must not be null'); Reminder.fromJson(Map json) { minutes = json['minutes'] as int; From b0d21ada0aa3bcb3bda7544259416d2b215951ab Mon Sep 17 00:00:00 2001 From: NaoKreuzeder Date: Sun, 7 Apr 2024 17:44:15 +0200 Subject: [PATCH 11/11] Added event color when create or update an event --- .../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 7164223d..76781b5a 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -625,6 +625,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, event.eventColor) return values }