diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 25401783..744c0ba4 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -34,8 +34,9 @@ jobs: flutter build apk test-ios: name: iOS build test - runs-on: macos-latest + runs-on: macos-13 steps: + - uses: maxim-lobanov/setup-xcode@v1 - uses: actions/checkout@v2 - uses: actions/setup-java@v1 with: diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index b836df6a..a08733c0 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -17,6 +17,8 @@ jobs: name: Development Release # The type of runner that the job will run on runs-on: ubuntu-latest + permissions: + id-token: write # Required for authentication using OIDC # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -36,15 +38,10 @@ jobs: run: | sed -i "0,/\#\# \[.*/s//## [${{steps.changelog_reader.outputs.version}}-$GITHUB_RUN_ID]/" CHANGELOG.md cat CHANGELOG.md - - name: Setup credentials - run: | - cat < $PUB_CACHE/credentials.json - ${{ secrets.CREDENTIALS }} - EOF - name: Publish package - run: flutter pub publish --force + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 - name: Add entry to Github release uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.changelog_reader.outputs.version }}+${{ github.run_id }} - prerelease: true \ No newline at end of file + prerelease: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35967b42..9d4a3728 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,8 @@ jobs: release: # The type of runner that the job will run on runs-on: ubuntu-latest + permissions: + id-token: write # Required for authentication using OIDC # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -25,13 +27,8 @@ jobs: channel: "stable" - run: dart --version - run: flutter --version - - name: Setup credentials - run: | - cat < $PUB_CACHE/credentials.json - ${{ secrets.CREDENTIALS }} - EOF - name: Publish package - run: flutter pub publish --force + uses: dart-lang/setup-dart/.github/workflows/publish.yml@v1 - name: Get Changelog Entry id: changelog_reader uses: mindsers/changelog-reader-action@v2.0.0 @@ -39,4 +36,4 @@ jobs: uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.changelog_reader.outputs.version }} - body: ${{ steps.changelog_reader.outputs.changes }} \ No newline at end of file + body: ${{ steps.changelog_reader.outputs.changes }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6285b62b..2fa175fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ +## [4.3.1](https://github.com/builttoroam/device_calendar/releases/tag/4.3.1) + +- Fixed an [issue](https://github.com/builttoroam/device_calendar/issues/470) that prevented the plugin from being used with Kotlin 1.7.10 + ## [4.3.0](https://github.com/builttoroam/device_calendar/releases/tag/4.3.0) - Updated multiple underlying dependencies diff --git a/README.md b/README.md index a91c6e5c..2af1e99d 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ If you don't need any timezone specific features in your app, you may use `flutt ```dart import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +initializeTimeZones(); + // As an example, our default timezone is UTC. Location _currentLocation = getLocation('Etc/UTC'); @@ -78,6 +80,8 @@ The following will need to be added to the `AndroidManifest.xml` file for your a ``` ### Proguard / R8 exceptions +> NOTE: From v4.3.2 developers no longer need to add proguard rule in their app. + By default, all android apps go through R8 for file shrinking when building a release version. Currently, it interferes with some functions such as `retrieveCalendars()`. @@ -107,4 +111,11 @@ For iOS 10+ support, you'll need to modify the `Info.plist` to add the following Access contacts for event attendee editing. ``` +For iOS 17+ support, add the following key/value pair as well. + +```xml +NSCalendarsFullAccessUsageDescription +Access most functions for calendar viewing and editing. +``` + Note that on iOS, this is a Swift plugin. There is a known issue being tracked [here](https://github.com/flutter/flutter/issues/16049) by the Flutter team, where adding a plugin developed in Swift to an Objective-C project causes problems. If you run into such issues, please look at the suggested workarounds there. diff --git a/android/build.gradle b/android/build.gradle index eb0783b7..73ccfc61 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,7 +2,7 @@ group 'com.builttoroam.devicecalendar' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.8.22' repositories { google() mavenCentral() @@ -25,14 +25,15 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 16 + minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard-rules.pro' } lintOptions { disable 'InvalidPackage' @@ -45,12 +46,13 @@ android { 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.11.2' + implementation 'org.dmfs:lib-recur:0.12.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 2212bc37..3c9d0852 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Thu May 17 10:56:13 AEST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/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/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt index 707b9286..5a803a6b 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/AvailabilitySerializer.kt @@ -4,9 +4,13 @@ import com.builttoroam.devicecalendar.models.Availability import com.google.gson.* import java.lang.reflect.Type -class AvailabilitySerializer: JsonSerializer { - override fun serialize(src: Availability?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { +class AvailabilitySerializer : JsonSerializer { + override fun serialize( + src: Availability?, + typeOfSrc: Type?, + context: JsonSerializationContext? + ): JsonElement { + if (src != null) { return JsonPrimitive(src.name) } return JsonObject() diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt index 744f636e..dd73716f 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/CalendarDelegate.kt @@ -10,62 +10,14 @@ import android.content.pm.PackageManager import android.database.Cursor import android.graphics.Color import android.net.Uri +import android.os.Build import android.os.Handler import android.os.Looper import android.provider.CalendarContract import android.provider.CalendarContract.CALLER_IS_SYNCADAPTER import android.provider.CalendarContract.Events import android.text.format.DateUtils -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_EMAIL_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_NAME_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_PROJECTION -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_RELATIONSHIP_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_STATUS_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.ATTENDEE_TYPE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_COLOR_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_DISPLAY_NAME_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_ID_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_IS_PRIMARY_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_OLDER_API -import com.builttoroam.devicecalendar.common.Constants.Companion.CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_BEGIN_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_END_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_ID_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_LAST_DATE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_INSTANCE_DELETION_RRULE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_ALL_DAY_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_AVAILABILITY_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_BEGIN_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_CUSTOM_APP_URI_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_DESCRIPTION_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_END_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_END_TIMEZONE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_EVENT_LOCATION_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_ID_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_RECURRING_RULE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_START_TIMEZONE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_STATUS_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.EVENT_PROJECTION_TITLE_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.REMINDER_MINUTES_INDEX -import com.builttoroam.devicecalendar.common.Constants.Companion.REMINDER_PROJECTION -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.GENERIC_ERROR -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.INVALID_ARGUMENT -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.NOT_ALLOWED -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.NOT_AUTHORIZED -import com.builttoroam.devicecalendar.common.ErrorCodes.Companion.NOT_FOUND import com.builttoroam.devicecalendar.common.ErrorMessages -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE -import com.builttoroam.devicecalendar.common.ErrorMessages.Companion.NOT_AUTHORIZED_MESSAGE -import com.builttoroam.devicecalendar.common.RecurrenceFrequency import com.builttoroam.devicecalendar.models.* import com.builttoroam.devicecalendar.models.Calendar import com.google.gson.Gson @@ -75,44 +27,54 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry import kotlinx.coroutines.* import org.dmfs.rfc5545.DateTime +import org.dmfs.rfc5545.DateTime.UTC import org.dmfs.rfc5545.Weekday -import org.dmfs.rfc5545.recur.Freq -import java.text.SimpleDateFormat +import org.dmfs.rfc5545.recur.RecurrenceRule.WeekdayNum import java.util.* - -class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { - private val RETRIEVE_CALENDARS_REQUEST_CODE = 0 - private val RETRIEVE_EVENTS_REQUEST_CODE = RETRIEVE_CALENDARS_REQUEST_CODE + 1 - private val RETRIEVE_CALENDAR_REQUEST_CODE = RETRIEVE_EVENTS_REQUEST_CODE + 1 - private val CREATE_OR_UPDATE_EVENT_REQUEST_CODE = RETRIEVE_CALENDAR_REQUEST_CODE + 1 - private val DELETE_EVENT_REQUEST_CODE = CREATE_OR_UPDATE_EVENT_REQUEST_CODE + 1 - private val REQUEST_PERMISSIONS_REQUEST_CODE = DELETE_EVENT_REQUEST_CODE + 1 - private val DELETE_CALENDAR_REQUEST_CODE = REQUEST_PERMISSIONS_REQUEST_CODE + 1 - private val PART_TEMPLATE = ";%s=" - private val BYMONTHDAY_PART = "BYMONTHDAY" - private val BYMONTH_PART = "BYMONTH" - private val BYSETPOS_PART = "BYSETPOS" - - private val _cachedParametersMap: MutableMap = mutableMapOf() - private var _binding: ActivityPluginBinding? = null - private var _context: Context? = null +import kotlin.math.absoluteValue +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import com.builttoroam.devicecalendar.common.Constants.Companion as Cst +import com.builttoroam.devicecalendar.common.ErrorCodes.Companion as EC +import com.builttoroam.devicecalendar.common.ErrorMessages.Companion as EM +import org.dmfs.rfc5545.recur.Freq as RruleFreq +import org.dmfs.rfc5545.recur.RecurrenceRule as Rrule +import android.provider.CalendarContract.Colors +import androidx.collection.SparseArrayCompat + +private const val RETRIEVE_CALENDARS_REQUEST_CODE = 0 +private const val RETRIEVE_EVENTS_REQUEST_CODE = RETRIEVE_CALENDARS_REQUEST_CODE + 1 +private const val RETRIEVE_CALENDAR_REQUEST_CODE = RETRIEVE_EVENTS_REQUEST_CODE + 1 +private const val CREATE_OR_UPDATE_EVENT_REQUEST_CODE = RETRIEVE_CALENDAR_REQUEST_CODE + 1 +private const val DELETE_EVENT_REQUEST_CODE = CREATE_OR_UPDATE_EVENT_REQUEST_CODE + 1 +private const val REQUEST_PERMISSIONS_REQUEST_CODE = DELETE_EVENT_REQUEST_CODE + 1 +private const val DELETE_CALENDAR_REQUEST_CODE = REQUEST_PERMISSIONS_REQUEST_CODE + 1 + +class CalendarDelegate(binding: ActivityPluginBinding?, context: Context) : + PluginRegistry.RequestPermissionsResultListener { + + private val _cachedParametersMap: MutableMap = + mutableMapOf() + private var _binding: ActivityPluginBinding? = binding + private var _context: Context? = context private var _gson: Gson? = null private val uiThreadHandler = Handler(Looper.getMainLooper()) - constructor(binding: ActivityPluginBinding?, context: Context) { - _binding = binding - _context = context + init { val gsonBuilder = GsonBuilder() - gsonBuilder.registerTypeAdapter(RecurrenceFrequency::class.java, RecurrenceFrequencySerializer()) - gsonBuilder.registerTypeAdapter(DayOfWeek::class.java, DayOfWeekSerializer()) gsonBuilder.registerTypeAdapter(Availability::class.java, AvailabilitySerializer()) gsonBuilder.registerTypeAdapter(EventStatus::class.java, EventStatusSerializer()) _gson = gsonBuilder.create() } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray): Boolean { - val permissionGranted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ): Boolean { + val permissionGranted = + grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED if (!_cachedParametersMap.containsKey(requestCode)) { // this plugin doesn't handle this request code @@ -120,13 +82,17 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } val cachedValues: CalendarMethodsParametersCacheModel = _cachedParametersMap[requestCode] - ?: // unlikely scenario where another plugin is potentially using the same request code but it's not one we are tracking so return to - // indicate we're not handling the request - return false + ?: // unlikely scenario where another plugin is potentially using the same request code but it's not one we are tracking so return to + // indicate we're not handling the request + return false try { if (!permissionGranted) { - finishWithError(NOT_AUTHORIZED, NOT_AUTHORIZED_MESSAGE, cachedValues.pendingChannelResult) + finishWithError( + EC.NOT_AUTHORIZED, + EM.NOT_AUTHORIZED_MESSAGE, + cachedValues.pendingChannelResult + ) return false } @@ -135,22 +101,36 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { retrieveCalendars(cachedValues.pendingChannelResult) } RETRIEVE_EVENTS_REQUEST_CODE -> { - retrieveEvents(cachedValues.calendarId, cachedValues.calendarEventsStartDate, cachedValues.calendarEventsEndDate, cachedValues.calendarEventsIds, cachedValues.pendingChannelResult) + retrieveEvents( + cachedValues.calendarId, + cachedValues.calendarEventsStartDate, + cachedValues.calendarEventsEndDate, + cachedValues.calendarEventsIds, + cachedValues.pendingChannelResult + ) } RETRIEVE_CALENDAR_REQUEST_CODE -> { retrieveCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) } CREATE_OR_UPDATE_EVENT_REQUEST_CODE -> { - createOrUpdateEvent(cachedValues.calendarId, cachedValues.event, cachedValues.pendingChannelResult) + createOrUpdateEvent( + cachedValues.calendarId, + cachedValues.event, + cachedValues.pendingChannelResult + ) } DELETE_EVENT_REQUEST_CODE -> { - deleteEvent(cachedValues.calendarId, cachedValues.eventId, cachedValues.pendingChannelResult) + deleteEvent( + cachedValues.calendarId, + cachedValues.eventId, + cachedValues.pendingChannelResult + ) } REQUEST_PERMISSIONS_REQUEST_CODE -> { finishWithSuccess(permissionGranted, cachedValues.pendingChannelResult) } DELETE_CALENDAR_REQUEST_CODE -> { - deleteCalendar(cachedValues.calendarId,cachedValues.pendingChannelResult) + deleteCalendar(cachedValues.calendarId, cachedValues.pendingChannelResult) } } @@ -164,7 +144,10 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { if (arePermissionsGranted()) { finishWithSuccess(true, pendingChannelResult) } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, REQUEST_PERMISSIONS_REQUEST_CODE) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + REQUEST_PERMISSIONS_REQUEST_CODE + ) requestPermissions(parameters) } } @@ -179,9 +162,9 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val contentResolver: ContentResolver? = _context?.contentResolver val uri: Uri = CalendarContract.Calendars.CONTENT_URI val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query(uri, CALENDAR_PROJECTION, null, null, null) + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION, null, null, null) } else { - contentResolver?.query(uri, CALENDAR_PROJECTION_OLDER_API, null, null, null) + contentResolver?.query(uri, Cst.CALENDAR_PROJECTION_OLDER_API, null, null, null) } val calendars: MutableList = mutableListOf() try { @@ -192,22 +175,33 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { finishWithSuccess(_gson?.toJson(calendars), pendingChannelResult) } catch (e: Exception) { - finishWithError(GENERIC_ERROR, e.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) } finally { cursor?.close() } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, RETRIEVE_CALENDARS_REQUEST_CODE) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDARS_REQUEST_CODE + ) requestPermissions(parameters) } } - private fun retrieveCalendar(calendarId: String, pendingChannelResult: MethodChannel.Result, isInternalCall: Boolean = false): Calendar? { + private fun retrieveCalendar( + calendarId: String, + pendingChannelResult: MethodChannel.Result, + isInternalCall: Boolean = false + ): Calendar? { if (isInternalCall || arePermissionsGranted()) { val calendarIdNumber = calendarId.toLongOrNull() if (calendarIdNumber == null) { if (!isInternalCall) { - finishWithError(INVALID_ARGUMENT, CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, + pendingChannelResult + ) } return null } @@ -216,9 +210,21 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val uri: Uri = CalendarContract.Calendars.CONTENT_URI val cursor: Cursor? = if (atLeastAPI(17)) { - contentResolver?.query(ContentUris.withAppendedId(uri, calendarIdNumber), CALENDAR_PROJECTION, null, null, null) + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION, + null, + null, + null + ) } else { - contentResolver?.query(ContentUris.withAppendedId(uri, calendarIdNumber), CALENDAR_PROJECTION_OLDER_API, null, null, null) + contentResolver?.query( + ContentUris.withAppendedId(uri, calendarIdNumber), + Cst.CALENDAR_PROJECTION_OLDER_API, + null, + null, + null + ) } try { @@ -231,74 +237,116 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } else { if (!isInternalCall) { - finishWithError(NOT_FOUND, "The calendar with the ID $calendarId could not be found", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) } } } catch (e: Exception) { - finishWithError(GENERIC_ERROR, e.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, e.message, pendingChannelResult) } finally { cursor?.close() } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, RETRIEVE_CALENDAR_REQUEST_CODE, calendarId) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_CALENDAR_REQUEST_CODE, + calendarId + ) requestPermissions(parameters) } return null } - fun deleteCalendar(calendarId: String, pendingChannelResult: MethodChannel.Result, isInternalCall: Boolean = false): Calendar? { + fun deleteCalendar( + calendarId: String, + pendingChannelResult: MethodChannel.Result, + isInternalCall: Boolean = false + ): Calendar? { if (isInternalCall || arePermissionsGranted()) { val calendarIdNumber = calendarId.toLongOrNull() if (calendarIdNumber == null) { if (!isInternalCall) { - finishWithError(INVALID_ARGUMENT, CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + EM.CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE, + pendingChannelResult + ) } return null } val contentResolver: ContentResolver? = _context?.contentResolver - val calendar = retrieveCalendar(calendarId,pendingChannelResult,true); - if(calendar != null) { - val calenderUriWithId = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarIdNumber) + val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) + if (calendar != null) { + val calenderUriWithId = ContentUris.withAppendedId( + CalendarContract.Calendars.CONTENT_URI, + calendarIdNumber + ) val deleteSucceeded = contentResolver?.delete(calenderUriWithId, null, null) ?: 0 finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) - }else { + } else { if (!isInternalCall) { - finishWithError(NOT_FOUND, "The calendar with the ID $calendarId could not be found", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) } } } else { val parameters = CalendarMethodsParametersCacheModel( - pendingChannelResult = pendingChannelResult, - calendarDelegateMethodCode = DELETE_CALENDAR_REQUEST_CODE, - calendarId = calendarId) + pendingChannelResult = pendingChannelResult, + calendarDelegateMethodCode = DELETE_CALENDAR_REQUEST_CODE, + calendarId = calendarId + ) requestPermissions(parameters) } return null } - fun createCalendar(calendarName: String, calendarColor: String?, localAccountName: String, pendingChannelResult: MethodChannel.Result) { + fun createCalendar( + calendarName: String, + calendarColor: String?, + localAccountName: String, + pendingChannelResult: MethodChannel.Result + ) { val contentResolver: ContentResolver? = _context?.contentResolver var uri = CalendarContract.Calendars.CONTENT_URI uri = uri.buildUpon() - .appendQueryParameter(CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, localAccountName) - .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - .build() + .appendQueryParameter(CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, localAccountName) + .appendQueryParameter( + CalendarContract.Calendars.ACCOUNT_TYPE, + CalendarContract.ACCOUNT_TYPE_LOCAL + ) + .build() val values = ContentValues() values.put(CalendarContract.Calendars.NAME, calendarName) values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, calendarName) values.put(CalendarContract.Calendars.ACCOUNT_NAME, localAccountName) values.put(CalendarContract.Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL) - values.put(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, CalendarContract.Calendars.CAL_ACCESS_OWNER) - values.put(CalendarContract.Calendars.CALENDAR_COLOR, Color.parseColor((calendarColor - ?: "0xFFFF0000").replace("0x", "#"))) // Red colour as a default + values.put( + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, + CalendarContract.Calendars.CAL_ACCESS_OWNER + ) + values.put( + CalendarContract.Calendars.CALENDAR_COLOR, Color.parseColor( + (calendarColor + ?: "0xFFFF0000").replace("0x", "#") + ) + ) // Red colour as a default values.put(CalendarContract.Calendars.OWNER_ACCOUNT, localAccountName) - values.put(CalendarContract.Calendars.CALENDAR_TIME_ZONE, java.util.Calendar.getInstance().timeZone.id) + values.put( + CalendarContract.Calendars.CALENDAR_TIME_ZONE, + java.util.Calendar.getInstance().timeZone.id + ) val result = contentResolver?.insert(uri, values) // Get the calendar ID that is the last element in the Uri @@ -307,16 +355,30 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { finishWithSuccess(calendarId.toString(), pendingChannelResult) } - fun retrieveEvents(calendarId: String, startDate: Long?, endDate: Long?, eventIds: List, pendingChannelResult: MethodChannel.Result) { + fun retrieveEvents( + calendarId: String, + startDate: Long?, + endDate: Long?, + eventIds: List, + pendingChannelResult: MethodChannel.Result + ) { if (startDate == null && endDate == null && eventIds.isEmpty()) { - finishWithError(INVALID_ARGUMENT, ErrorMessages.RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + ErrorMessages.RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE, + pendingChannelResult + ) return } if (arePermissionsGranted()) { val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) if (calendar == null) { - finishWithError(NOT_FOUND, "Couldn't retrieve the Calendar with ID $calendarId", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) return } @@ -328,7 +390,8 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val eventsUri = eventsUriBuilder.build() val eventsCalendarQuery = "(${Events.CALENDAR_ID} = $calendarId)" val eventsNotDeletedQuery = "(${Events.DELETED} != 1)" - val eventsIdsQuery = "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" + val eventsIdsQuery = + "(${CalendarContract.Instances.EVENT_ID} IN (${eventIds.joinToString()}))" var eventsSelectionQuery = "$eventsCalendarQuery AND $eventsNotDeletedQuery" if (eventIds.isNotEmpty()) { @@ -336,13 +399,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } val eventsSortOrder = Events.DTSTART + " DESC" - val eventsCursor = contentResolver?.query(eventsUri, EVENT_PROJECTION, eventsSelectionQuery, null, eventsSortOrder) + val eventsCursor = contentResolver?.query( + eventsUri, + Cst.EVENT_PROJECTION, + eventsSelectionQuery, + null, + eventsSortOrder + ) val events: MutableList = mutableListOf() val exceptionHandler = CoroutineExceptionHandler { _, exception -> uiThreadHandler.post { - finishWithError(GENERIC_ERROR, exception.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) } } @@ -353,7 +422,8 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } for (event in events) { val attendees = retrieveAttendees(calendar, event.eventId!!, contentResolver) - event.organizer = attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } + event.organizer = + attendees.firstOrNull { it.isOrganizer != null && it.isOrganizer } event.attendees = attendees event.reminders = retrieveReminders(event.eventId!!, contentResolver) } @@ -366,23 +436,41 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, RETRIEVE_EVENTS_REQUEST_CODE, calendarId, startDate, endDate) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + RETRIEVE_EVENTS_REQUEST_CODE, + calendarId, + startDate, + endDate + ) requestPermissions(parameters) } return } - fun createOrUpdateEvent(calendarId: String, event: Event?, pendingChannelResult: MethodChannel.Result) { + fun createOrUpdateEvent( + calendarId: String, + event: Event?, + pendingChannelResult: MethodChannel.Result + ) { if (arePermissionsGranted()) { if (event == null) { - finishWithError(GENERIC_ERROR, CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, pendingChannelResult) + finishWithError( + EC.GENERIC_ERROR, + EM.CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE, + pendingChannelResult + ) return } val calendar = retrieveCalendar(calendarId, pendingChannelResult, true) if (calendar == null) { - finishWithError(NOT_FOUND, "Couldn't retrieve the Calendar with ID $calendarId", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "Couldn't retrieve the Calendar with ID $calendarId", + pendingChannelResult + ) return } @@ -391,7 +479,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { val exceptionHandler = CoroutineExceptionHandler { _, exception -> uiThreadHandler.post { - finishWithError(GENERIC_ERROR, exception.message, pendingChannelResult) + finishWithError(EC.GENERIC_ERROR, exception.message, pendingChannelResult) } } @@ -407,14 +495,22 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } else { job = GlobalScope.launch(Dispatchers.IO + exceptionHandler) { - contentResolver?.update(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), values, null, null) - val existingAttendees = retrieveAttendees(calendar, eventId.toString(), contentResolver) - val attendeesToDelete = if (event.attendees.isNotEmpty()) existingAttendees.filter { existingAttendee -> event.attendees.all { it.emailAddress != existingAttendee.emailAddress } } else existingAttendees + contentResolver?.update( + ContentUris.withAppendedId(Events.CONTENT_URI, eventId), + values, + null, + null + ) + val existingAttendees = + retrieveAttendees(calendar, eventId.toString(), contentResolver) + val attendeesToDelete = + if (event.attendees.isNotEmpty()) existingAttendees.filter { existingAttendee -> event.attendees.all { it.emailAddress != existingAttendee.emailAddress } } else existingAttendees for (attendeeToDelete in attendeesToDelete) { deleteAttendee(eventId, attendeeToDelete, contentResolver) } - val attendeesToInsert = event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } + val attendeesToInsert = + event.attendees.filter { existingAttendees.all { existingAttendee -> existingAttendee.emailAddress != it.emailAddress } } insertAttendees(attendeesToInsert, eventId, contentResolver) deleteExistingReminders(contentResolver, eventId) insertReminders(event.reminders, eventId, contentResolver!!) @@ -427,13 +523,13 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } if (existingSelfAttendee != null && newSelfAttendee != null && newSelfAttendee.attendanceStatus != null && - existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus) { + existingSelfAttendee.attendanceStatus != newSelfAttendee.attendanceStatus + ) { updateAttendeeStatus(eventId, newSelfAttendee, contentResolver) } } } - job.invokeOnCompletion { - cause -> + job.invokeOnCompletion { cause -> if (cause == null) { uiThreadHandler.post { finishWithSuccess(eventId.toString(), pendingChannelResult) @@ -441,21 +537,28 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, CREATE_OR_UPDATE_EVENT_REQUEST_CODE, calendarId) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + CREATE_OR_UPDATE_EVENT_REQUEST_CODE, + calendarId + ) parameters.event = event requestPermissions(parameters) } } private fun deleteExistingReminders(contentResolver: ContentResolver?, eventId: Long) { - val cursor = CalendarContract.Reminders.query(contentResolver, eventId, arrayOf( - CalendarContract.Reminders._ID - )) + val cursor = CalendarContract.Reminders.query( + contentResolver, eventId, arrayOf( + CalendarContract.Reminders._ID + ) + ) while (cursor != null && cursor.moveToNext()) { var reminderUri: Uri? = null val reminderId = cursor.getLong(0) if (reminderId > 0) { - reminderUri = ContentUris.withAppendedId(CalendarContract.Reminders.CONTENT_URI, reminderId) + reminderUri = + ContentUris.withAppendedId(CalendarContract.Reminders.CONTENT_URI, reminderId) } if (reminderUri != null) { contentResolver?.delete(reminderUri, null, null) @@ -465,7 +568,11 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun insertReminders(reminders: List, eventId: Long?, contentResolver: ContentResolver) { + private fun insertReminders( + reminders: List, + eventId: Long?, + contentResolver: ContentResolver + ) { if (reminders.isEmpty()) { return } @@ -481,25 +588,47 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { private fun buildEventContentValues(event: Event, calendarId: String): ContentValues { val values = ContentValues() - val duration: String? = null - values.put(Events.ALL_DAY, event.eventAllDay) + + values.put(Events.ALL_DAY, if (event.eventAllDay) 1 else 0) values.put(Events.DTSTART, event.eventStartDate!!) values.put(Events.EVENT_TIMEZONE, getTimeZone(event.eventStartTimeZone).id) - values.put(Events.DTEND, event.eventEndDate!!) - values.put(Events.EVENT_END_TIMEZONE, getTimeZone(event.eventEndTimeZone).id) values.put(Events.TITLE, event.eventTitle) values.put(Events.DESCRIPTION, event.eventDescription) values.put(Events.EVENT_LOCATION, event.eventLocation) values.put(Events.CUSTOM_APP_URI, event.eventURL) values.put(Events.CALENDAR_ID, calendarId) - values.put(Events.DURATION, duration) values.put(Events.AVAILABILITY, getAvailability(event.availability)) - values.put(Events.STATUS, getEventStatus(event.eventStatus)) + var status: Int? = getEventStatus(event.eventStatus) + if (status != null) { + values.put(Events.STATUS, status) + } + + var duration: String? = null + var end: Long? = null + var endTimeZone: String? = null if (event.recurrenceRule != null) { val recurrenceRuleParams = buildRecurrenceRuleParams(event.recurrenceRule!!) values.put(Events.RRULE, recurrenceRuleParams) + val difference = event.eventEndDate!!.minus(event.eventStartDate!!) + val rawDuration = difference.toDuration(DurationUnit.MILLISECONDS) + rawDuration.toComponents { days, hours, minutes, seconds, _ -> + if (days > 0 || hours > 0 || minutes > 0 || seconds > 0) duration = "P" + if (days > 0) duration = duration.plus("${days}D") + if (hours > 0 || minutes > 0 || seconds > 0) duration = duration.plus("T") + if (hours > 0) duration = duration.plus("${hours}H") + if (minutes > 0) duration = duration.plus("${minutes}M") + if (seconds > 0) duration = duration.plus("${seconds}S") + } + } else { + end = event.eventEndDate!! + endTimeZone = getTimeZone(event.eventEndTimeZone).id } + values.put(Events.DTEND, end) + values.put(Events.EVENT_END_TIMEZONE, endTimeZone) + values.put(Events.DURATION, duration) + values.put(Events.EVENT_COLOR_KEY, event.eventColorKey) + values.put(Events.EVENT_COLOR, event.eventColor) return values } @@ -530,7 +659,11 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun insertAttendees(attendees: List, eventId: Long?, contentResolver: ContentResolver?) { + private fun insertAttendees( + attendees: List, + eventId: Long?, + contentResolver: ContentResolver? + ) { if (attendees.isEmpty()) { return } @@ -540,13 +673,13 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { put(CalendarContract.Attendees.ATTENDEE_NAME, it.name) put(CalendarContract.Attendees.ATTENDEE_EMAIL, it.emailAddress) put( - CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, - CalendarContract.Attendees.RELATIONSHIP_ATTENDEE + CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.RELATIONSHIP_ATTENDEE ) put(CalendarContract.Attendees.ATTENDEE_TYPE, it.role) put( - CalendarContract.Attendees.ATTENDEE_STATUS, - it.attendanceStatus + CalendarContract.Attendees.ATTENDEE_STATUS, + it.attendanceStatus ) put(CalendarContract.Attendees.EVENT_ID, eventId) } @@ -556,37 +689,71 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun deleteAttendee(eventId: Long, attendee: Attendee, contentResolver: ContentResolver?) { - val selection = "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" + private fun deleteAttendee( + eventId: Long, + attendee: Attendee, + contentResolver: ContentResolver? + ) { + val selection = + "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" val selectionArgs = arrayOf(eventId.toString() + "", attendee.emailAddress) contentResolver?.delete(CalendarContract.Attendees.CONTENT_URI, selection, selectionArgs) } - private fun updateAttendeeStatus(eventId: Long, attendee: Attendee, contentResolver: ContentResolver?) { - val selection = "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" + private fun updateAttendeeStatus( + eventId: Long, + attendee: Attendee, + contentResolver: ContentResolver? + ) { + val selection = + "(" + CalendarContract.Attendees.EVENT_ID + " = ?) AND (" + CalendarContract.Attendees.ATTENDEE_EMAIL + " = ?)" val selectionArgs = arrayOf(eventId.toString() + "", attendee.emailAddress) val values = ContentValues() values.put(CalendarContract.Attendees.ATTENDEE_STATUS, attendee.attendanceStatus) - contentResolver?.update(CalendarContract.Attendees.CONTENT_URI, values, selection, selectionArgs) + contentResolver?.update( + CalendarContract.Attendees.CONTENT_URI, + values, + selection, + selectionArgs + ) } - fun deleteEvent(calendarId: String, eventId: String, pendingChannelResult: MethodChannel.Result, startDate: Long? = null, endDate: Long? = null, followingInstances: Boolean? = null) { + fun deleteEvent( + calendarId: String, + eventId: String, + pendingChannelResult: MethodChannel.Result, + startDate: Long? = null, + endDate: Long? = null, + followingInstances: Boolean? = null + ) { if (arePermissionsGranted()) { val existingCal = retrieveCalendar(calendarId, pendingChannelResult, true) if (existingCal == null) { - finishWithError(NOT_FOUND, "The calendar with the ID $calendarId could not be found", pendingChannelResult) + finishWithError( + EC.NOT_FOUND, + "The calendar with the ID $calendarId could not be found", + pendingChannelResult + ) return } if (existingCal.isReadOnly) { - finishWithError(NOT_ALLOWED, "Calendar with ID $calendarId is read-only", pendingChannelResult) + finishWithError( + EC.NOT_ALLOWED, + "Calendar with ID $calendarId is read-only", + pendingChannelResult + ) return } val eventIdNumber = eventId.toLongOrNull() if (eventIdNumber == null) { - finishWithError(INVALID_ARGUMENT, EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, pendingChannelResult) + finishWithError( + EC.INVALID_ARGUMENT, + EM.EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE, + pendingChannelResult + ) return } @@ -597,15 +764,25 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { finishWithSuccess(deleteSucceeded > 0, pendingChannelResult) } else { if (!followingInstances!!) { // Only this instance - val exceptionUriWithId = ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) + val exceptionUriWithId = + ContentUris.withAppendedId(Events.CONTENT_EXCEPTION_URI, eventIdNumber) val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate!!, endDate!!) + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) while (instanceCursor.moveToNext()) { - val foundEventID = instanceCursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX) + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) if (eventIdNumber == foundEventID) { - values.put(Events.ORIGINAL_INSTANCE_TIME, instanceCursor.getLong(EVENT_INSTANCE_DELETION_BEGIN_INDEX)) + values.put( + Events.ORIGINAL_INSTANCE_TIME, + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_BEGIN_INDEX) + ) values.put(Events.STATUS, Events.STATUS_CANCELED) } } @@ -614,32 +791,52 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { instanceCursor.close() finishWithSuccess(deleteSucceeded != null, pendingChannelResult) } else { // This and following instances - val eventsUriWithId = ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) + val eventsUriWithId = + ContentUris.withAppendedId(Events.CONTENT_URI, eventIdNumber) val values = ContentValues() - val instanceCursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate!!, endDate!!) + val instanceCursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate!!, + endDate!! + ) while (instanceCursor.moveToNext()) { - val foundEventID = instanceCursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX) + val foundEventID = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX) if (eventIdNumber == foundEventID) { - val newRule = org.dmfs.rfc5545.recur.RecurrenceRule(instanceCursor.getString(EVENT_INSTANCE_DELETION_RRULE_INDEX)) - val lastDate = instanceCursor.getLong(EVENT_INSTANCE_DELETION_LAST_DATE_INDEX) + val newRule = + Rrule(instanceCursor.getString(Cst.EVENT_INSTANCE_DELETION_RRULE_INDEX)) + val lastDate = + instanceCursor.getLong(Cst.EVENT_INSTANCE_DELETION_LAST_DATE_INDEX) if (lastDate > 0 && newRule.count != null && newRule.count > 0) { // Update occurrence rule - val cursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate, lastDate) + val cursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate, + lastDate + ) while (cursor.moveToNext()) { - if (eventIdNumber == cursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX)) { + if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { newRule.count-- } } cursor.close() } else { // Indefinite and specified date rule - val cursor = CalendarContract.Instances.query(contentResolver, EVENT_INSTANCE_DELETION, startDate - DateUtils.YEAR_IN_MILLIS, startDate - 1) + val cursor = CalendarContract.Instances.query( + contentResolver, + Cst.EVENT_INSTANCE_DELETION, + startDate - DateUtils.YEAR_IN_MILLIS, + startDate - 1 + ) var lastRecurrenceDate: Long? = null while (cursor.moveToNext()) { - if (eventIdNumber == cursor.getLong(EVENT_INSTANCE_DELETION_ID_INDEX)) { - lastRecurrenceDate = cursor.getLong(EVENT_INSTANCE_DELETION_END_INDEX) + if (eventIdNumber == cursor.getLong(Cst.EVENT_INSTANCE_DELETION_ID_INDEX)) { + lastRecurrenceDate = + cursor.getLong(Cst.EVENT_INSTANCE_DELETION_END_INDEX) } } @@ -660,7 +857,11 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } } } else { - val parameters = CalendarMethodsParametersCacheModel(pendingChannelResult, DELETE_EVENT_REQUEST_CODE, calendarId) + val parameters = CalendarMethodsParametersCacheModel( + pendingChannelResult, + DELETE_EVENT_REQUEST_CODE, + calendarId + ) parameters.eventId = eventId requestPermissions(parameters) } @@ -683,7 +884,12 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { private fun requestPermissions(requestCode: Int) { if (atLeastAPI(23)) { - _binding!!.activity.requestPermissions(arrayOf(Manifest.permission.WRITE_CALENDAR, Manifest.permission.READ_CALENDAR), requestCode) + _binding!!.activity.requestPermissions( + arrayOf( + Manifest.permission.WRITE_CALENDAR, + Manifest.permission.READ_CALENDAR + ), requestCode + ) } } @@ -692,13 +898,13 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return null } - val calId = cursor.getLong(CALENDAR_PROJECTION_ID_INDEX) - val displayName = cursor.getString(CALENDAR_PROJECTION_DISPLAY_NAME_INDEX) - val accessLevel = cursor.getInt(CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX) - val calendarColor = cursor.getInt(CALENDAR_PROJECTION_COLOR_INDEX) - val accountName = cursor.getString(CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX) - val accountType = cursor.getString(CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX) - val ownerAccount = cursor.getString(CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX) + val calId = cursor.getLong(Cst.CALENDAR_PROJECTION_ID_INDEX) + val displayName = cursor.getString(Cst.CALENDAR_PROJECTION_DISPLAY_NAME_INDEX) + val accessLevel = cursor.getInt(Cst.CALENDAR_PROJECTION_ACCESS_LEVEL_INDEX) + val calendarColor = cursor.getInt(Cst.CALENDAR_PROJECTION_COLOR_INDEX) + val accountName = cursor.getString(Cst.CALENDAR_PROJECTION_ACCOUNT_NAME_INDEX) + val accountType = cursor.getString(Cst.CALENDAR_PROJECTION_ACCOUNT_TYPE_INDEX) + val ownerAccount = cursor.getString(Cst.CALENDAR_PROJECTION_OWNER_ACCOUNT_INDEX) val calendar = Calendar( calId.toString(), @@ -711,7 +917,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { calendar.isReadOnly = isCalendarReadOnly(accessLevel) if (atLeastAPI(17)) { - val isPrimary = cursor.getString(CALENDAR_PROJECTION_IS_PRIMARY_INDEX) + val isPrimary = cursor.getString(Cst.CALENDAR_PROJECTION_IS_PRIMARY_INDEX) calendar.isDefault = isPrimary == "1" } else { calendar.isDefault = false @@ -723,21 +929,21 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { if (cursor == null) { return null } - - val eventId = cursor.getLong(EVENT_PROJECTION_ID_INDEX) - val title = cursor.getString(EVENT_PROJECTION_TITLE_INDEX) - val description = cursor.getString(EVENT_PROJECTION_DESCRIPTION_INDEX) - val begin = cursor.getLong(EVENT_PROJECTION_BEGIN_INDEX) - val end = cursor.getLong(EVENT_PROJECTION_END_INDEX) - val recurringRule = cursor.getString(EVENT_PROJECTION_RECURRING_RULE_INDEX) - val allDay = cursor.getInt(EVENT_PROJECTION_ALL_DAY_INDEX) > 0 - val location = cursor.getString(EVENT_PROJECTION_EVENT_LOCATION_INDEX) - val url = cursor.getString(EVENT_PROJECTION_CUSTOM_APP_URI_INDEX) - val startTimeZone = cursor.getString(EVENT_PROJECTION_START_TIMEZONE_INDEX) - val endTimeZone = cursor.getString(EVENT_PROJECTION_END_TIMEZONE_INDEX) - val availability = parseAvailability(cursor.getInt(EVENT_PROJECTION_AVAILABILITY_INDEX)) - val eventStatus = parseEventStatus(cursor.getInt(EVENT_PROJECTION_STATUS_INDEX)) - + val eventId = cursor.getLong(Cst.EVENT_PROJECTION_ID_INDEX) + val title = cursor.getString(Cst.EVENT_PROJECTION_TITLE_INDEX) + val description = cursor.getString(Cst.EVENT_PROJECTION_DESCRIPTION_INDEX) + val begin = cursor.getLong(Cst.EVENT_PROJECTION_BEGIN_INDEX) + val end = cursor.getLong(Cst.EVENT_PROJECTION_END_INDEX) + val recurringRule = cursor.getString(Cst.EVENT_PROJECTION_RECURRING_RULE_INDEX) + val allDay = cursor.getInt(Cst.EVENT_PROJECTION_ALL_DAY_INDEX) > 0 + val location = cursor.getString(Cst.EVENT_PROJECTION_EVENT_LOCATION_INDEX) + val url = cursor.getString(Cst.EVENT_PROJECTION_CUSTOM_APP_URI_INDEX) + val startTimeZone = cursor.getString(Cst.EVENT_PROJECTION_START_TIMEZONE_INDEX) + val endTimeZone = cursor.getString(Cst.EVENT_PROJECTION_END_TIMEZONE_INDEX) + val availability = parseAvailability(cursor.getInt(Cst.EVENT_PROJECTION_AVAILABILITY_INDEX)) + val eventStatus = parseEventStatus(cursor.getInt(Cst.EVENT_PROJECTION_STATUS_INDEX)) + val eventColor = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_INDEX) + val eventColorKey = cursor.getInt(Cst.EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX) val event = Event() event.eventTitle = title ?: "New Event" event.eventId = eventId.toString() @@ -753,6 +959,8 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { event.eventEndTimeZone = endTimeZone event.availability = availability event.eventStatus = eventStatus + event.eventColor = if (eventColor == 0) null else eventColor + event.eventColorKey = if (eventColorKey == 0) null else eventColorKey return event } @@ -761,64 +969,79 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { if (recurrenceRuleString == null) { return null } - - val rfcRecurrenceRule = org.dmfs.rfc5545.recur.RecurrenceRule(recurrenceRuleString) + val rfcRecurrenceRule = Rrule(recurrenceRuleString) val frequency = when (rfcRecurrenceRule.freq) { - Freq.YEARLY -> RecurrenceFrequency.YEARLY - Freq.MONTHLY -> RecurrenceFrequency.MONTHLY - Freq.WEEKLY -> RecurrenceFrequency.WEEKLY - Freq.DAILY -> RecurrenceFrequency.DAILY + RruleFreq.YEARLY -> RruleFreq.YEARLY + RruleFreq.MONTHLY -> RruleFreq.MONTHLY + RruleFreq.WEEKLY -> RruleFreq.WEEKLY + RruleFreq.DAILY -> RruleFreq.DAILY else -> null - } + } ?: return null + //Avoid handling HOURLY/MINUTELY/SECONDLY frequencies for now - val recurrenceRule = RecurrenceRule(frequency!!) - if (rfcRecurrenceRule.count != null) { - recurrenceRule.totalOccurrences = rfcRecurrenceRule.count - } + val recurrenceRule = RecurrenceRule(frequency) + recurrenceRule.count = rfcRecurrenceRule.count recurrenceRule.interval = rfcRecurrenceRule.interval - if (rfcRecurrenceRule.until != null) { - recurrenceRule.endDate = rfcRecurrenceRule.until.timestamp + + val until = rfcRecurrenceRule.until + if (until != null) { + recurrenceRule.until = formatDateTime(dateTime = until) } - when (rfcRecurrenceRule.freq) { - Freq.WEEKLY, Freq.MONTHLY, Freq.YEARLY -> { - recurrenceRule.daysOfWeek = rfcRecurrenceRule.byDayPart?.mapNotNull { - DayOfWeek.values().find { dayOfWeek -> dayOfWeek.ordinal == it.weekday.ordinal } - }?.toMutableList() + recurrenceRule.sourceRruleString = recurrenceRuleString + + //TODO: Force set to Monday (atm RRULE package only seem to support Monday) + recurrenceRule.wkst = /*rfcRecurrenceRule.weekStart.name*/Weekday.MO.name + recurrenceRule.byday = rfcRecurrenceRule.byDayPart?.mapNotNull { + it.toString() + }?.toMutableList() + recurrenceRule.bymonthday = rfcRecurrenceRule.getByPart(Rrule.Part.BYMONTHDAY) + recurrenceRule.byyearday = rfcRecurrenceRule.getByPart(Rrule.Part.BYYEARDAY) + recurrenceRule.byweekno = rfcRecurrenceRule.getByPart(Rrule.Part.BYWEEKNO) + + // Below adjustment of byMonth ints is necessary as the library somehow gives a wrong int + // See also [buildRecurrenceRuleParams] where 1 is subtracted. + val oldByMonth = rfcRecurrenceRule.getByPart(Rrule.Part.BYMONTH) + if (oldByMonth != null) { + val newByMonth = mutableListOf() + for (month in oldByMonth) { + newByMonth.add(month + 1) } + recurrenceRule.bymonth = newByMonth + } else { + recurrenceRule.bymonth = rfcRecurrenceRule.getByPart(Rrule.Part.BYMONTH) } - val rfcRecurrenceRuleString = rfcRecurrenceRule.toString() - if (rfcRecurrenceRule.freq == Freq.MONTHLY || rfcRecurrenceRule.freq == Freq.YEARLY) { - // Get week number value from BYSETPOS - recurrenceRule.weekOfMonth = convertCalendarPartToNumericValues(rfcRecurrenceRuleString, BYSETPOS_PART) + recurrenceRule.bysetpos = rfcRecurrenceRule.getByPart(Rrule.Part.BYSETPOS) - // If value is not found in BYSETPOS and not repeating by nth day or nth month - // Get the week number value from the BYDAY position - if (recurrenceRule.weekOfMonth == null && rfcRecurrenceRule.byDayPart != null) { - recurrenceRule.weekOfMonth = rfcRecurrenceRule.byDayPart.first().pos - } + return recurrenceRule + } - recurrenceRule.dayOfMonth = convertCalendarPartToNumericValues(rfcRecurrenceRuleString, BYMONTHDAY_PART) + private fun formatDateTime(dateTime: DateTime): String { + assert(dateTime.year in 0..9999) - if (rfcRecurrenceRule.freq == Freq.YEARLY) { - recurrenceRule.monthOfYear = convertCalendarPartToNumericValues(rfcRecurrenceRuleString, BYMONTH_PART) - } + fun twoDigits(n: Int): String { + return if (n < 10) "0$n" else "$n" } - return recurrenceRule - } - - private fun convertCalendarPartToNumericValues(rfcRecurrenceRuleString: String, partName: String): Int? { - val partIndex = rfcRecurrenceRuleString.indexOf(partName) - if (partIndex == -1) { - return null + fun fourDigits(n: Int): String { + val absolute = n.absoluteValue + val sign = if (n < 0) "-" else "" + if (absolute >= 1000) return "$n" + if (absolute >= 100) return "${sign}0$absolute" + if (absolute >= 10) return "${sign}00$absolute" + return "${sign}000$absolute" } - return rfcRecurrenceRuleString.substring(partIndex).split(";").firstOrNull()?.split("=")?.lastOrNull()?.split(",")?.map { - it.toInt() - }?.firstOrNull() + val year = fourDigits(dateTime.year) + val month = twoDigits(dateTime.month.plus(1)) + val day = twoDigits(dateTime.dayOfMonth) + val hour = twoDigits(dateTime.hours) + val minute = twoDigits(dateTime.minutes) + val second = twoDigits(dateTime.seconds) + val utcSuffix = if (dateTime.timeZone == UTC) 'Z' else "" + return "$year-$month-${day}T$hour:$minute:$second$utcSuffix" } private fun parseAttendeeRow(calendar: Calendar, cursor: Cursor?): Attendee? { @@ -826,14 +1049,14 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return null } - val emailAddress = cursor.getString(ATTENDEE_EMAIL_INDEX) + val emailAddress = cursor.getString(Cst.ATTENDEE_EMAIL_INDEX) return Attendee( emailAddress, - cursor.getString(ATTENDEE_NAME_INDEX), - cursor.getInt(ATTENDEE_TYPE_INDEX), - cursor.getInt(ATTENDEE_STATUS_INDEX), - cursor.getInt(ATTENDEE_RELATIONSHIP_INDEX) == CalendarContract.Attendees.RELATIONSHIP_ORGANIZER, + cursor.getString(Cst.ATTENDEE_NAME_INDEX), + cursor.getInt(Cst.ATTENDEE_TYPE_INDEX), + cursor.getInt(Cst.ATTENDEE_STATUS_INDEX), + cursor.getInt(Cst.ATTENDEE_RELATIONSHIP_INDEX) == CalendarContract.Attendees.RELATIONSHIP_ORGANIZER, emailAddress == calendar.ownerAccount ) } @@ -843,7 +1066,7 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return null } - return Reminder(cursor.getInt(REMINDER_MINUTES_INDEX)) + return Reminder(cursor.getInt(Cst.REMINDER_MINUTES_INDEX)) } private fun isCalendarReadOnly(accessLevel: Int): Boolean { @@ -858,10 +1081,20 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun retrieveAttendees(calendar: Calendar, eventId: String, contentResolver: ContentResolver?): MutableList { + private fun retrieveAttendees( + calendar: Calendar, + eventId: String, + contentResolver: ContentResolver? + ): MutableList { val attendees: MutableList = mutableListOf() val attendeesQuery = "(${CalendarContract.Attendees.EVENT_ID} = ${eventId})" - val attendeesCursor = contentResolver?.query(CalendarContract.Attendees.CONTENT_URI, ATTENDEE_PROJECTION, attendeesQuery, null, null) + val attendeesCursor = contentResolver?.query( + CalendarContract.Attendees.CONTENT_URI, + Cst.ATTENDEE_PROJECTION, + attendeesQuery, + null, + null + ) attendeesCursor.use { cursor -> if (cursor?.moveToFirst() == true) { do { @@ -875,10 +1108,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } @SuppressLint("MissingPermission") - private fun retrieveReminders(eventId: String, contentResolver: ContentResolver?): MutableList { + private fun retrieveReminders( + eventId: String, + contentResolver: ContentResolver? + ): MutableList { val reminders: MutableList = mutableListOf() val remindersQuery = "(${CalendarContract.Reminders.EVENT_ID} = ${eventId})" - val remindersCursor = contentResolver?.query(CalendarContract.Reminders.CONTENT_URI, REMINDER_PROJECTION, remindersQuery, null, null) + val remindersCursor = contentResolver?.query( + CalendarContract.Reminders.CONTENT_URI, + Cst.REMINDER_PROJECTION, + remindersQuery, + null, + null + ) remindersCursor.use { cursor -> if (cursor?.moveToFirst() == true) { do { @@ -891,6 +1133,73 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { return reminders } + /** + * load available event colors for the given account name + * unable to find official documentation, so logic is based on https://android.googlesource.com/platform/packages/apps/Calendar.git/+/refs/heads/pie-release/src/com/android/calendar/EventInfoFragment.java + **/ + private fun retrieveColors(accountName: String, colorType: Int): List> { + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = Colors.CONTENT_URI + val colors = mutableListOf() + val displayColorKeyMap = SparseArrayCompat() + + val projection = arrayOf( + Colors.COLOR, + Colors.COLOR_KEY, + ) + + // load only event colors for the given account name + val selection = "${Colors.COLOR_TYPE} = ? AND ${Colors.ACCOUNT_NAME} = ?" + val selectionArgs = arrayOf(colorType.toString(), accountName) + + + val cursor: Cursor? = contentResolver?.query(uri, projection, selection, selectionArgs, null) + cursor?.use { + while (it.moveToNext()) { + val color = it.getInt(it.getColumnIndexOrThrow(Colors.COLOR)) + val colorKey = it.getInt(it.getColumnIndexOrThrow(Colors.COLOR_KEY)) + displayColorKeyMap.put(color, colorKey); + colors.add(color) + } + cursor.close(); + // sort colors by colorValue, since they are loaded unordered + colors.sortWith(HsvColorComparator()) + } + return colors.map { Pair(it, displayColorKeyMap[it]!! ) }.toList() + } + + fun retrieveEventColors(accountName: String): List> { + return retrieveColors(accountName, Colors.TYPE_EVENT) + } + fun retrieveCalendarColors(accountName: String): List> { + return retrieveColors(accountName, Colors.TYPE_CALENDAR) + } + + fun updateCalendarColor(calendarId: Long, newColorKey: Int?, newColor: Int?): Boolean { + val contentResolver: ContentResolver? = _context?.contentResolver + val uri: Uri = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendarId) + val values = ContentValues().apply { + put(CalendarContract.Calendars.CALENDAR_COLOR_KEY, newColorKey) + put(CalendarContract.Calendars.CALENDAR_COLOR, newColor) + } + val rows = contentResolver?.update(uri, values, null, null) + return (rows ?: 0) > 0 + } + + /** + * Compares colors based on their hue values in the HSV color space. + * https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-compose-integration-release/android-34/com/android/colorpicker/HsvColorComparator.java + */ + private class HsvColorComparator : Comparator { + override fun compare(color1: Int, color2: Int): Int { + val hsv1 = FloatArray(3) + val hsv2 = FloatArray(3) + Color.colorToHSV(color1, hsv1) + Color.colorToHSV(color2, hsv2) + return hsv1[0].compareTo(hsv2[0]) + } + } + @Synchronized private fun generateUniqueRequestCodeAndCacheParameters(parameters: CalendarMethodsParametersCacheModel): Int { // TODO we can ran out of Int's at some point so this probably should re-use some of the freed ones @@ -906,13 +1215,19 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { clearCachedParameters(pendingChannelResult) } - private fun finishWithError(errorCode: String, errorMessage: String?, pendingChannelResult: MethodChannel.Result) { + private fun finishWithError( + errorCode: String, + errorMessage: String?, + pendingChannelResult: MethodChannel.Result + ) { pendingChannelResult.error(errorCode, errorMessage, null) clearCachedParameters(pendingChannelResult) } private fun clearCachedParameters(pendingChannelResult: MethodChannel.Result) { - val cachedParameters = _cachedParametersMap.values.filter { it.pendingChannelResult == pendingChannelResult }.toList() + val cachedParameters = + _cachedParametersMap.values.filter { it.pendingChannelResult == pendingChannelResult } + .toList() for (cachedParameter in cachedParameters) { if (_cachedParametersMap.containsKey(cachedParameter.ownCacheKey)) { _cachedParametersMap.remove(cachedParameter.ownCacheKey) @@ -921,71 +1236,92 @@ class CalendarDelegate : PluginRegistry.RequestPermissionsResultListener { } private fun atLeastAPI(api: Int): Boolean { - return api <= android.os.Build.VERSION.SDK_INT + return api <= Build.VERSION.SDK_INT } - private fun buildRecurrenceRuleParams(recurrenceRule: RecurrenceRule): String { - val frequencyParam = when (recurrenceRule.recurrenceFrequency) { - RecurrenceFrequency.DAILY -> Freq.DAILY - RecurrenceFrequency.WEEKLY -> Freq.WEEKLY - RecurrenceFrequency.MONTHLY -> Freq.MONTHLY - RecurrenceFrequency.YEARLY -> Freq.YEARLY - } - val rr = org.dmfs.rfc5545.recur.RecurrenceRule(frequencyParam) + private fun buildRecurrenceRuleParams(recurrenceRule: RecurrenceRule): String? { + val frequencyParam = when (recurrenceRule.freq) { + RruleFreq.DAILY -> RruleFreq.DAILY + RruleFreq.WEEKLY -> RruleFreq.WEEKLY + RruleFreq.MONTHLY -> RruleFreq.MONTHLY + RruleFreq.YEARLY -> RruleFreq.YEARLY + else -> null + } ?: return null + + val rr = Rrule(frequencyParam) if (recurrenceRule.interval != null) { rr.interval = recurrenceRule.interval!! } - if (recurrenceRule.recurrenceFrequency == RecurrenceFrequency.WEEKLY || - recurrenceRule.weekOfMonth != null && (recurrenceRule.recurrenceFrequency == RecurrenceFrequency.MONTHLY || recurrenceRule.recurrenceFrequency == RecurrenceFrequency.YEARLY)) { - rr.byDayPart = buildByDayPart(recurrenceRule) + if (recurrenceRule.count != null) { + rr.count = recurrenceRule.count!! + } else if (recurrenceRule.until != null) { + var untilString: String = recurrenceRule.until!! + if (!untilString.endsWith("Z")) { + untilString += "Z" + } + rr.until = parseDateTime(untilString) } - if (recurrenceRule.totalOccurrences != null) { - rr.count = recurrenceRule.totalOccurrences!! - } else if (recurrenceRule.endDate != null) { - val calendar = java.util.Calendar.getInstance() - calendar.timeInMillis = recurrenceRule.endDate!! - val dateFormat = SimpleDateFormat("yyyyMMdd") - dateFormat.timeZone = calendar.timeZone - rr.until = DateTime(calendar.timeZone, recurrenceRule.endDate!!) + if (recurrenceRule.wkst != null) { + rr.weekStart = Weekday.valueOf(recurrenceRule.wkst!!) } - var rrString = rr.toString() - - if (recurrenceRule.monthOfYear != null && recurrenceRule.recurrenceFrequency == RecurrenceFrequency.YEARLY) { - rrString = rrString.addPartWithValues(BYMONTH_PART, recurrenceRule.monthOfYear) + if (recurrenceRule.byday != null) { + rr.byDayPart = recurrenceRule.byday?.mapNotNull { + WeekdayNum.valueOf(it) + }?.toMutableList() } - if (recurrenceRule.recurrenceFrequency == RecurrenceFrequency.MONTHLY || recurrenceRule.recurrenceFrequency == RecurrenceFrequency.YEARLY) { - if (recurrenceRule.weekOfMonth == null) { - rrString = rrString.addPartWithValues(BYMONTHDAY_PART, recurrenceRule.dayOfMonth) - } + if (recurrenceRule.bymonthday != null) { + rr.setByPart(Rrule.Part.BYMONTHDAY, recurrenceRule.bymonthday!!) } - return rrString - } - - private fun buildByDayPart(recurrenceRule: RecurrenceRule): List? { - if (recurrenceRule.daysOfWeek?.isEmpty() == true) { - return null + if (recurrenceRule.byyearday != null) { + rr.setByPart(Rrule.Part.BYYEARDAY, recurrenceRule.byyearday!!) } - return recurrenceRule.daysOfWeek?.mapNotNull { dayOfWeek -> - Weekday.values().firstOrNull { - it.ordinal == dayOfWeek.ordinal + if (recurrenceRule.byweekno != null) { + rr.setByPart(Rrule.Part.BYWEEKNO, recurrenceRule.byweekno!!) + } + // Below adjustment of byMonth ints is necessary as the library somehow gives a wrong int + // See also [parseRecurrenceRuleString] where +1 is added. + if (recurrenceRule.bymonth != null) { + val byMonth = recurrenceRule.bymonth!! + val newMonth = mutableListOf() + byMonth.forEach { + newMonth.add(it - 1) } - }?.map { - org.dmfs.rfc5545.recur.RecurrenceRule.WeekdayNum(recurrenceRule.weekOfMonth ?: 0, it) + rr.setByPart(Rrule.Part.BYMONTH, newMonth) } - } - private fun String.addPartWithValues(partName: String, values: Int?): String { - if (values != null) { - return this + PART_TEMPLATE.format(partName) + values + if (recurrenceRule.bysetpos != null) { + rr.setByPart(Rrule.Part.BYSETPOS, recurrenceRule.bysetpos!!) } + return rr.toString() + } - return this + private fun parseDateTime(string: String): DateTime { + val year = Regex("""(?\d{4})""").pattern + val month = Regex("""(?\d{2})""").pattern + val day = Regex("""(?\d{2})""").pattern + val hour = Regex("""(?\d{2})""").pattern + val minute = Regex("""(?\d{2})""").pattern + val second = Regex("""(?\d{2})""").pattern + + val regEx = Regex("^$year-$month-${day}T$hour:$minute:${second}Z?\$") + + val match = regEx.matchEntire(string) + + return DateTime( + UTC, + match?.groups?.get(1)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(2)?.value?.toIntOrNull()?.minus(1) ?: 0, + match?.groups?.get(3)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(4)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(5)?.value?.toIntOrNull() ?: 0, + match?.groups?.get(6)?.value?.toIntOrNull() ?: 0 + ) } private fun parseAvailability(availability: Int): Availability? = when (availability) { diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt deleted file mode 100644 index b2bbce05..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DayOfWeekSerializer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.builttoroam.devicecalendar - -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.google.gson.* -import java.lang.reflect.Type - -class DayOfWeekSerializer: JsonSerializer { - override fun serialize(src: DayOfWeek?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { - return JsonPrimitive(src.ordinal) - } - return JsonObject() - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt index d2aef019..a5d7df80 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/DeviceCalendarPlugin.kt @@ -4,8 +4,6 @@ import android.app.Activity import android.content.Context import androidx.annotation.NonNull import com.builttoroam.devicecalendar.common.Constants -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.builttoroam.devicecalendar.common.RecurrenceFrequency import com.builttoroam.devicecalendar.models.* import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware @@ -13,11 +11,69 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result +import org.dmfs.rfc5545.recur.Freq const val CHANNEL_NAME = "plugins.builttoroam.com/device_calendar" -class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { +// Methods +private const val REQUEST_PERMISSIONS_METHOD = "requestPermissions" +private const val HAS_PERMISSIONS_METHOD = "hasPermissions" +private const val RETRIEVE_CALENDARS_METHOD = "retrieveCalendars" +private const val RETRIEVE_EVENTS_METHOD = "retrieveEvents" +private const val DELETE_EVENT_METHOD = "deleteEvent" +private const val DELETE_EVENT_INSTANCE_METHOD = "deleteEventInstance" +private const val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent" +private const val CREATE_CALENDAR_METHOD = "createCalendar" +private const val DELETE_CALENDAR_METHOD = "deleteCalendar" +private const val RETRIEVE_EVENT_COLORS_METHOD = "retrieveEventColors" +private const val RETRIEVE_CALENDAR_COLORS_METHOD = "retrieveCalendarColors" +private const val UPDATE_CALENDAR_COLOR = "updateCalendarColor" + +// Method arguments +private const val CALENDAR_ID_ARGUMENT = "calendarId" +private const val CALENDAR_NAME_ARGUMENT = "calendarName" +private const val CALENDAR_ACCOUNT_NAME_ARGUMENT = "accountName" +private const val START_DATE_ARGUMENT = "startDate" +private const val END_DATE_ARGUMENT = "endDate" +private const val EVENT_IDS_ARGUMENT = "eventIds" +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" +private const val EVENT_COLOR_KEY_ARGUMENT = "eventColorKey" +private const val CALENDAR_COLOR_KEY_ARGUMENT = "calendarColorKey" + +class DeviceCalendarPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { /// The MethodChannel that will the communication between Flutter and native Android /// @@ -27,54 +83,6 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { private var context: Context? = null private var activity: Activity? = null - // Methods - private val REQUEST_PERMISSIONS_METHOD = "requestPermissions" - private val HAS_PERMISSIONS_METHOD = "hasPermissions" - private val RETRIEVE_CALENDARS_METHOD = "retrieveCalendars" - private val RETRIEVE_EVENTS_METHOD = "retrieveEvents" - private val DELETE_EVENT_METHOD = "deleteEvent" - private val DELETE_EVENT_INSTANCE_METHOD = "deleteEventInstance" - private val CREATE_OR_UPDATE_EVENT_METHOD = "createOrUpdateEvent" - private val CREATE_CALENDAR_METHOD = "createCalendar" - private val DELETE_CALENDAR_METHOD = "deleteCalendar" - - // Method arguments - private val CALENDAR_ID_ARGUMENT = "calendarId" - private val CALENDAR_NAME_ARGUMENT = "calendarName" - private val START_DATE_ARGUMENT = "startDate" - private val END_DATE_ARGUMENT = "endDate" - private val EVENT_IDS_ARGUMENT = "eventIds" - private val EVENT_ID_ARGUMENT = "eventId" - private val EVENT_TITLE_ARGUMENT = "eventTitle" - private val EVENT_LOCATION_ARGUMENT = "eventLocation" - private val EVENT_URL_ARGUMENT = "eventURL" - private val EVENT_DESCRIPTION_ARGUMENT = "eventDescription" - private val EVENT_ALL_DAY_ARGUMENT = "eventAllDay" - private val EVENT_START_DATE_ARGUMENT = "eventStartDate" - private val EVENT_END_DATE_ARGUMENT = "eventEndDate" - private val EVENT_START_TIMEZONE_ARGUMENT = "eventStartTimeZone" - private val EVENT_END_TIMEZONE_ARGUMENT = "eventEndTimeZone" - private val RECURRENCE_RULE_ARGUMENT = "recurrenceRule" - private val RECURRENCE_FREQUENCY_ARGUMENT = "recurrenceFrequency" - private val TOTAL_OCCURRENCES_ARGUMENT = "totalOccurrences" - private val INTERVAL_ARGUMENT = "interval" - private val DAYS_OF_WEEK_ARGUMENT = "daysOfWeek" - private val DAY_OF_MONTH_ARGUMENT = "dayOfMonth" - private val MONTH_OF_YEAR_ARGUMENT = "monthOfYear" - private val WEEK_OF_MONTH_ARGUMENT = "weekOfMonth" - private val ATTENDEES_ARGUMENT = "attendees" - private val EMAIL_ADDRESS_ARGUMENT = "emailAddress" - private val NAME_ARGUMENT = "name" - private val ROLE_ARGUMENT = "role" - private val REMINDERS_ARGUMENT = "reminders" - private val MINUTES_ARGUMENT = "minutes" - private val FOLLOWING_INSTANCES = "followingInstances" - private val CALENDAR_COLOR_ARGUMENT = "calendarColor" - private val LOCAL_ACCOUNT_NAME_ARGUMENT = "localAccountName" - private val EVENT_AVAILABILITY_ARGUMENT = "availability" - private val ATTENDANCE_STATUS_ARGUMENT = "attendanceStatus" - private val EVENT_STATUS_ARGUMENT = "eventStatus" - private lateinit var _calendarDelegate: CalendarDelegate override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { @@ -108,7 +116,7 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { activity = null } - override fun onMethodCall(call: MethodCall, result: Result) { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { REQUEST_PERMISSIONS_METHOD -> { _calendarDelegate.requestPermissions(result) @@ -124,13 +132,11 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { val startDate = call.argument(START_DATE_ARGUMENT) val endDate = call.argument(END_DATE_ARGUMENT) val eventIds = call.argument>(EVENT_IDS_ARGUMENT) ?: listOf() - _calendarDelegate.retrieveEvents(calendarId!!, startDate, endDate, eventIds, result) } CREATE_OR_UPDATE_EVENT_METHOD -> { val calendarId = call.argument(CALENDAR_ID_ARGUMENT) val event = parseEventArgs(call, calendarId) - _calendarDelegate.createOrUpdateEvent(calendarId!!, event, result) } DELETE_EVENT_METHOD -> { @@ -146,18 +152,59 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { val endDate = call.argument(EVENT_END_DATE_ARGUMENT) val followingInstances = call.argument(FOLLOWING_INSTANCES) - _calendarDelegate.deleteEvent(calendarId!!, eventId!!, result, startDate, endDate, followingInstances) + _calendarDelegate.deleteEvent( + calendarId!!, + eventId!!, + result, + startDate, + endDate, + followingInstances + ) } CREATE_CALENDAR_METHOD -> { val calendarName = call.argument(CALENDAR_NAME_ARGUMENT) val calendarColor = call.argument(CALENDAR_COLOR_ARGUMENT) val localAccountName = call.argument(LOCAL_ACCOUNT_NAME_ARGUMENT) - _calendarDelegate.createCalendar(calendarName!!, calendarColor, localAccountName!!, result) + _calendarDelegate.createCalendar( + calendarName!!, + calendarColor, + localAccountName!!, + result + ) } DELETE_CALENDAR_METHOD -> { val calendarId = call.argument(CALENDAR_ID_ARGUMENT) - _calendarDelegate.deleteCalendar(calendarId!!,result) + _calendarDelegate.deleteCalendar(calendarId!!, result) + } + RETRIEVE_EVENT_COLORS_METHOD -> { + val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) + if (accountName == null) { + result.success(intArrayOf()) + return; + } + val colors = _calendarDelegate.retrieveEventColors(accountName!!, ) + result.success(colors.map { listOf(it.first, it.second) }) + } + RETRIEVE_CALENDAR_COLORS_METHOD -> { + val accountName = call.argument(CALENDAR_ACCOUNT_NAME_ARGUMENT) + if (accountName == null) { + result.success(intArrayOf()) + return; + } + val colors = _calendarDelegate.retrieveCalendarColors(accountName) + result.success(colors.map { listOf(it.first, it.second) }) + } + UPDATE_CALENDAR_COLOR -> { + val calendarId = call.argument(CALENDAR_ID_ARGUMENT)?.toLong() + if (calendarId == null) { + result.success(false) + return + } + val newColorKey = (call.argument(CALENDAR_COLOR_KEY_ARGUMENT))?.toInt() + val newColor = (call.argument(CALENDAR_COLOR_ARGUMENT))?.toInt() + val success = _calendarDelegate.updateCalendarColor(calendarId, newColorKey, newColor) + result.success(success) } else -> { result.notImplemented() @@ -180,68 +227,93 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { event.eventURL = call.argument(EVENT_URL_ARGUMENT) event.availability = parseAvailability(call.argument(EVENT_AVAILABILITY_ARGUMENT)) event.eventStatus = parseEventStatus(call.argument(EVENT_STATUS_ARGUMENT)) + event.eventColorKey = call.argument(EVENT_COLOR_KEY_ARGUMENT) - if (call.hasArgument(RECURRENCE_RULE_ARGUMENT) && call.argument>(RECURRENCE_RULE_ARGUMENT) != null) { + if (call.hasArgument(RECURRENCE_RULE_ARGUMENT) && call.argument>( + RECURRENCE_RULE_ARGUMENT + ) != null + ) { val recurrenceRule = parseRecurrenceRuleArgs(call) event.recurrenceRule = recurrenceRule } - if (call.hasArgument(ATTENDEES_ARGUMENT) && call.argument>>(ATTENDEES_ARGUMENT) != null) { + if (call.hasArgument(ATTENDEES_ARGUMENT) && call.argument>>( + ATTENDEES_ARGUMENT + ) != null + ) { event.attendees = mutableListOf() val attendeesArgs = call.argument>>(ATTENDEES_ARGUMENT)!! for (attendeeArgs in attendeesArgs) { - event.attendees.add(Attendee( + event.attendees.add( + Attendee( attendeeArgs[EMAIL_ADDRESS_ARGUMENT] as String, attendeeArgs[NAME_ARGUMENT] as String?, attendeeArgs[ROLE_ARGUMENT] as Int, attendeeArgs[ATTENDANCE_STATUS_ARGUMENT] as Int?, - null, null)) + null, null + ) + ) } } - if (call.hasArgument(REMINDERS_ARGUMENT) && call.argument>>(REMINDERS_ARGUMENT) != null) { + if (call.hasArgument(REMINDERS_ARGUMENT) && call.argument>>( + REMINDERS_ARGUMENT + ) != null + ) { event.reminders = mutableListOf() val remindersArgs = call.argument>>(REMINDERS_ARGUMENT)!! for (reminderArgs in remindersArgs) { event.reminders.add(Reminder(reminderArgs[MINUTES_ARGUMENT] as Int)) } } - return event } private fun parseRecurrenceRuleArgs(call: MethodCall): RecurrenceRule { val recurrenceRuleArgs = call.argument>(RECURRENCE_RULE_ARGUMENT)!! - val recurrenceFrequencyIndex = recurrenceRuleArgs[RECURRENCE_FREQUENCY_ARGUMENT] as Int - val recurrenceRule = RecurrenceRule(RecurrenceFrequency.values()[recurrenceFrequencyIndex]) - if (recurrenceRuleArgs.containsKey(TOTAL_OCCURRENCES_ARGUMENT)) { - recurrenceRule.totalOccurrences = recurrenceRuleArgs[TOTAL_OCCURRENCES_ARGUMENT] as Int + val recurrenceFrequencyString = recurrenceRuleArgs[FREQUENCY_ARGUMENT] as String + val recurrenceFrequency = Freq.valueOf(recurrenceFrequencyString) + val recurrenceRule = RecurrenceRule(recurrenceFrequency) + + if (recurrenceRuleArgs.containsKey(COUNT_ARGUMENT)) { + recurrenceRule.count = recurrenceRuleArgs[COUNT_ARGUMENT] as Int? } if (recurrenceRuleArgs.containsKey(INTERVAL_ARGUMENT)) { recurrenceRule.interval = recurrenceRuleArgs[INTERVAL_ARGUMENT] as Int } - if (recurrenceRuleArgs.containsKey(END_DATE_ARGUMENT)) { - recurrenceRule.endDate = recurrenceRuleArgs[END_DATE_ARGUMENT] as Long + if (recurrenceRuleArgs.containsKey(UNTIL_ARGUMENT)) { + recurrenceRule.until = recurrenceRuleArgs[UNTIL_ARGUMENT] as String? + } + + if (recurrenceRuleArgs.containsKey(BY_WEEK_DAYS_ARGUMENT)) { + recurrenceRule.byday = + recurrenceRuleArgs[BY_WEEK_DAYS_ARGUMENT].toListOf()?.toMutableList() } - if (recurrenceRuleArgs.containsKey(DAYS_OF_WEEK_ARGUMENT)) { - recurrenceRule.daysOfWeek = recurrenceRuleArgs[DAYS_OF_WEEK_ARGUMENT].toListOf()?.map { DayOfWeek.values()[it] }?.toMutableList() + if (recurrenceRuleArgs.containsKey(BY_MONTH_DAYS_ARGUMENT)) { + recurrenceRule.bymonthday = + recurrenceRuleArgs[BY_MONTH_DAYS_ARGUMENT] as MutableList? } - if (recurrenceRuleArgs.containsKey(DAY_OF_MONTH_ARGUMENT)) { - recurrenceRule.dayOfMonth = recurrenceRuleArgs[DAY_OF_MONTH_ARGUMENT] as Int + if (recurrenceRuleArgs.containsKey(BY_YEAR_DAYS_ARGUMENT)) { + recurrenceRule.byyearday = + recurrenceRuleArgs[BY_YEAR_DAYS_ARGUMENT] as MutableList? } - if (recurrenceRuleArgs.containsKey(MONTH_OF_YEAR_ARGUMENT)) { - recurrenceRule.monthOfYear = recurrenceRuleArgs[MONTH_OF_YEAR_ARGUMENT] as Int + if (recurrenceRuleArgs.containsKey(BY_WEEKS_ARGUMENT)) { + recurrenceRule.byweekno = recurrenceRuleArgs[BY_WEEKS_ARGUMENT] as MutableList? } - if (recurrenceRuleArgs.containsKey(WEEK_OF_MONTH_ARGUMENT)) { - recurrenceRule.weekOfMonth = recurrenceRuleArgs[WEEK_OF_MONTH_ARGUMENT] as Int + if (recurrenceRuleArgs.containsKey(BY_MONTH_ARGUMENT)) { + recurrenceRule.bymonth = recurrenceRuleArgs[BY_MONTH_ARGUMENT] as MutableList? } + if (recurrenceRuleArgs.containsKey(BY_SET_POSITION_ARGUMENT)) { + recurrenceRule.bysetpos = + recurrenceRuleArgs[BY_SET_POSITION_ARGUMENT] as MutableList? + } return recurrenceRule } @@ -249,10 +321,6 @@ class DeviceCalendarPlugin() : FlutterPlugin, MethodCallHandler, ActivityAware { return (this as List<*>?)?.filterIsInstance()?.toList() } - private inline fun Any?.toMutableListOf(): MutableList? { - return this?.toListOf()?.toMutableList() - } - private fun parseAvailability(value: String?): Availability? = if (value == null || value == Constants.AVAILABILITY_UNAVAILABLE) { null diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt deleted file mode 100644 index c4374353..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/RecurrenceFrequencySerializer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.builttoroam.devicecalendar - -import com.builttoroam.devicecalendar.common.RecurrenceFrequency -import com.google.gson.* -import java.lang.reflect.Type - -class RecurrenceFrequencySerializer: JsonSerializer { - override fun serialize(src: RecurrenceFrequency?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement { - if(src != null) { - return JsonPrimitive(src.ordinal) - } - return JsonObject() - } - -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt index 95b083a7..f02eebd2 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/Constants.kt @@ -15,26 +15,26 @@ class Constants { // API 17 or higher val CALENDAR_PROJECTION: Array = arrayOf( - CalendarContract.Calendars._ID, // 0 - CalendarContract.Calendars.ACCOUNT_NAME, // 1 - CalendarContract.Calendars.ACCOUNT_TYPE, // 2 - CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 - CalendarContract.Calendars.OWNER_ACCOUNT, // 4 - CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 - CalendarContract.Calendars.CALENDAR_COLOR, // 6 - CalendarContract.Calendars.IS_PRIMARY // 7 + CalendarContract.Calendars._ID, // 0 + CalendarContract.Calendars.ACCOUNT_NAME, // 1 + CalendarContract.Calendars.ACCOUNT_TYPE, // 2 + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 + CalendarContract.Calendars.OWNER_ACCOUNT, // 4 + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 + CalendarContract.Calendars.CALENDAR_COLOR, // 6 + CalendarContract.Calendars.IS_PRIMARY // 7 ) // API 16 or lower val CALENDAR_PROJECTION_OLDER_API: Array = arrayOf( - CalendarContract.Calendars._ID, // 0 - CalendarContract.Calendars.ACCOUNT_NAME, // 1 - CalendarContract.Calendars.ACCOUNT_TYPE, // 2 - CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 - CalendarContract.Calendars.OWNER_ACCOUNT, // 4 - CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 - CalendarContract.Calendars.CALENDAR_COLOR // 6 + CalendarContract.Calendars._ID, // 0 + CalendarContract.Calendars.ACCOUNT_NAME, // 1 + CalendarContract.Calendars.ACCOUNT_TYPE, // 2 + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, // 3 + CalendarContract.Calendars.OWNER_ACCOUNT, // 4 + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, // 5 + CalendarContract.Calendars.CALENDAR_COLOR // 6 ) const val EVENT_PROJECTION_ID_INDEX: Int = 0 @@ -50,6 +50,8 @@ 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 + const val EVENT_PROJECTION_EVENT_COLOR_KEY_INDEX: Int = 16 val EVENT_PROJECTION: Array = arrayOf( CalendarContract.Instances.EVENT_ID, @@ -66,7 +68,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, + CalendarContract.Events.EVENT_COLOR_KEY ) const val EVENT_INSTANCE_DELETION_ID_INDEX: Int = 0 @@ -76,11 +80,11 @@ class Constants { const val EVENT_INSTANCE_DELETION_END_INDEX: Int = 4 val EVENT_INSTANCE_DELETION: Array = arrayOf( - CalendarContract.Instances.EVENT_ID, - CalendarContract.Events.RRULE, - CalendarContract.Events.LAST_DATE, - CalendarContract.Instances.BEGIN, - CalendarContract.Instances.END + CalendarContract.Instances.EVENT_ID, + CalendarContract.Events.RRULE, + CalendarContract.Events.LAST_DATE, + CalendarContract.Instances.BEGIN, + CalendarContract.Instances.END ) const val ATTENDEE_ID_INDEX: Int = 0 @@ -92,19 +96,19 @@ class Constants { const val ATTENDEE_STATUS_INDEX: Int = 6 val ATTENDEE_PROJECTION: Array = arrayOf( - CalendarContract.Attendees._ID, - CalendarContract.Attendees.EVENT_ID, - CalendarContract.Attendees.ATTENDEE_NAME, - CalendarContract.Attendees.ATTENDEE_EMAIL, - CalendarContract.Attendees.ATTENDEE_TYPE, - CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, - CalendarContract.Attendees.ATTENDEE_STATUS + CalendarContract.Attendees._ID, + CalendarContract.Attendees.EVENT_ID, + CalendarContract.Attendees.ATTENDEE_NAME, + CalendarContract.Attendees.ATTENDEE_EMAIL, + CalendarContract.Attendees.ATTENDEE_TYPE, + CalendarContract.Attendees.ATTENDEE_RELATIONSHIP, + CalendarContract.Attendees.ATTENDEE_STATUS ) const val REMINDER_MINUTES_INDEX = 1 val REMINDER_PROJECTION: Array = arrayOf( - CalendarContract.Reminders.EVENT_ID, - CalendarContract.Reminders.MINUTES + CalendarContract.Reminders.EVENT_ID, + CalendarContract.Reminders.MINUTES ) const val AVAILABILITY_UNAVAILABLE = "UNAVAILABLE" diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt deleted file mode 100644 index f6c04838..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/DayOfWeek.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.builttoroam.devicecalendar.common - -enum class DayOfWeek { - SUNDAY, - MONDAY, - TUESDAY, - WEDNESDAY, - THURSDAY, - FRIDAY, - SATURDAY -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt index e8a8c82c..e8486baa 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/ErrorMessages.kt @@ -2,10 +2,15 @@ package com.builttoroam.devicecalendar.common class ErrorMessages { companion object { - const val CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE: String = "Calendar ID is not a number" - const val EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE: String = "Event ID cannot be null on deletion" - const val RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE: String = "Provided arguments (i.e. start, end and event ids) are null or empty" - const val CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE: String = "Some of the event arguments are not valid" - const val NOT_AUTHORIZED_MESSAGE: String = "The user has not allowed this application to modify their calendar(s)" + const val CALENDAR_ID_INVALID_ARGUMENT_NOT_A_NUMBER_MESSAGE: String = + "Calendar ID is not a number" + const val EVENT_ID_CANNOT_BE_NULL_ON_DELETION_MESSAGE: String = + "Event ID cannot be null on deletion" + const val RETRIEVE_EVENTS_ARGUMENTS_NOT_VALID_MESSAGE: String = + "Provided arguments (i.e. start, end and event ids) are null or empty" + const val CREATE_EVENT_ARGUMENTS_NOT_VALID_MESSAGE: String = + "Some of the event arguments are not valid" + const val NOT_AUTHORIZED_MESSAGE: String = + "The user has not allowed this application to modify their calendar(s)" } } diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt deleted file mode 100644 index 5e9551c9..00000000 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/common/RecurrenceFrequency.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.builttoroam.devicecalendar.common - -enum class RecurrenceFrequency { - DAILY, WEEKLY, MONTHLY, YEARLY -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt index 1336d362..825ca964 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Attendee.kt @@ -1,3 +1,10 @@ package com.builttoroam.devicecalendar.models -class Attendee(val emailAddress: String, val name: String?, val role: Int, val attendanceStatus: Int?, val isOrganizer: Boolean?, val isCurrentUser: Boolean?) \ No newline at end of file +class Attendee( + val emailAddress: String, + val name: String?, + val role: Int, + val attendanceStatus: Int?, + val isOrganizer: Boolean?, + val isCurrentUser: Boolean? +) \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt index 955c4768..22bb4c4b 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/CalendarMethodsParametersCacheModel.kt @@ -2,13 +2,15 @@ package com.builttoroam.devicecalendar.models import io.flutter.plugin.common.MethodChannel -class CalendarMethodsParametersCacheModel(val pendingChannelResult: MethodChannel.Result, - val calendarDelegateMethodCode: Int, - var calendarId: String = "", - var calendarEventsStartDate: Long? = null, - var calendarEventsEndDate: Long? = null, - var calendarEventsIds: List = listOf(), - var eventId: String = "", - var event: Event? = null) { +class CalendarMethodsParametersCacheModel( + val pendingChannelResult: MethodChannel.Result, + val calendarDelegateMethodCode: Int, + var calendarId: String = "", + var calendarEventsStartDate: Long? = null, + var calendarEventsEndDate: Long? = null, + var calendarEventsIds: List = listOf(), + var eventId: String = "", + var event: Event? = null +) { var ownCacheKey: Int? = null } \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt index 67152245..dc988fbb 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/Event.kt @@ -1,6 +1,5 @@ package com.builttoroam.devicecalendar.models - class Event { var eventTitle: String? = null var eventId: String? = null @@ -19,4 +18,6 @@ class Event { var reminders: MutableList = mutableListOf() var availability: Availability? = null var eventStatus: EventStatus? = null + var eventColor: Int? = null + var eventColorKey: Int? = null } \ No newline at end of file diff --git a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt index 659afe52..1da83111 100644 --- a/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt +++ b/android/src/main/kotlin/com/builttoroam/devicecalendar/models/RecurrenceRule.kt @@ -1,15 +1,17 @@ package com.builttoroam.devicecalendar.models -import com.builttoroam.devicecalendar.common.DayOfWeek -import com.builttoroam.devicecalendar.common.RecurrenceFrequency +import org.dmfs.rfc5545.recur.Freq - -class RecurrenceRule(val recurrenceFrequency : RecurrenceFrequency) { - var totalOccurrences: Int? = null +class RecurrenceRule(val freq: Freq) { + var count: Int? = null var interval: Int? = null - var endDate: Long? = null - var daysOfWeek: MutableList? = null - var dayOfMonth: Int? = null - var monthOfYear: Int? = null - var weekOfMonth: Int? = null + var until: String? = null + var sourceRruleString: String? = null + var wkst: String? = null + var byday: MutableList? = null + var bymonthday: MutableList? = null + var byyearday: MutableList? = null + var byweekno: MutableList? = null + var bymonth: MutableList? = null + var bysetpos: MutableList? = null } diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index dd924715..7f0cf3be 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -16,7 +16,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 32 + compileSdkVersion 34 ndkVersion '22.1.7171670' sourceSets { @@ -30,7 +30,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.builttoroam.devicecalendarexample" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 31 versionCode 1 versionName "1.0" diff --git a/example/android/build.gradle b/example/android/build.gradle index d135914c..8bbe685b 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.8.22' repositories { google() mavenCentral() @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir -} \ No newline at end of file +} diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f..9b41e7d8 100755 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/example/ios/Podfile b/example/ios/Podfile index 8ab33cfb..997d1cb3 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index faea259c..cb8f159b 100755 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -27,8 +27,8 @@ SPEC CHECKSUMS: device_calendar: 9cb33f88a02e19652ec7b8b122ca778f751b1f7b Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_timezone: 5f05b2de06c9776b4cc70e1839f03de178394d22 - integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 + integration_test: 13825b8a9334a850581300559b8839134b124670 -PODFILE CHECKSUM: d3740c426905916d1f2ada0ddfce28cc99f7b7af +PODFILE CHECKSUM: 10625bdc9b9ef8574174815aabd5b048e6e29bff COCOAPODS: 1.11.3 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 345447bf..160e1d14 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -161,12 +161,12 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1240; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 5X4222W8C2; + DevelopmentTeam = PG8Q9ZR89L; LastSwiftMigration = 1130; ProvisioningStyle = Automatic; }; @@ -229,10 +229,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -243,6 +245,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -355,7 +358,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -405,7 +408,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -423,7 +426,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 5X4222W8C2; + DEVELOPMENT_TEAM = PG8Q9ZR89L; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -455,7 +458,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 5X4222W8C2; + DEVELOPMENT_TEAM = PG8Q9ZR89L; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 0254012e..14d255fd 100755 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/presentation/color_picker_dialog.dart b/example/lib/presentation/color_picker_dialog.dart new file mode 100644 index 00000000..04d7fc7d --- /dev/null +++ b/example/lib/presentation/color_picker_dialog.dart @@ -0,0 +1,28 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class ColorPickerDialog { + static Future selectColorDialog(List colors, BuildContext context) async { + return await showDialog( + context: context, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('Select color'), + children: [ + ...colors.map((color) => + SimpleDialogOption( + onPressed: () { Navigator.pop(context, color); }, + child: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color), + ), + ) + )] + ); + } + ); + } +} \ No newline at end of file diff --git a/example/lib/presentation/date_time_picker.dart b/example/lib/presentation/date_time_picker.dart index dc11e8d9..449b82e8 100644 --- a/example/lib/presentation/date_time_picker.dart +++ b/example/lib/presentation/date_time_picker.dart @@ -45,7 +45,7 @@ class DateTimePicker extends StatelessWidget { @override Widget build(BuildContext context) { - final valueStyle = Theme.of(context).textTheme.headline6; + final valueStyle = Theme.of(context).textTheme.titleLarge; return Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ diff --git a/example/lib/presentation/event_item.dart b/example/lib/presentation/event_item.dart index ca401086..b18d299d 100644 --- a/example/lib/presentation/event_item.dart +++ b/example/lib/presentation/event_item.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:intl/intl.dart'; import 'recurring_event_dialog.dart'; @@ -39,7 +39,7 @@ class _EventItemState extends State { @override void initState() { super.initState(); - setCurentLocation(); + WidgetsBinding.instance.addPostFrameCallback((_) => setCurentLocation()); } @override @@ -313,9 +313,9 @@ class _EventItemState extends State { void setCurentLocation() async { String? timezone; try { - timezone = await FlutterNativeTimezone.getLocalTimezone(); + timezone = await FlutterTimezone.getLocalTimezone(); } catch (e) { - print('Could not get the local timezone'); + debugPrint('Could not get the local timezone'); } timezone ??= 'Etc/UTC'; _currentLocation = timeZoneDatabase.locations[timezone]; diff --git a/example/lib/presentation/pages/calendar_add.dart b/example/lib/presentation/pages/calendar_add.dart index fc4abbce..7d6d8820 100644 --- a/example/lib/presentation/pages/calendar_add.dart +++ b/example/lib/presentation/pages/calendar_add.dart @@ -119,7 +119,6 @@ class _CalendarAddPageState extends State { void showInSnackBar(String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); - // _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(value))); } } diff --git a/example/lib/presentation/pages/calendar_event.dart b/example/lib/presentation/pages/calendar_event.dart index ee9040e9..83ad1a23 100644 --- a/example/lib/presentation/pages/calendar_event.dart +++ b/example/lib/presentation/pages/calendar_event.dart @@ -4,9 +4,10 @@ import 'package:collection/collection.dart'; import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_native_timezone/flutter_native_timezone.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:intl/intl.dart'; +import '../color_picker_dialog.dart'; import '../date_time_picker.dart'; import '../recurring_event_dialog.dart'; import 'event_attendee.dart'; @@ -15,15 +16,18 @@ import 'event_reminders.dart'; enum RecurrenceRuleEndType { Indefinite, MaxOccurrences, SpecifiedEndDate } class CalendarEventPage extends StatefulWidget { - late final Calendar _calendar; + final Calendar _calendar; final Event? _event; final RecurringEventDialog? _recurringEventDialog; + final List? _eventColors; - CalendarEventPage(this._calendar, [this._event, this._recurringEventDialog]); + const CalendarEventPage(this._calendar, + [this._event, this._recurringEventDialog, this._eventColors, Key? key]) + : super(key: key); @override _CalendarEventPageState createState() { - return _CalendarEventPageState(_calendar, _event, _recurringEventDialog); + return _CalendarEventPageState(_calendar, _event, _recurringEventDialog, _eventColors); } } @@ -33,9 +37,13 @@ class _CalendarEventPageState extends State { final Calendar _calendar; Event? _event; - late DeviceCalendarPlugin _deviceCalendarPlugin; + late final DeviceCalendarPlugin _deviceCalendarPlugin; final RecurringEventDialog? _recurringEventDialog; + DateTime get nowDate => DateTime.now(); + + // TimeOfDay get nowTime => TimeOfDay(hour: nowDate.hour, minute: nowDate.hour); + TZDateTime? _startDate; TimeOfDay? _startTime; @@ -43,128 +51,113 @@ class _CalendarEventPageState extends State { TimeOfDay? _endTime; AutovalidateMode _autovalidate = AutovalidateMode.disabled; - DayOfWeekGroup? _dayOfWeekGroup = DayOfWeekGroup.None; + DayOfWeekGroup _dayOfWeekGroup = DayOfWeekGroup.None; + + RecurrenceRuleEndType _recurrenceRuleEndType = + RecurrenceRuleEndType.Indefinite; + RecurrenceRule? _rrule; - bool _isRecurringEvent = false; - bool _isByDayOfMonth = false; - RecurrenceRuleEndType? _recurrenceRuleEndType; - int? _totalOccurrences; - int? _interval; - late DateTime _recurrenceEndDate; - RecurrenceFrequency? _recurrenceFrequency = RecurrenceFrequency.Daily; - List _daysOfWeek = []; - int? _dayOfMonth; final List _validDaysOfMonth = []; - MonthOfYear? _monthOfYear; - WeekNumber? _weekOfMonth; - DayOfWeek? _selectedDayOfWeek = DayOfWeek.Monday; + Availability _availability = Availability.Busy; EventStatus? _eventStatus; - - List _attendees = []; - List _reminders = []; + List? _attendees; + List? _reminders; + List? _eventColors; String _timezone = 'Etc/UTC'; _CalendarEventPageState( - this._calendar, this._event, this._recurringEventDialog) { + this._calendar, this._event, this._recurringEventDialog, this._eventColors) { getCurentLocation(); } void getCurentLocation() async { try { - _timezone = await FlutterNativeTimezone.getLocalTimezone(); + _timezone = await FlutterTimezone.getLocalTimezone(); } catch (e) { - print('Could not get the local timezone'); + debugPrint('Could not get the local timezone'); } _deviceCalendarPlugin = DeviceCalendarPlugin(); - _attendees = []; - _reminders = []; - _recurrenceRuleEndType = RecurrenceRuleEndType.Indefinite; - - if (_event == null) { - print('calendar_event _timezone ------------------------- $_timezone'); - var currentLocation = timeZoneDatabase.locations[_timezone]; + final event = _event; + if (event == null) { + debugPrint( + 'calendar_event _timezone ------------------------- $_timezone'); + final currentLocation = timeZoneDatabase.locations[_timezone]; if (currentLocation != null) { - _startDate = TZDateTime.now(currentLocation); - _endDate = - TZDateTime.now(currentLocation).add(const Duration(hours: 1)); + final now = TZDateTime.now(currentLocation); + _startDate = now; + _startTime = TimeOfDay(hour: now.hour, minute: now.minute); + final oneHourLater = now.add(const Duration(hours: 1)); + _endDate = oneHourLater; + _endTime = + TimeOfDay(hour: oneHourLater.hour, minute: oneHourLater.minute); } else { var fallbackLocation = timeZoneDatabase.locations['Etc/UTC']; - _startDate = TZDateTime.now(fallbackLocation!); - _endDate = - TZDateTime.now(fallbackLocation).add(const Duration(hours: 1)); + final now = TZDateTime.now(fallbackLocation!); + _startDate = now; + _startTime = TimeOfDay(hour: now.hour, minute: now.minute); + final oneHourLater = now.add(const Duration(hours: 1)); + _endDate = oneHourLater; + _endTime = + TimeOfDay(hour: oneHourLater.hour, minute: oneHourLater.minute); } - _event = Event(_calendar.id, start: _startDate, end: _endDate); + _event = Event(_calendar.id, + start: _startDate, end: _endDate, availability: _availability); - print('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); + debugPrint('DeviceCalendarPlugin calendar id is: ${_calendar.id}'); - _recurrenceEndDate = _endDate as DateTime; - _dayOfMonth = 1; - _monthOfYear = MonthOfYear.January; - _weekOfMonth = WeekNumber.First; - _availability = Availability.Busy; _eventStatus = EventStatus.None; } else { - _startDate = _event!.start!; - _endDate = _event!.end!; - _isRecurringEvent = _event!.recurrenceRule != null; - - if (_event!.attendees!.isNotEmpty) { - _attendees.addAll(_event!.attendees! as Iterable); + final start = event.start; + final end = event.end; + if (start != null && end != null) { + _startDate = start; + _startTime = TimeOfDay(hour: start.hour, minute: start.minute); + _endDate = end; + _endTime = TimeOfDay(hour: end.hour, minute: end.minute); } - if (_event!.reminders!.isNotEmpty) { - _reminders.addAll(_event!.reminders!); + final attendees = event.attendees; + if (attendees != null && attendees.isNotEmpty) { + _attendees = []; + _attendees?.addAll(attendees as Iterable); } - if (_isRecurringEvent) { - _interval = _event!.recurrenceRule!.interval!; - _totalOccurrences = _event!.recurrenceRule!.totalOccurrences; - _recurrenceFrequency = _event!.recurrenceRule!.recurrenceFrequency; + final reminders = event.reminders; + if (reminders != null && reminders.isNotEmpty) { + _reminders = []; + _reminders?.addAll(reminders); + } - if (_totalOccurrences != null) { + final rrule = event.recurrenceRule; + if (rrule != null) { + // debugPrint('OLD_RRULE: ${rrule.toString()}'); + _rrule = rrule; + if (rrule.count != null) { _recurrenceRuleEndType = RecurrenceRuleEndType.MaxOccurrences; } - - if (_event!.recurrenceRule!.endDate != null) { + if (rrule.until != null) { _recurrenceRuleEndType = RecurrenceRuleEndType.SpecifiedEndDate; - _recurrenceEndDate = _event!.recurrenceRule!.endDate!; - } - - _isByDayOfMonth = _event?.recurrenceRule?.weekOfMonth == null; - _daysOfWeek = _event?.recurrenceRule?.daysOfWeek ?? []; - _monthOfYear = - _event?.recurrenceRule?.monthOfYear ?? MonthOfYear.January; - _weekOfMonth = _event?.recurrenceRule?.weekOfMonth ?? WeekNumber.First; - _selectedDayOfWeek = - _daysOfWeek.isNotEmpty ? _daysOfWeek.first : DayOfWeek.Monday; - _dayOfMonth = _event?.recurrenceRule?.dayOfMonth ?? 1; - - if (_daysOfWeek.isNotEmpty) { - _updateDaysOfWeekGroup(); } } - _availability = _event!.availability; - _eventStatus = _event!.status; + _availability = event.availability; + _eventStatus = event.status; } - _startTime = TimeOfDay(hour: _startDate!.hour, minute: _startDate!.minute); - _endTime = TimeOfDay(hour: _endDate!.hour, minute: _endDate!.minute); - // Getting days of the current month (or a selected month for the yearly recurrence) as a default - _getValidDaysOfMonth(_recurrenceFrequency); + _getValidDaysOfMonth(_rrule?.frequency); setState(() {}); } void printAttendeeDetails(Attendee attendee) { - print( + debugPrint( 'attendee name: ${attendee.name}, email address: ${attendee.emailAddress}, type: ${attendee.role?.enumToString}'); - print( + debugPrint( 'ios specifics - status: ${attendee.iosAttendeeDetails?.attendanceStatus}, type: ${attendee.iosAttendeeDetails?.attendanceStatus?.enumToString}'); - print( + debugPrint( 'android specifics - status ${attendee.androidAttendeeDetails?.attendanceStatus}, type: ${attendee.androidAttendeeDetails?.attendanceStatus?.enumToString}'); } @@ -293,6 +286,29 @@ class _CalendarEventPageState extends State { }).toList(), ), ), + if (_eventColors?.isNotEmpty ?? false) + ListTile( + leading: const Text( + 'EventColor', + style: TextStyle(fontSize: 16), + ), + trailing: widget._event?.color == null ? const Text("not set") : Container( + width: 30, + height: 30, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(widget._event?.color ?? 0), + )), + onTap: () async { + if (_eventColors != null) { + final colors = _eventColors?.map((eventColor) => Color(eventColor.color)).toList(); + final newColor = await ColorPickerDialog.selectColorDialog(colors ?? [], context); + setState(() { + _event?.updateEventColor(_eventColors?.firstWhereOrNull((eventColor) => Color(eventColor.color).value == newColor?.value)); + }); + } + }, + ), SwitchListTile( value: _event?.allDay ?? false, onChanged: (value) => @@ -399,8 +415,9 @@ class _CalendarEventPageState extends State { builder: (context) => const EventAttendeePage())); if (result != null) { + _attendees ??= []; setState(() { - _attendees.add(result); + _attendees?.add(result); }); } } @@ -413,7 +430,7 @@ class _CalendarEventPageState extends State { ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, - itemCount: _attendees.length, + itemCount: _attendees?.length ?? 0, itemBuilder: (context, index) { return Container( color: (_attendees?[index].isOrganiser ?? false) @@ -428,11 +445,11 @@ class _CalendarEventPageState extends State { context, MaterialPageRoute( builder: (context) => EventAttendeePage( - attendee: _attendees[index], + attendee: _attendees?[index], eventId: _event?.eventId))); if (result != null) { return setState(() { - _attendees[index] = result; + _attendees?[index] = result; }); } }, @@ -440,7 +457,7 @@ class _CalendarEventPageState extends State { padding: const EdgeInsets.symmetric(vertical: 10.0), child: Text( - '${_attendees[index].name} (${_attendees[index].emailAddress})'), + '${_attendees?[index].name} (${_attendees?[index].emailAddress})'), ), subtitle: Wrap( spacing: 10, @@ -448,7 +465,7 @@ class _CalendarEventPageState extends State { alignment: WrapAlignment.end, children: [ Visibility( - visible: _attendees[index] + visible: _attendees?[index] .androidAttendeeDetails != null, child: Container( @@ -459,11 +476,11 @@ class _CalendarEventPageState extends State { border: Border.all( color: Colors.blueAccent)), child: Text( - 'Android: ${_attendees[index].androidAttendeeDetails?.attendanceStatus?.enumToString}')), + 'Android: ${_attendees?[index].androidAttendeeDetails?.attendanceStatus?.enumToString}')), ), Visibility( visible: - _attendees[index].iosAttendeeDetails != + _attendees?[index].iosAttendeeDetails != null, child: Container( margin: const EdgeInsets.symmetric( @@ -473,10 +490,12 @@ class _CalendarEventPageState extends State { border: Border.all( color: Colors.blueAccent)), child: Text( - 'iOS: ${_attendees[index].iosAttendeeDetails?.attendanceStatus?.enumToString}')), + 'iOS: ${_attendees?[index].iosAttendeeDetails?.attendanceStatus?.enumToString}')), ), Visibility( - visible: _attendees[index].isCurrentUser, + visible: + _attendees?[index].isCurrentUser ?? + false, child: Container( margin: const EdgeInsets.symmetric( vertical: 10.0), @@ -486,7 +505,8 @@ class _CalendarEventPageState extends State { color: Colors.blueAccent)), child: const Text('current user'))), Visibility( - visible: _attendees[index].isOrganiser, + visible: _attendees?[index].isOrganiser ?? + false, child: Container( margin: const EdgeInsets.symmetric( vertical: 10.0), @@ -503,13 +523,13 @@ class _CalendarEventPageState extends State { border: Border.all( color: Colors.blueAccent)), child: Text( - '${_attendees[index].role?.enumToString}'), + '${_attendees?[index].role?.enumToString}'), ), IconButton( padding: const EdgeInsets.all(0), onPressed: () { setState(() { - _attendees.removeAt(index); + _attendees?.removeAt(index); }); }, icon: const Icon( @@ -529,7 +549,7 @@ class _CalendarEventPageState extends State { context, MaterialPageRoute( builder: (context) => - EventRemindersPage(_reminders))); + EventRemindersPage(_reminders ?? []))); if (result == null) { return; } @@ -544,11 +564,11 @@ class _CalendarEventPageState extends State { spacing: 10.0, children: [ const Icon(Icons.alarm), - if (_reminders.isEmpty) + if (_reminders?.isEmpty ?? true) Text(_calendar.isReadOnly == false ? 'Add reminders' : 'Reminders'), - for (var reminder in _reminders) + for (var reminder in _reminders ?? []) Text('${reminder.minutes} minutes before; ') ], ), @@ -556,26 +576,42 @@ class _CalendarEventPageState extends State { ), ), CheckboxListTile( - value: _isRecurringEvent, + value: _rrule != null, title: const Text('Is recurring'), onChanged: (isChecked) { - setState(() { - _isRecurringEvent = isChecked ?? false; - }); + if (isChecked != null) { + setState(() { + if (isChecked) { + _rrule = + RecurrenceRule(frequency: Frequency.daily); + } else { + _rrule = null; + } + }); + } }, ), - if (_isRecurringEvent) ...[ + if (_rrule != null) ...[ ListTile( leading: const Text('Select a Recurrence Type'), - trailing: DropdownButton( + trailing: DropdownButton( onChanged: (selectedFrequency) { setState(() { - _recurrenceFrequency = selectedFrequency; - _getValidDaysOfMonth(_recurrenceFrequency); + _onFrequencyChange( + selectedFrequency ?? Frequency.daily); + _getValidDaysOfMonth(selectedFrequency); }); }, - value: _recurrenceFrequency, - items: RecurrenceFrequency.values + value: _rrule?.frequency, + items: [ + // Frequency.secondly, + // Frequency.minutely, + // Frequency.hourly, + Frequency.daily, + Frequency.weekly, + Frequency.monthly, + Frequency.yearly, + ] .map((frequency) => DropdownMenuItem( value: frequency, child: @@ -591,7 +627,7 @@ class _CalendarEventPageState extends State { const Text('Repeat Every '), Flexible( child: TextFormField( - initialValue: _interval?.toString() ?? '1', + initialValue: '${_rrule?.interval ?? 1}', decoration: const InputDecoration(hintText: '1'), keyboardType: TextInputType.number, @@ -603,30 +639,33 @@ class _CalendarEventPageState extends State { textAlign: TextAlign.right, onSaved: (String? value) { if (value != null) { - _interval = int.tryParse(value); + _rrule = _rrule?.copyWith( + interval: int.tryParse(value)); } }, ), ), _recurrenceFrequencyToIntervalText( - _recurrenceFrequency), + _rrule?.frequency), ], ), ), - if (_recurrenceFrequency == - RecurrenceFrequency.Weekly) ...[ + if (_rrule?.frequency == Frequency.weekly) ...[ Column( children: [ ...DayOfWeek.values.map((day) { return CheckboxListTile( title: Text(day.enumToString), - value: _daysOfWeek.any((dow) => dow == day), + value: _rrule?.byWeekDays + .contains(ByWeekDayEntry(day.index + 1)), onChanged: (selected) { setState(() { if (selected == true) { - _daysOfWeek.add(day); + _rrule?.byWeekDays + .add(ByWeekDayEntry(day.index + 1)); } else { - _daysOfWeek.remove(day); + _rrule?.byWeekDays.remove( + ByWeekDayEntry(day.index + 1)); } _updateDaysOfWeekGroup(selectedDay: day); }); @@ -639,12 +678,13 @@ class _CalendarEventPageState extends State { title: Text(group.enumToString), value: group, groupValue: _dayOfWeekGroup, - onChanged: (selected) { - setState(() { - _dayOfWeekGroup = - selected as DayOfWeekGroup; - _updateDaysOfWeek(); - }); + onChanged: (DayOfWeekGroup? selected) { + if (selected != null) { + setState(() { + _dayOfWeekGroup = selected; + _updateDaysOfWeek(); + }); + } }, controlAffinity: ListTileControlAffinity.trailing); @@ -652,30 +692,43 @@ class _CalendarEventPageState extends State { ], ) ], - if (_recurrenceFrequency == - RecurrenceFrequency.Monthly || - _recurrenceFrequency == - RecurrenceFrequency.Yearly) ...[ + if (_rrule?.frequency == Frequency.monthly || + _rrule?.frequency == Frequency.yearly) ...[ SwitchListTile( - value: _isByDayOfMonth, - onChanged: (value) => - setState(() => _isByDayOfMonth = value), + 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 (_recurrenceFrequency == - RecurrenceFrequency.Yearly && - _isByDayOfMonth) ...[ + if (_rrule?.frequency == Frequency.yearly && + (_rrule?.hasByMonthDays ?? false)) ...[ ListTile( leading: const Text('Month of the year'), trailing: DropdownButton( onChanged: (value) { - setState(() { - _monthOfYear = value; - _getValidDaysOfMonth(_recurrenceFrequency); - }); + if (value != null) { + setState(() { + _rrule = _rrule + ?.copyWith(byMonths: [value.index + 1]); + _getValidDaysOfMonth(_rrule?.frequency); + }); + } }, - value: _monthOfYear, + value: MonthOfYear.values.toList()[ + (_rrule?.hasByMonths ?? false) + ? _rrule!.byMonths.first - 1 + : 0], items: MonthOfYear.values .map((month) => DropdownMenuItem( value: month, @@ -685,20 +738,23 @@ class _CalendarEventPageState extends State { ), ), ], - if (_isByDayOfMonth && - (_recurrenceFrequency == - RecurrenceFrequency.Monthly || - _recurrenceFrequency == - RecurrenceFrequency.Yearly)) ...[ + if ((_rrule?.hasByMonthDays ?? false) && + (_rrule?.frequency == Frequency.monthly || + _rrule?.frequency == Frequency.yearly)) ...[ ListTile( leading: const Text('Day of the month'), trailing: DropdownButton( onChanged: (value) { - setState(() { - _dayOfMonth = value; - }); + if (value != null) { + setState(() { + _rrule = + _rrule?.copyWith(byMonthDays: [value]); + }); + } }, - value: _dayOfMonth, + value: (_rrule?.hasByMonthDays ?? false) + ? _rrule!.byMonthDays.first + : 1, items: _validDaysOfMonth .map((day) => DropdownMenuItem( value: day, @@ -708,23 +764,19 @@ class _CalendarEventPageState extends State { ), ), ], - if (!_isByDayOfMonth && - (_recurrenceFrequency == - RecurrenceFrequency.Monthly || - _recurrenceFrequency == - RecurrenceFrequency.Yearly)) ...[ + 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( - _recurrenceFrequency) + _rrule?.frequency) .data != null - ? Text(_recurrenceFrequencyToText( - _recurrenceFrequency) - .data! + - ' on the ') + ? Text( + '${_recurrenceFrequencyToText(_rrule?.frequency).data!} on the ') : const Text('')), ), Padding( @@ -735,11 +787,23 @@ class _CalendarEventPageState extends State { Flexible( child: DropdownButton( onChanged: (value) { - setState(() { - _weekOfMonth = value; - }); + if (value != null) { + final weekDay = + _rrule?.byWeekDays.first.day ?? 1; + setState(() { + _rrule = _rrule?.copyWith( + byWeekDays: [ + ByWeekDayEntry( + weekDay, value.index + 1) + ]); + }); + } }, - value: _weekOfMonth ?? WeekNumber.First, + value: WeekNumber.values.toList()[ + (_rrule?.hasByWeekDays ?? false) + ? _weekNumFromWeekDayOccurence( + _rrule!.byWeekDays) + : 0], items: WeekNumber.values .map((weekNum) => DropdownMenuItem( value: weekNum, @@ -751,13 +815,25 @@ class _CalendarEventPageState extends State { Flexible( child: DropdownButton( onChanged: (value) { - setState(() { - _selectedDayOfWeek = value; - }); + if (value != null) { + final weekNo = _rrule + ?.byWeekDays.first.occurrence ?? + 1; + setState(() { + _rrule = _rrule?.copyWith( + byWeekDays: [ + ByWeekDayEntry( + value.index + 1, weekNo) + ]); + }); + } }, - value: _selectedDayOfWeek != null - ? DayOfWeek - .values[_selectedDayOfWeek!.index] + 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( @@ -767,17 +843,22 @@ class _CalendarEventPageState extends State { .toList(), ), ), - if (_recurrenceFrequency == - RecurrenceFrequency.Yearly) ...[ + if (_rrule?.frequency == Frequency.yearly) ...[ const Text('of'), Flexible( child: DropdownButton( onChanged: (value) { - setState(() { - _monthOfYear = value; - }); + if (value != null) { + setState(() { + _rrule = _rrule?.copyWith( + byMonths: [value.index + 1]); + }); + } }, - value: _monthOfYear, + value: MonthOfYear.values.toList()[ + (_rrule?.hasByMonths ?? false) + ? _rrule!.byMonths.first - 1 + : 0], items: MonthOfYear.values .map((month) => DropdownMenuItem( value: month, @@ -796,7 +877,9 @@ class _CalendarEventPageState extends State { trailing: DropdownButton( onChanged: (value) { setState(() { - _recurrenceRuleEndType = value; + if (value != null) { + _recurrenceRuleEndType = value; + } }); }, value: _recurrenceRuleEndType, @@ -818,8 +901,7 @@ class _CalendarEventPageState extends State { const Text('For the next '), Flexible( child: TextFormField( - initialValue: - _totalOccurrences?.toString() ?? '1', + initialValue: '${_rrule?.count ?? 1}', decoration: const InputDecoration(hintText: '1'), keyboardType: TextInputType.number, @@ -831,7 +913,8 @@ class _CalendarEventPageState extends State { textAlign: TextAlign.right, onSaved: (String? value) { if (value != null) { - _totalOccurrences = int.tryParse(value); + _rrule = _rrule?.copyWith( + count: int.tryParse(value)); } }, ), @@ -847,15 +930,27 @@ class _CalendarEventPageState extends State { child: DateTimePicker( labelText: 'Date', enableTime: false, - selectedDate: _recurrenceEndDate, + selectedDate: _rrule?.until ?? DateTime.now(), selectDate: (DateTime date) { setState(() { - _recurrenceEndDate = date; + _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), + ] ], ), ), @@ -864,10 +959,11 @@ class _CalendarEventPageState extends State { ElevatedButton( key: const Key('deleteEventButton'), style: ElevatedButton.styleFrom( - primary: Colors.red, onPrimary: Colors.white), + foregroundColor: Colors.white, + backgroundColor: Colors.red), onPressed: () async { bool? result = true; - if (!_isRecurringEvent) { + if (!(_rrule != null)) { await _deviceCalendarPlugin.deleteEvent( _calendar.id, _event?.eventId); } else { @@ -877,7 +973,7 @@ class _CalendarEventPageState extends State { builder: (BuildContext context) { return _recurringEventDialog != null ? _recurringEventDialog as Widget - : const SizedBox(); + : const SizedBox.shrink(); }); } @@ -887,7 +983,7 @@ class _CalendarEventPageState extends State { }, child: const Text('Delete'), ), - ] + ], ], ), ), @@ -902,50 +998,29 @@ class _CalendarEventPageState extends State { if (form?.validate() == false) { _autovalidate = AutovalidateMode.always; // Start validating on every change. - showInSnackBar('Please fix the errors in red before submitting.'); + showInSnackBar( + context, 'Please fix the errors in red before submitting.'); + return; } else { form?.save(); - if (_isRecurringEvent) { - if (!_isByDayOfMonth && - (_recurrenceFrequency == RecurrenceFrequency.Monthly || - _recurrenceFrequency == RecurrenceFrequency.Yearly)) { - // Setting day of the week parameters for WeekNumber to avoid clashing with the weekly recurrence values - _daysOfWeek.clear(); - if (_selectedDayOfWeek != null) { - _daysOfWeek.add(_selectedDayOfWeek as DayOfWeek); - } - } else { - _weekOfMonth = null; - } - - _event?.recurrenceRule = RecurrenceRule(_recurrenceFrequency, - interval: _interval, - totalOccurrences: (_recurrenceRuleEndType == - RecurrenceRuleEndType.MaxOccurrences) - ? _totalOccurrences - : null, - endDate: _recurrenceRuleEndType == - RecurrenceRuleEndType.SpecifiedEndDate - ? _recurrenceEndDate - : null, - daysOfWeek: _daysOfWeek, - dayOfMonth: _dayOfMonth, - monthOfYear: _monthOfYear, - weekOfMonth: _weekOfMonth); - } - _event?.attendees = _attendees; - _event?.reminders = _reminders; - _event?.availability = _availability; - _event?.status = _eventStatus; - var createEventResult = - await _deviceCalendarPlugin.createOrUpdateEvent(_event); - if (createEventResult?.isSuccess == true) { - Navigator.pop(context, true); - } else { - showInSnackBar(createEventResult?.errors - .map((err) => '[${err.errorCode}] ${err.errorMessage}') - .join(' | ') as String); - } + _adjustStartEnd(); + _event?.recurrenceRule = _rrule; + // debugPrint('FINAL_RRULE: ${_rrule.toString()}'); + } + _event?.attendees = _attendees; + _event?.reminders = _reminders; + _event?.availability = _availability; + _event?.status = _eventStatus; + var createEventResult = + await _deviceCalendarPlugin.createOrUpdateEvent(_event); + if (createEventResult?.isSuccess == true) { + Navigator.pop(context, true); + } else { + showInSnackBar( + context, + createEventResult?.errors + .map((err) => '[${err.errorCode}] ${err.errorMessage}') + .join(' | ') as String); } }, child: const Icon(Icons.check), @@ -954,34 +1029,31 @@ class _CalendarEventPageState extends State { ); } - Text _recurrenceFrequencyToText(RecurrenceFrequency? recurrenceFrequency) { - switch (recurrenceFrequency) { - case RecurrenceFrequency.Daily: - return const Text('Daily'); - case RecurrenceFrequency.Weekly: - return const Text('Weekly'); - case RecurrenceFrequency.Monthly: - return const Text('Monthly'); - case RecurrenceFrequency.Yearly: - return const Text('Yearly'); - default: - return const Text(''); + Text _recurrenceFrequencyToText(Frequency? recurrenceFrequency) { + if (recurrenceFrequency == Frequency.daily) { + return const Text('Daily'); + } else if (recurrenceFrequency == Frequency.weekly) { + return const Text('Weekly'); + } else if (recurrenceFrequency == Frequency.monthly) { + return const Text('Monthly'); + } else if (recurrenceFrequency == Frequency.yearly) { + return const Text('Yearly'); + } else { + return const Text(''); } } - Text _recurrenceFrequencyToIntervalText( - RecurrenceFrequency? recurrenceFrequency) { - switch (recurrenceFrequency) { - case RecurrenceFrequency.Daily: - return const Text(' Day(s)'); - case RecurrenceFrequency.Weekly: - return const Text(' Week(s) on'); - case RecurrenceFrequency.Monthly: - return const Text(' Month(s)'); - case RecurrenceFrequency.Yearly: - return const Text(' Year(s)'); - default: - 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(''); } } @@ -999,14 +1071,14 @@ class _CalendarEventPageState extends State { } // Get total days of a month - void _getValidDaysOfMonth(RecurrenceFrequency? frequency) { + void _getValidDaysOfMonth(Frequency? frequency) { _validDaysOfMonth.clear(); var totalDays = 0; // Year frequency: Get total days of the selected month - if (frequency == RecurrenceFrequency.Yearly) { + if (frequency == Frequency.yearly) { totalDays = DateTime(DateTime.now().year, - _monthOfYear?.value != null ? _monthOfYear!.value + 1 : 1, 0) + (_rrule?.hasByMonths ?? false) ? _rrule!.byMonths.first : 1, 0) .day; } else { // Otherwise, get total days of the current month @@ -1020,49 +1092,153 @@ class _CalendarEventPageState extends State { } void _updateDaysOfWeek() { - if (_dayOfWeekGroup == null) return; - var days = _dayOfWeekGroup!.getDays; - switch (_dayOfWeekGroup) { case DayOfWeekGroup.Weekday: + _rrule = _rrule?.copyWith(byWeekDays: [ + ByWeekDayEntry(1), + ByWeekDayEntry(2), + ByWeekDayEntry(3), + ByWeekDayEntry(4), + ByWeekDayEntry(5), + ]); + break; case DayOfWeekGroup.Weekend: + _rrule = _rrule?.copyWith(byWeekDays: [ + ByWeekDayEntry(6), + ByWeekDayEntry(7), + ]); + break; case DayOfWeekGroup.AllDays: - _daysOfWeek.clear(); - _daysOfWeek.addAll(days.where((a) => _daysOfWeek.every((b) => a != b))); + _rrule = _rrule?.copyWith(byWeekDays: [ + ByWeekDayEntry(1), + ByWeekDayEntry(2), + ByWeekDayEntry(3), + ByWeekDayEntry(4), + ByWeekDayEntry(5), + ByWeekDayEntry(6), + ByWeekDayEntry(7), + ]); break; case DayOfWeekGroup.None: - _daysOfWeek.clear(); - break; default: - _daysOfWeek.clear(); + _rrule?.byWeekDays.clear(); + break; } + // () => setState(() => {}); } void _updateDaysOfWeekGroup({DayOfWeek? selectedDay}) { - var deepEquality = const DeepCollectionEquality.unordered().equals; - - // If _daysOfWeek contains nothing - if (_daysOfWeek.isEmpty && _dayOfWeekGroup != DayOfWeekGroup.None) { - _dayOfWeekGroup = DayOfWeekGroup.None; - } - // If _daysOfWeek contains Monday to Friday - else if (deepEquality(_daysOfWeek, DayOfWeekGroup.Weekday.getDays) && - _dayOfWeekGroup != DayOfWeekGroup.Weekday) { - _dayOfWeekGroup = DayOfWeekGroup.Weekday; + final byWeekDays = _rrule?.byWeekDays; + if (byWeekDays != null) { + if (byWeekDays.length == 7 && + byWeekDays.every((p0) => + p0.day == 1 || + p0.day == 2 || + p0.day == 3 || + p0.day == 4 || + p0.day == 5 || + p0.day == 6 || + p0.day == 7)) { + _dayOfWeekGroup = DayOfWeekGroup.AllDays; + } else if (byWeekDays.length == 5 && + byWeekDays.every((p0) => + p0.day == 1 || + p0.day == 2 || + p0.day == 3 || + p0.day == 4 || + p0.day == 5) && + byWeekDays.none((p0) => p0.day == 6 || p0.day == 7)) { + _dayOfWeekGroup = DayOfWeekGroup.Weekday; + } else if (byWeekDays.length == 2 && + byWeekDays.every((p0) => p0.day == 6 || p0.day == 7) && + byWeekDays.none((p0) => + p0.day == 1 || + p0.day == 2 || + p0.day == 3 || + p0.day == 4 || + p0.day == 5)) { + _dayOfWeekGroup = DayOfWeekGroup.Weekend; + } else { + _dayOfWeekGroup = DayOfWeekGroup.None; + } } - // If _daysOfWeek contains Saturday and Sunday - else if (deepEquality(_daysOfWeek, DayOfWeekGroup.Weekend.getDays) && - _dayOfWeekGroup != DayOfWeekGroup.Weekend) { - _dayOfWeekGroup = DayOfWeekGroup.Weekend; + } + + int _weekNumFromWeekDayOccurence(List weekdays) { + final weekNum = weekdays.first.occurrence; + if (weekNum != null) { + return weekNum - 1; + } else { + return 0; } - // If _daysOfWeek contains all days - else if (deepEquality(_daysOfWeek, DayOfWeekGroup.AllDays.getDays) && - _dayOfWeekGroup != DayOfWeekGroup.AllDays) { - _dayOfWeekGroup = DayOfWeekGroup.AllDays; + } + + void _onFrequencyChange(Frequency freq) { + final rrule = _rrule; + if (rrule != null) { + final hasByWeekDays = rrule.hasByWeekDays; + final hasByMonthDays = rrule.hasByMonthDays; + final hasByMonths = rrule.hasByMonths; + if (freq == Frequency.daily || freq == Frequency.weekly) { + if (hasByWeekDays) { + rrule.byWeekDays.clear(); + } + if (hasByMonths) { + rrule.byMonths.clear(); + } + _rrule = rrule.copyWith(frequency: freq); + } + if (freq == Frequency.monthly) { + if (hasByMonths) { + rrule.byMonths.clear(); + } + if (!hasByWeekDays && !hasByMonthDays) { + _rrule = rrule + .copyWith(frequency: freq, byWeekDays: [ByWeekDayEntry(1, 1)]); + } else { + _rrule = rrule.copyWith(frequency: freq); + } + } + if (freq == Frequency.yearly) { + if (!hasByWeekDays || !hasByMonths) { + _rrule = rrule.copyWith( + frequency: freq, + byWeekDays: [ByWeekDayEntry(1, 1)], + byMonths: [1]); + } else { + _rrule = rrule.copyWith(frequency: freq); + } + } } - // Otherwise null - else { - _dayOfWeekGroup = null; + } + + /// In order to avoid an event instance to appear outside of the recurrence + /// rrule, the start and end date have to be adjusted to match the first + /// instance. + void _adjustStartEnd() { + final start = _event?.start; + final end = _event?.end; + final rrule = _rrule; + if (start != null && end != null && rrule != null) { + final allDay = _event?.allDay ?? false; + final duration = end.difference(start); + final instances = rrule.getAllInstances( + start: allDay + ? DateTime.utc(start.year, start.month, start.day) + : DateTime(start.year, start.month, start.day, start.hour, + start.minute) + .toUtc(), + before: rrule.count == null && rrule.until == null + ? DateTime(start.year + 2, start.month, start.day, start.hour, + start.minute) + .toUtc() + : null); + if (instances.isNotEmpty) { + var newStart = TZDateTime.from(instances.first, start.location); + var newEnd = newStart.add(duration); + _event?.start = newStart; + _event?.end = newEnd; + } } } @@ -1087,7 +1263,6 @@ class _CalendarEventPageState extends State { if (value.isEmpty) { return 'Name is required.'; } - return null; } @@ -1106,7 +1281,7 @@ class _CalendarEventPageState extends State { .add(Duration(hours: time.hour, minutes: time.minute)); } - void showInSnackBar(String value) { + void showInSnackBar(BuildContext context, String value) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value))); } } diff --git a/example/lib/presentation/pages/calendar_events.dart b/example/lib/presentation/pages/calendar_events.dart index 584458e3..6b2e8384 100644 --- a/example/lib/presentation/pages/calendar_events.dart +++ b/example/lib/presentation/pages/calendar_events.dart @@ -24,6 +24,7 @@ class _CalendarEventsPageState extends State { late DeviceCalendarPlugin _deviceCalendarPlugin; List _calendarEvents = []; + List? _eventColors; bool _isLoading = true; _CalendarEventsPageState(this._calendar) { @@ -33,6 +34,7 @@ class _CalendarEventsPageState extends State { @override void initState() { super.initState(); + _retrieveEventColors(); _retrieveCalendarEvents(); } @@ -77,7 +79,7 @@ class _CalendarEventsPageState extends State { onPressed: () async { final refreshEvents = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventPage(_calendar); + return CalendarEventPage(_calendar, null, null, _eventColors); })); if (refreshEvents == true) { await _retrieveCalendarEvents(); @@ -123,6 +125,7 @@ class _CalendarEventsPageState extends State { _onLoading, _onDeletedFinished, ), + _eventColors ); })); if (refreshEvents != null && refreshEvents) { @@ -132,16 +135,20 @@ class _CalendarEventsPageState extends State { Future _retrieveCalendarEvents() async { final startDate = DateTime.now().add(const Duration(days: -30)); - final endDate = 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 as List; + _calendarEvents = calendarEventsResult.data ?? []; _isLoading = false; }); } + void _retrieveEventColors() async { + _eventColors = await _deviceCalendarPlugin.retrieveEventColors(_calendar); + } + Widget _getDeleteButton() { return IconButton( icon: const Icon(Icons.delete), @@ -158,9 +165,9 @@ class _CalendarEventsPageState extends State { title: const Text('Warning'), content: SingleChildScrollView( child: ListBody( - children: [ - const Text('This will delete this calendar'), - const Text('Are you sure?'), + children: const [ + Text('This will delete this calendar'), + Text('Are you sure?'), ], ), ), @@ -169,7 +176,7 @@ class _CalendarEventsPageState extends State { onPressed: () async { var returnValue = await _deviceCalendarPlugin.deleteCalendar(_calendar.id!); - print( + debugPrint( 'returnValue: ${returnValue.data}, ${returnValue.errors}'); Navigator.of(context).pop(); Navigator.of(context).pop(); diff --git a/example/lib/presentation/pages/calendars.dart b/example/lib/presentation/pages/calendars.dart index 4dbf22e3..bc434173 100644 --- a/example/lib/presentation/pages/calendars.dart +++ b/example/lib/presentation/pages/calendars.dart @@ -1,7 +1,11 @@ +import 'dart:io'; + import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/presentation/pages/calendar_add.dart'; -import 'package:flutter/services.dart'; +import 'package:device_calendar_example/presentation/color_picker_dialog.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:collection/collection.dart'; import 'calendar_events.dart'; @@ -17,6 +21,7 @@ class CalendarsPage extends StatefulWidget { class _CalendarsPageState extends State { late DeviceCalendarPlugin _deviceCalendarPlugin; List _calendars = []; + List get _writableCalendars => _calendars.where((c) => c.isReadOnly == false).toList(); @@ -46,7 +51,10 @@ class _CalendarsPageState extends State { padding: const EdgeInsets.all(10.0), child: Text( 'WARNING: some aspects of saving events are hardcoded in this example app. As such we recommend you do not modify existing events as this may result in loss of information', - style: Theme.of(context).textTheme.headline6, + style: Theme + .of(context) + .textTheme + .titleLarge, ), ), Expanded( @@ -56,14 +64,14 @@ class _CalendarsPageState extends State { 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)}'), + ? 'readOnlyCalendar${_readOnlyCalendars.indexWhere((c) => c.id == _calendars[index].id)} color:${_calendars[index].color}' + : 'writableCalendar${_writableCalendars.indexWhere((c) => c.id == _calendars[index].id)} color:${_calendars[index].color}'), onTap: () async { await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return CalendarEventsPage(_calendars[index], - key: const Key('calendarEventsPage')); - })); + return CalendarEventsPage(_calendars[index], + key: const Key('calendarEventsPage')); + })); }, child: Padding( padding: const EdgeInsets.all(10.0), @@ -75,21 +83,64 @@ class _CalendarsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "${_calendars[index].id}: ${_calendars[index].name!}", + "${_calendars[index] + .id}: ${_calendars[index].name!}", style: - Theme.of(context).textTheme.subtitle1, + Theme + .of(context) + .textTheme + .titleSmall, ), Text( - "Account: ${_calendars[index].accountName!}"), + "Account: ${_calendars[index] + .accountName!}"), Text( "type: ${_calendars[index].accountType}"), ])), - Container( - width: 15, - height: 15, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color(_calendars[index].color!)), + GestureDetector( + onTap: () async { + final calendar = _calendars[index]; + final googleCalendarColors = await _deviceCalendarPlugin + .retrieveCalendarColors(_calendars[index]); + final colors = googleCalendarColors.isNotEmpty + ? googleCalendarColors.map((calendarColor) => + Color(calendarColor.color)).toList() + : [ + Colors.red, + Colors.green, + Colors.blue, + Colors.yellow, + Colors.orange, + Colors.purple, + Colors.cyan, + Colors.pink, + Colors.brown, + Colors.grey, + ]; + final color = await ColorPickerDialog + .selectColorDialog(colors, context); + if (color != null) { + final success = await _deviceCalendarPlugin + .updateCalendarColor(calendar, + calendarColor: googleCalendarColors + .firstWhereOrNull((calendarColor) => + calendarColor.color == color.value), + color: color); + if (success) { + _retrieveCalendars(); + } + } + }, + child: Container( + key: ValueKey(_calendars[index].color), + margin: const EdgeInsets.symmetric( + horizontal: 5, vertical: 10), + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Color(_calendars[index].color!)), + ), ), const SizedBox(width: 10), if (_calendars[index].isDefault!) @@ -116,8 +167,8 @@ class _CalendarsPageState extends State { onPressed: () async { final createCalendar = await Navigator.push(context, MaterialPageRoute(builder: (BuildContext context) { - return const CalendarAddPage(); - })); + return const CalendarAddPage(); + })); if (createCalendar == true) { _retrieveCalendars(); @@ -146,8 +197,8 @@ class _CalendarsPageState extends State { setState(() { _calendars = calendarsResult.data as List; }); - } on PlatformException catch (e) { - print(e); + } on PlatformException catch (e, s) { + debugPrint('RETRIEVE_CALENDARS: $e, $s'); } } @@ -158,4 +209,4 @@ class _CalendarsPageState extends State { _retrieveCalendars(); }); } -} +} \ No newline at end of file diff --git a/example/lib/presentation/pages/event_attendee.dart b/example/lib/presentation/pages/event_attendee.dart index d262d64f..2fff734e 100644 --- a/example/lib/presentation/pages/event_attendee.dart +++ b/example/lib/presentation/pages/event_attendee.dart @@ -1,8 +1,8 @@ import 'dart:io'; +import 'package:device_calendar/device_calendar.dart'; import 'package:device_calendar_example/common/app_routes.dart'; import 'package:flutter/material.dart'; -import 'package:device_calendar/device_calendar.dart'; late DeviceCalendarPlugin _deviceCalendarPlugin; @@ -14,7 +14,7 @@ class EventAttendeePage extends StatefulWidget { @override _EventAttendeePageState createState() => - _EventAttendeePageState(attendee, eventId); + _EventAttendeePageState(attendee, eventId ?? ''); } class _EventAttendeePageState extends State { @@ -112,7 +112,7 @@ class _EventAttendeePageState extends State { onTap: () async { _deviceCalendarPlugin = DeviceCalendarPlugin(); - var result = await _deviceCalendarPlugin + await _deviceCalendarPlugin .showiOSEventModal(_eventId); Navigator.popUntil( context, ModalRoute.withName(AppRoutes.calendars)); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e840c944..9ffb81f4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,66 +4,24 @@ version: 3.2.0 publish_to: none environment: - sdk: '>=2.12.0 <3.0.0' + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - intl: - uuid: - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: + intl: ^0.17.0 + uuid: ^3.0.6 + flutter_timezone: ^3.0.1 + device_calendar: + path: ../ dev_dependencies: integration_test: sdk: flutter flutter_test: sdk: flutter - test: - - device_calendar: - path: ../ + flutter_lints: ^2.0.1 -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages + uses-material-design: true \ No newline at end of file diff --git a/ios/Classes/SwiftDeviceCalendarPlugin.swift b/ios/Classes/SwiftDeviceCalendarPlugin.swift index 41863133..8c302f49 100644 --- a/ios/Classes/SwiftDeviceCalendarPlugin.swift +++ b/ios/Classes/SwiftDeviceCalendarPlugin.swift @@ -14,8 +14,17 @@ extension EKParticipant { } } +extension String { + func match(_ regex: String) -> [[String]] { + let nsString = self as NSString + return (try? NSRegularExpression(pattern: regex, options: []))?.matches(in: self, options: [], range: NSMakeRange(0, nsString.length)).map { match in + (0.. EKSource? { let localSources = eventStore.sources.filter { $0.sourceType == .local } - if (!localSources.isEmpty) { - return localSources.first - } + if (!localSources.isEmpty) { + return localSources.first + } - if let defaultSource = eventStore.defaultCalendarForNewEvents?.source { - return defaultSource - } + if let defaultSource = eventStore.defaultCalendarForNewEvents?.source { + return defaultSource + } - let iCloudSources = eventStore.sources.filter { $0.sourceType == .calDAV && $0.sourceIdentifier == "iCloud" } + let iCloudSources = eventStore.sources.filter { $0.sourceType == .calDAV && $0.sourceIdentifier == "iCloud" } - if (!iCloudSources.isEmpty) { - return iCloudSources.first - } + if (!iCloudSources.isEmpty) { + return iCloudSources.first + } - return nil - } + return nil + } private func createCalendar(_ call: FlutterMethodCall, _ result: FlutterResult) { let arguments = call.arguments as! Dictionary @@ -217,7 +237,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return } - calendar.source = source + calendar.source = source try eventStore.saveCalendar(calendar, commit: true) result(calendar.calendarIdentifier) @@ -228,13 +248,45 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } + private func updateCalendarColor(_ call: FlutterMethodCall, _ result: FlutterResult) { + let arguments = call.arguments as! Dictionary + let calendarId = arguments[calendarIdArgument] as! String + let color = arguments[calendarColorArgument] as! Int + + guard let calendar = eventStore.calendar(withIdentifier: calendarId) else { + print("Calendar not found") + result(false) + return + } + + // Update the calendar color + calendar.cgColor = UIColorFromRGB(color).cgColor + + // Save the changes + do { + try eventStore.saveCalendar(calendar, commit: true) + result(true) // Assuming the operation was successful, return true + } catch { + result(FlutterError(code: self.genericError, message: error.localizedDescription, details: nil)) + } + } + + func UIColorFromRGB(_ rgbValue: Int) -> UIColor { + return UIColor( + red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0, + green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0, + blue: CGFloat(rgbValue & 0x0000FF) / 255.0, + alpha: CGFloat(1.0) + ) + } + private func retrieveCalendars(_ result: @escaping FlutterResult) { checkPermissionsThenExecute(permissionsGrantedAction: { let ekCalendars = self.eventStore.calendars(for: .event) let defaultCalendar = self.eventStore.defaultCalendarForNewEvents - var calendars = [Calendar]() + var calendars = [DeviceCalendar]() for ekCalendar in ekCalendars { - let calendar = Calendar( + let calendar = DeviceCalendar( id: ekCalendar.calendarIdentifier, name: ekCalendar.title, isReadOnly: !ekCalendar.allowsContentModifications, @@ -308,11 +360,35 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch!.doubleValue / 1000.0) let ekCalendar = self.eventStore.calendar(withIdentifier: calendarId) if ekCalendar != nil { - let predicate = self.eventStore.predicateForEvents( - withStart: startDate, - end: endDate, - calendars: [ekCalendar!]) - let ekEvents = self.eventStore.events(matching: predicate) + var ekEvents = [EKEvent]() + let fourYearsInSeconds = 4 * 365 * 24 * 60 * 60 + 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) @@ -341,6 +417,7 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } let event = createEventFromEkEvent(calendarId: calendarId, ekEvent: ekEvent!) + events.append(event) } @@ -410,13 +487,13 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele private func convertEkEventAvailability(ekEventAvailability: EKEventAvailability?) -> Availability? { switch ekEventAvailability { case .busy: - return Availability.BUSY + return Availability.BUSY case .free: return Availability.FREE - case .tentative: - return Availability.TENTATIVE - case .unavailable: - return Availability.UNAVAILABLE + case .tentative: + return Availability.TENTATIVE + case .unavailable: + return Availability.UNAVAILABLE default: return nil } @@ -425,13 +502,13 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele private func convertEkEventStatus(ekEventStatus: EKEventStatus?) -> EventStatus? { switch ekEventStatus { case .confirmed: - return EventStatus.CONFIRMED + return EventStatus.CONFIRMED case .tentative: return EventStatus.TENTATIVE - case .canceled: - return EventStatus.CANCELED - case .none?: - return EventStatus.NONE + case .canceled: + return EventStatus.CANCELED + case .none?: + return EventStatus.NONE default: return nil } @@ -441,92 +518,157 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele var recurrenceRule: RecurrenceRule? if ekEvent.hasRecurrenceRules { let ekRecurrenceRule = ekEvent.recurrenceRules![0] - var frequency: Int + var frequency: String switch ekRecurrenceRule.frequency { case EKRecurrenceFrequency.daily: - frequency = 0 + frequency = "DAILY" case EKRecurrenceFrequency.weekly: - frequency = 1 + frequency = "WEEKLY" case EKRecurrenceFrequency.monthly: - frequency = 2 + frequency = "MONTHLY" case EKRecurrenceFrequency.yearly: - frequency = 3 + frequency = "YEARLY" default: - frequency = 0 + frequency = "DAILY" } - var totalOccurrences: Int? - var endDate: Int64? + var count: Int? + var endDate: String? if(ekRecurrenceRule.recurrenceEnd?.occurrenceCount != nil && ekRecurrenceRule.recurrenceEnd?.occurrenceCount != 0) { - totalOccurrences = ekRecurrenceRule.recurrenceEnd?.occurrenceCount + count = ekRecurrenceRule.recurrenceEnd?.occurrenceCount } - let endDateMs = ekRecurrenceRule.recurrenceEnd?.endDate?.millisecondsSinceEpoch - if(endDateMs != nil) { - endDate = Int64(exactly: endDateMs!) + let endDateRaw = ekRecurrenceRule.recurrenceEnd?.endDate + if(endDateRaw != nil) { + endDate = formateDateTime(dateTime: endDateRaw!) } - var weekOfMonth = ekRecurrenceRule.setPositions?.first?.intValue + let byWeekDays = ekRecurrenceRule.daysOfTheWeek + let byMonthDays = ekRecurrenceRule.daysOfTheMonth + let byYearDays = ekRecurrenceRule.daysOfTheYear + let byWeeks = ekRecurrenceRule.weeksOfTheYear + let byMonths = ekRecurrenceRule.monthsOfTheYear + let bySetPositions = ekRecurrenceRule.setPositions - var daysOfWeek: [Int]? - if ekRecurrenceRule.daysOfTheWeek != nil && !ekRecurrenceRule.daysOfTheWeek!.isEmpty { - daysOfWeek = [] - for dayOfWeek in ekRecurrenceRule.daysOfTheWeek! { - daysOfWeek!.append(dayOfWeek.dayOfTheWeek.rawValue - 1) + 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 + } - if weekOfMonth == nil { - weekOfMonth = dayOfWeek.weekNumber - } - } - } + 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)" + } + } - // For recurrence of nth day of nth month every year, no calendar parameters are given - // So we need to explicitly set them from event start date - var dayOfMonth = ekRecurrenceRule.daysOfTheMonth?.first?.intValue - var monthOfYear = ekRecurrenceRule.monthsOfTheYear?.first?.intValue - if (ekRecurrenceRule.frequency == EKRecurrenceFrequency.yearly - && weekOfMonth == nil && dayOfMonth == nil && monthOfYear == nil) { - let dateFormatter = DateFormatter() + 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" + } + } - // Setting day of the month - dateFormatter.dateFormat = "d" - dayOfMonth = Int(dateFormatter.string(from: ekEvent.startDate)) + private func formateDateTime(dateTime: Date) -> String { + var calendar = Calendar.current + calendar.timeZone = TimeZone.current - // Setting month of the year - dateFormatter.dateFormat = "M" - monthOfYear = Int(dateFormatter.string(from: ekEvent.startDate)) - } + func twoDigits(_ n: Int) -> String { + if (n < 10) {return "0\(n)"} else {return "\(n)"} + } - recurrenceRule = RecurrenceRule( - recurrenceFrequency: frequency, - totalOccurrences: totalOccurrences, - interval: ekRecurrenceRule.interval, - endDate: endDate, - daysOfWeek: daysOfWeek, - dayOfMonth: dayOfMonth, - monthOfYear: monthOfYear, - weekOfMonth: weekOfMonth) + 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)" } - return recurrenceRule + let year = calendar.component(.year, from: dateTime) + let month = calendar.component(.month, from: dateTime) + let day = calendar.component(.day, from: dateTime) + let hour = calendar.component(.hour, from: dateTime) + let minutes = calendar.component(.minute, from: dateTime) + let seconds = calendar.component(.second, from: dateTime) + + assert(year >= 0 && year <= 9999) + + let yearString = fourDigits(year) + let monthString = twoDigits(month) + let dayString = twoDigits(day) + let hourString = twoDigits(hour) + let minuteString = twoDigits(minutes) + let secondString = twoDigits(seconds) + let utcSuffix = calendar.timeZone == TimeZone(identifier: "UTC") ? "Z" : "" + return "\(yearString)-\(monthString)-\(dayString)T\(hourString):\(minuteString):\(secondString)\(utcSuffix)" + } private func createEKRecurrenceRules(_ arguments: [String : AnyObject]) -> [EKRecurrenceRule]?{ let recurrenceRuleArguments = arguments[recurrenceRuleArgument] as? Dictionary + + //print("ARGUMENTS: \(recurrenceRuleArguments as AnyObject)") + if recurrenceRuleArguments == nil { return nil } - let recurrenceFrequencyIndex = recurrenceRuleArguments![recurrenceFrequencyArgument] as? NSInteger - let totalOccurrences = recurrenceRuleArguments![totalOccurrencesArgument] as? NSInteger + let recurrenceFrequency = recurrenceRuleArguments![recurrenceFrequencyArgument] as? String + let totalOccurrences = recurrenceRuleArguments![countArgument] as? NSInteger let interval = recurrenceRuleArguments![intervalArgument] as? NSInteger var recurrenceInterval = 1 - let endDate = recurrenceRuleArguments![endDateArgument] as? NSNumber - let namedFrequency = validFrequencyTypes[recurrenceFrequencyIndex!] + var 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? + var recurrenceEnd: EKRecurrenceEnd? if endDate != nil { - recurrenceEnd = EKRecurrenceEnd(end: Date.init(timeIntervalSince1970: endDate!.doubleValue / 1000)) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" + + if (!endDate!.hasSuffix("Z")){ + endDate!.append("Z") + } + + let dateTime = dateFormatter.date(from: endDate!) + if dateTime != nil { + recurrenceEnd = EKRecurrenceEnd(end: dateTime!) + } } else if(totalOccurrences != nil && totalOccurrences! > 0) { recurrenceEnd = EKRecurrenceEnd(occurrenceCount: totalOccurrences!) } @@ -535,59 +677,44 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele recurrenceInterval = interval! } - let daysOfWeekIndices = recurrenceRuleArguments![daysOfWeekArgument] as? [Int] - var daysOfWeek : [EKRecurrenceDayOfWeek]? + let byWeekDaysStrings = recurrenceRuleArguments![byWeekDaysArgument] as? [String] + var byWeekDays = [EKRecurrenceDayOfWeek]() - if daysOfWeekIndices != nil && !daysOfWeekIndices!.isEmpty { - daysOfWeek = [] - for dayOfWeekIndex in daysOfWeekIndices! { - // Append week number to BYDAY for yearly or monthly with 'last' week number - if let weekOfMonth = recurrenceRuleArguments![weekOfMonthArgument] as? Int { - if namedFrequency == EKRecurrenceFrequency.yearly || weekOfMonth == -1 { - daysOfWeek!.append(EKRecurrenceDayOfWeek.init( - dayOfTheWeek: EKWeekday.init(rawValue: dayOfWeekIndex + 1)!, - weekNumber: weekOfMonth - )) - } - } else { - daysOfWeek!.append(EKRecurrenceDayOfWeek.init(EKWeekday.init(rawValue: dayOfWeekIndex + 1)!)) - } + if (byWeekDaysStrings != nil) { + byWeekDaysStrings?.forEach { string in + let entry = recurrenceDayOfWeekFromString(recDay: string) + if entry != nil {byWeekDays.append(entry!)} } } - var dayOfMonthArray : [NSNumber]? - if let dayOfMonth = recurrenceRuleArguments![dayOfMonthArgument] as? Int { - dayOfMonthArray = [] - dayOfMonthArray!.append(NSNumber(value: dayOfMonth)) - } - - var monthOfYearArray : [NSNumber]? - if let monthOfYear = recurrenceRuleArguments![monthOfYearArgument] as? Int { - monthOfYearArray = [] - monthOfYearArray!.append(NSNumber(value: monthOfYear)) - } - - // Append BYSETPOS only on monthly (but not last), yearly's week number (and last for monthly) appends to BYDAY - var weekOfMonthArray : [NSNumber]? - if namedFrequency == EKRecurrenceFrequency.monthly { - if let weekOfMonth = recurrenceRuleArguments![weekOfMonthArgument] as? Int { - if weekOfMonth != -1 { - weekOfMonthArray = [] - weekOfMonthArray!.append(NSNumber(value: weekOfMonth)) - } - } - } + let 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] - return [EKRecurrenceRule( + let ekrecurrenceRule = EKRecurrenceRule( recurrenceWith: namedFrequency, interval: recurrenceInterval, - daysOfTheWeek: daysOfWeek, - daysOfTheMonth: dayOfMonthArray, - monthsOfTheYear: monthOfYearArray, - weeksOfTheYear: nil, - daysOfTheYear: nil, - setPositions: weekOfMonthArray, - end: recurrenceEnd)] + daysOfTheWeek: byWeekDays.isEmpty ? nil : byWeekDays, + daysOfTheMonth: byMonthDays?.map {NSNumber(value: $0)}, + monthsOfTheYear: byMonths?.map {NSNumber(value: $0)}, + weeksOfTheYear: byWeeks?.map {NSNumber(value: $0)}, + daysOfTheYear: byYearDays?.map {NSNumber(value: $0)}, + setPositions: bySetPositions?.map {NSNumber(value: $0)}, + end: recurrenceEnd) + //print("ekrecurrenceRule: \(String(describing: ekrecurrenceRule))") + return [ekrecurrenceRule] + } + + private func rruleStringFromEKRRule(_ ekRrule: EKRecurrenceRule) -> String { + let ekRRuleAnyObject = ekRrule as AnyObject + var ekRRuleString = "\(ekRRuleAnyObject)" + if let range = ekRRuleString.range(of: "RRULE ") { + ekRRuleString = String(ekRRuleString[range.upperBound...]) + //print("EKRULE_RESULT_STRING: \(ekRRuleString)") + } + return ekRRuleString } private func setAttendees(_ arguments: [String : AnyObject], _ ekEvent: EKEvent?) { @@ -642,6 +769,54 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele 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 @@ -652,8 +827,8 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele return .busy case Availability.FREE.rawValue: return .free - case Availability.TENTATIVE.rawValue: - return .tentative + case Availability.TENTATIVE.rawValue: + return .tentative case Availability.UNAVAILABLE.rawValue: return .unavailable default: @@ -666,13 +841,13 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele let arguments = call.arguments as! Dictionary let calendarId = arguments[calendarIdArgument] as! String let eventId = arguments[eventIdArgument] as? String - let isAllDay = arguments[eventAllDayArgument] as! Bool + let isAllDay = (arguments[eventAllDayArgument] as? Bool) ?? false let startDateMillisecondsSinceEpoch = arguments[eventStartDateArgument] as! NSNumber let endDateDateMillisecondsSinceEpoch = arguments[eventEndDateArgument] as! NSNumber let startDate = Date (timeIntervalSince1970: startDateMillisecondsSinceEpoch.doubleValue / 1000.0) let endDate = Date (timeIntervalSince1970: endDateDateMillisecondsSinceEpoch.doubleValue / 1000.0) let startTimeZoneString = arguments[eventStartTimeZoneArgument] as? String - let title = arguments[self.eventTitleArgument] 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 @@ -698,16 +873,16 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele } } - ekEvent!.title = title + ekEvent!.title = title ?? "" ekEvent!.notes = description ekEvent!.isAllDay = isAllDay ekEvent!.startDate = startDate - ekEvent!.endDate = endDate + ekEvent!.endDate = endDate - if (!isAllDay) { - let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current + if (!isAllDay) { + let timeZone = TimeZone(identifier: startTimeZoneString ?? TimeZone.current.identifier) ?? .current ekEvent!.timeZone = timeZone - } + } ekEvent!.calendar = ekCalendar! ekEvent!.location = location @@ -818,60 +993,60 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele }, result: result) } - private func showEventModal(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { - checkPermissionsThenExecute(permissionsGrantedAction: { - let arguments = call.arguments as! Dictionary - let eventId = arguments[eventIdArgument] as! String - let event = self.eventStore.event(withIdentifier: eventId) + 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 + 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) + let flutterViewController = getTopMostViewController() + let navigationController = UINavigationController(rootViewController: eventController) - navigationController.toolbar.isTranslucent = false - navigationController.toolbar.tintColor = .blue - navigationController.toolbar.backgroundColor = .white + navigationController.toolbar.isTranslucent = false + navigationController.toolbar.tintColor = .blue + navigationController.toolbar.backgroundColor = .white - flutterViewController.present(navigationController, animated: true, completion: nil) + 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) + } else { + result(FlutterError(code: self.genericError, message: self.eventNotFoundErrorMessageFormat, details: nil)) + } + }, result: result) + } - if flutterResult != nil { - switch action { - case .done: - flutterResult!(nil) - case .responded: - flutterResult!(nil) - case .deleted: - flutterResult!(nil) - @unknown default: - flutterResult!(nil) + 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 - } + private func getTopMostViewController() -> UIViewController { + var topController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController + while ((topController?.presentedViewController) != nil) { + topController = topController?.presentedViewController + } - return topController! - } + return topController! + } private func finishWithUnauthorizedError(result: @escaping FlutterResult) { result(FlutterError(code:self.unauthorizedErrorCode, message: self.unauthorizedErrorMessage, details: nil)) @@ -911,30 +1086,31 @@ public class SwiftDeviceCalendarPlugin: NSObject, FlutterPlugin, EKEventViewDele self.finishWithUnauthorizedError(result: result) } - private func requestPermissions(completion: @escaping (Bool) -> Void) { + private func requestPermissions(_ completion: @escaping (Bool) -> Void) { if hasEventPermissions() { completion(true) return } - eventStore.requestAccess(to: .event, completion: { - (accessGranted: Bool, _: Error?) in - completion(accessGranted) - }) + if #available(iOS 17, *) { + eventStore.requestFullAccessToEvents { + (accessGranted: Bool, _: Error?) in + completion(accessGranted) + } + } else { + eventStore.requestAccess(to: .event, completion: { + (accessGranted: Bool, _: Error?) in + completion(accessGranted) + }) + } } private func hasEventPermissions() -> Bool { let status = EKEventStore.authorizationStatus(for: .event) - return status == EKAuthorizationStatus.authorized - } - - private func requestPermissions(_ result: @escaping FlutterResult) { - if hasEventPermissions() { - result(true) + if #available(iOS 17, *) { + return status == EKAuthorizationStatus.fullAccess + } else { + return status == EKAuthorizationStatus.authorized } - eventStore.requestAccess(to: .event, completion: { - (accessGranted: Bool, _: Error?) in - result(accessGranted) - }) } } @@ -991,4 +1167,5 @@ extension UIColor { return nil } + } diff --git a/lib/device_calendar.dart b/lib/device_calendar.dart index 4811bb72..ab9f78d6 100644 --- a/lib/device_calendar.dart +++ b/lib/device_calendar.dart @@ -1,14 +1,16 @@ library device_calendar; export 'src/common/calendar_enums.dart'; -export 'src/common/recurrence_frequency.dart'; export 'src/models/attendee.dart'; export 'src/models/calendar.dart'; export 'src/models/result.dart'; export 'src/models/reminder.dart'; export 'src/models/event.dart'; +export 'src/models/event_color.dart'; +export 'src/models/calendar_color.dart'; export 'src/models/retrieve_events_params.dart'; -export 'src/models/recurrence_rule.dart'; +export 'package:rrule/rrule.dart'; +export 'package:rrule/src/frequency.dart'; export 'src/models/platform_specifics/ios/attendee_details.dart'; export 'src/models/platform_specifics/ios/attendance_status.dart'; export 'src/models/platform_specifics/android/attendee_details.dart'; diff --git a/lib/src/common/calendar_enums.dart b/lib/src/common/calendar_enums.dart index 2e7cd246..aa77ec02 100644 --- a/lib/src/common/calendar_enums.dart +++ b/lib/src/common/calendar_enums.dart @@ -86,6 +86,7 @@ extension DayOfWeekExtension on DayOfWeek { } int get value => _value(this); + String get enumToString => _enumToString(this); } @@ -122,6 +123,7 @@ extension DaysOfWeekGroupExtension on DayOfWeekGroup { } List get getDays => _getDays(this); + String get enumToString => _enumToString(this); } @@ -162,6 +164,7 @@ extension MonthOfYearExtension on MonthOfYear { } int get value => _value(this); + String get enumToString => _enumToString(this); } @@ -188,6 +191,7 @@ extension WeekNumberExtension on WeekNumber { } int get value => _value(this); + String get enumToString => _enumToString(this); } @@ -262,7 +266,9 @@ extension IntExtensions on int { } DayOfWeek get getDayOfWeekEnumValue => _getDayOfWeekEnumValue(this); + MonthOfYear get getMonthOfYearEnumValue => _getMonthOfYearEnumValue(this); + WeekNumber get getWeekNumberEnumValue => _getWeekNumberEnumValue(this); } @@ -306,4 +312,4 @@ extension EventStatusExtensions on EventStatus { } String get enumToString => _enumToString(this); -} \ No newline at end of file +} diff --git a/lib/src/common/channel_constants.dart b/lib/src/common/channel_constants.dart index 2eef3d2d..b56f8adf 100644 --- a/lib/src/common/channel_constants.dart +++ b/lib/src/common/channel_constants.dart @@ -11,6 +11,9 @@ class ChannelConstants { static const String methodNameCreateCalendar = 'createCalendar'; static const String methodNameDeleteCalendar = 'deleteCalendar'; static const String methodNameShowiOSEventModal = 'showiOSEventModal'; + static const String methodNameRetrieveEventColors = 'retrieveEventColors'; + static const String methodNameRetrieveCalendarColors = 'retrieveCalendarColors'; + static const String methodNameUpdateCalendarColor = 'updateCalendarColor'; static const String parameterNameCalendarId = 'calendarId'; static const String parameterNameStartDate = 'startDate'; @@ -22,5 +25,7 @@ class ChannelConstants { static const String parameterNameFollowingInstances = 'followingInstances'; static const String parameterNameCalendarName = 'calendarName'; static const String parameterNameCalendarColor = 'calendarColor'; + static const String parameterNameCalendarColorKey = 'calendarColorKey'; static const String parameterNameLocalAccountName = 'localAccountName'; + static const String parameterAccountName = "accountName"; } diff --git a/lib/src/common/recurrence_frequency.dart b/lib/src/common/recurrence_frequency.dart deleted file mode 100644 index 176cf3de..00000000 --- a/lib/src/common/recurrence_frequency.dart +++ /dev/null @@ -1,6 +0,0 @@ -enum RecurrenceFrequency { - Daily, - Weekly, - Monthly, - Yearly, -} diff --git a/lib/src/device_calendar.dart b/lib/src/device_calendar.dart index 728024c1..6fc778e9 100644 --- a/lib/src/device_calendar.dart +++ b/lib/src/device_calendar.dart @@ -2,19 +2,14 @@ import 'dart:collection'; import 'dart:convert'; import 'dart:io'; +import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:sprintf/sprintf.dart'; import 'package:timezone/data/latest.dart' as tz; -import 'package:timezone/timezone.dart'; 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 { @@ -80,42 +75,48 @@ class DeviceCalendarPlugin { String? calendarId, RetrieveEventsParams? retrieveEventsParams, ) async { - return _invokeChannelMethod( - ChannelConstants.methodNameRetrieveEvents, - assertParameters: (result) { - _validateCalendarIdParameter( - result, - calendarId, - ); - - _assertParameter( - result, - !((retrieveEventsParams?.eventIds?.isEmpty ?? true) && - ((retrieveEventsParams?.startDate == null || - retrieveEventsParams?.endDate == null) || - (retrieveEventsParams?.startDate != null && - retrieveEventsParams?.endDate != null && - (retrieveEventsParams != null && - retrieveEventsParams.startDate! - .isAfter(retrieveEventsParams.endDate!))))), - ErrorCodes.invalidArguments, - ErrorMessages.invalidRetrieveEventsParams, - ); - }, - arguments: () => { - ChannelConstants.parameterNameCalendarId: calendarId, - ChannelConstants.parameterNameStartDate: - retrieveEventsParams?.startDate?.millisecondsSinceEpoch, - ChannelConstants.parameterNameEndDate: - retrieveEventsParams?.endDate?.millisecondsSinceEpoch, - ChannelConstants.parameterNameEventIds: retrieveEventsParams?.eventIds, - }, - evaluateResponse: (rawData) => UnmodifiableListView( + return _invokeChannelMethod(ChannelConstants.methodNameRetrieveEvents, + assertParameters: (result) { + _validateCalendarIdParameter( + result, + calendarId, + ); + + _assertParameter( + result, + !((retrieveEventsParams?.eventIds?.isEmpty ?? true) && + ((retrieveEventsParams?.startDate == null || + retrieveEventsParams?.endDate == null) || + (retrieveEventsParams?.startDate != null && + retrieveEventsParams?.endDate != null && + (retrieveEventsParams != null && + retrieveEventsParams.startDate! + .isAfter(retrieveEventsParams.endDate!))))), + ErrorCodes.invalidArguments, + ErrorMessages.invalidRetrieveEventsParams, + ); + }, + arguments: () => { + ChannelConstants.parameterNameCalendarId: calendarId, + ChannelConstants.parameterNameStartDate: + retrieveEventsParams?.startDate?.millisecondsSinceEpoch, + ChannelConstants.parameterNameEndDate: + retrieveEventsParams?.endDate?.millisecondsSinceEpoch, + ChannelConstants.parameterNameEventIds: + retrieveEventsParams?.eventIds, + }, + /*evaluateResponse: (rawData) => UnmodifiableListView( json .decode(rawData) .map((decodedEvent) => Event.fromJson(decodedEvent)), - ), - ); + ),*/ + evaluateResponse: (rawData) => UnmodifiableListView( + json.decode(rawData).map((decodedEvent) { + // debugPrint( + // "JSON_RRULE: ${decodedEvent['recurrenceRule']}, ${(decodedEvent['recurrenceRule']['byday'])}"); + return Event.fromJson(decodedEvent); + }), + )); } /// Deletes an event from a calendar. For a recurring event, this will delete all instances of it.\ @@ -337,6 +338,80 @@ class DeviceCalendarPlugin { ); } + Future?> retrieveEventColors(Calendar calendar) async { + if (!Platform.isAndroid) { + return null; + } + final accountName = calendar.accountName; + if (accountName == null) { + return []; + } + final dynamic colors = await _invokeChannelMethod( + ChannelConstants.methodNameRetrieveEventColors, + arguments: () => { + ChannelConstants.parameterAccountName: accountName, + }, + ); + return (colors.data as List) + .cast() + .map((color) => EventColor(color[0], color[1])) + .toList(); + } + + /// Retrieves available colors for Google Calendars. + /// + /// For non-Google calendars, an empty list is returned. Use the `color` parameter in [updateCalendarColor] for these. + /// + /// [calendar] The calendar to retrieve colors for. + /// + /// Returns a List with available colors for Google Calendars or an empty list for others. + Future> retrieveCalendarColors(Calendar calendar) async { + if (!Platform.isAndroid) { + return []; + } + final accountName = calendar.accountName; + if (accountName == null) { + return []; + } + final dynamic colors = await _invokeChannelMethod( + ChannelConstants.methodNameRetrieveCalendarColors, + arguments: () => { + ChannelConstants.parameterAccountName: accountName, + }, + ); + return (colors.data as List) + .cast() + .map((color) => CalendarColor(color[0], color[1])) + .toList(); + } + + /// Updates the color of a calendar using Google Calendar colors or platform-specific colors. + /// [calendar] The calendar to update. Must have a non-null `id`. + /// [calendarColor] Required for Google Calendars where [retrieveCalendarColors] is not empty. + /// [color] Required for locale or iOS Calendars where [retrieveCalendarColors] is empty. + /// + /// Returns `true` if the update was successful, otherwise `false`. + Future updateCalendarColor(Calendar calendar, + {CalendarColor? calendarColor, Color? color}) async { + final calendarId = calendar.id; + if (calendarId == null || color == null && calendarColor == null) { + return false; + } + final result = await _invokeChannelMethod( + ChannelConstants.methodNameUpdateCalendarColor, + arguments: () => { + ChannelConstants.parameterNameCalendarId: Platform.isAndroid ? int.tryParse(calendarId) : calendarId, + ChannelConstants.parameterNameCalendarColorKey: calendarColor?.colorKey, + ChannelConstants.parameterNameCalendarColor: color?.value, + }, + ); + final success = (result.data as bool?) ?? false; + if (success) { + calendar.color = color?.value ?? calendarColor?.color; + } + return success; + } + Future> _invokeChannelMethod( String channelMethodName, { Function(Result)? assertParameters, @@ -363,8 +438,15 @@ class DeviceCalendarPlugin { } else { result.data = rawData; } - } catch (e) { - _parsePlatformExceptionAndUpdateResult(e as Exception?, result); + } catch (e, s) { + if (e is ArgumentError) { + debugPrint( + "INVOKE_CHANNEL_METHOD_ERROR! Name: ${e.name}, InvalidValue: ${e.invalidValue}, Message: ${e.message}, ${e.toString()}"); + } else if (e is PlatformException) { + debugPrint('INVOKE_CHANNEL_METHOD_ERROR: $e\n$s'); + } else { + _parsePlatformExceptionAndUpdateResult(e as Exception?, result); + } } return result; @@ -382,22 +464,20 @@ class DeviceCalendarPlugin { return; } - print(exception); + debugPrint('$exception'); if (exception is PlatformException) { result.errors.add( ResultError( ErrorCodes.platformSpecific, - sprintf(ErrorMessages.unknownDeviceExceptionTemplate, - [exception.code, exception.message]), + '${ErrorMessages.unknownDeviceExceptionTemplate}, Code: ${exception.code}, Exception: ${exception.message}', ), ); } else { result.errors.add( ResultError( ErrorCodes.generic, - sprintf(ErrorMessages.unknownDeviceGenericExceptionTemplate, - [exception.toString()]), + '${ErrorMessages.unknownDeviceGenericExceptionTemplate} ${exception.toString}', ), ); } @@ -409,6 +489,9 @@ class DeviceCalendarPlugin { int errorCode, String errorMessage, ) { + if (result.data != null) { + debugPrint("RESULT of _assertParameter: ${result.data}"); + } if (!predicate) { result.errors.add( ResultError(errorCode, errorMessage), diff --git a/lib/src/models/calendar_color.dart b/lib/src/models/calendar_color.dart new file mode 100644 index 00000000..e63e0be1 --- /dev/null +++ b/lib/src/models/calendar_color.dart @@ -0,0 +1,9 @@ +class CalendarColor { + final int color; + final int colorKey; + + CalendarColor(this.color, this.colorKey); + + @override + String toString() => 'CalendarColor(color: $color, colorKey: $colorKey)'; +} \ No newline at end of file diff --git a/lib/src/models/event.dart b/lib/src/models/event.dart index c7478005..eda68ffa 100644 --- a/lib/src/models/event.dart +++ b/lib/src/models/event.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; +import 'event_color.dart'; import '../../device_calendar.dart'; import '../common/error_messages.dart'; @@ -49,6 +50,12 @@ class Event { /// Indicates if this event is of confirmed, canceled, tentative or none status EventStatus? status; + /// Read-only. Android exclusive. Updatable only using [Event.updateEventColor] with color from [DeviceCalendarPlugin.retrieveEventColors] + int? color; + + /// Read-only. Android exclusive. Updatable only using [Event.updateEventColor] with color from [DeviceCalendarPlugin.retrieveEventColors] + int? colorKey; + ///Note for development: /// ///JSON field names are coded in dart, swift and kotlin to facilitate data exchange. @@ -110,6 +117,8 @@ class Event { calendarId = json['calendarId']; title = json['eventTitle']; description = json['eventDescription']; + color = json['eventColor']; + colorKey = json['eventColorKey']; startTimestamp = json['eventStartDate']; startLocationName = json['eventStartTimeZone']; @@ -170,7 +179,42 @@ class Event { } if (json['recurrenceRule'] != null) { + // debugPrint( + // "EVENT_MODEL: $title; START: $start, END: $end RRULE = ${json['recurrenceRule']}"); + + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'byday'') + if (json['recurrenceRule']['byday'] != null) { + json['recurrenceRule']['byday'] = + json['recurrenceRule']['byday'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'bymonthday'') + if (json['recurrenceRule']['bymonthday'] != null) { + json['recurrenceRule']['bymonthday'] = + json['recurrenceRule']['bymonthday'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'byyearday'') + if (json['recurrenceRule']['byyearday'] != null) { + json['recurrenceRule']['byyearday'] = + json['recurrenceRule']['byyearday'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'byweekno'') + if (json['recurrenceRule']['byweekno'] != null) { + json['recurrenceRule']['byweekno'] = + json['recurrenceRule']['byweekno'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'bymonth'') + if (json['recurrenceRule']['bymonth'] != null) { + json['recurrenceRule']['bymonth'] = + json['recurrenceRule']['bymonth'].cast(); + } + //TODO: If we don't cast it to List, the rrule package throws an error as it detects it as List ('Invalid JSON in 'bysetpos'') + if (json['recurrenceRule']['bysetpos'] != null) { + json['recurrenceRule']['bysetpos'] = + json['recurrenceRule']['bysetpos'].cast(); + } + // debugPrint("EVENT_MODEL: $title; RRULE = ${json['recurrenceRule']}"); recurrenceRule = RecurrenceRule.fromJson(json['recurrenceRule']); + // debugPrint("EVENT_MODEL_recurrenceRule: ${recurrenceRule.toString()}"); } if (json['reminders'] != null) { @@ -202,6 +246,8 @@ class Event { data['eventURL'] = url?.data?.contentText; data['availability'] = availability.enumToString; data['eventStatus'] = status?.enumToString; + data['eventColor'] = color; + data['eventColorKey'] = colorKey; if (attendees != null) { data['attendees'] = attendees?.map((a) => a?.toJson()).toList(); @@ -214,12 +260,13 @@ class Event { if (recurrenceRule != null) { data['recurrenceRule'] = recurrenceRule?.toJson(); + // print("EVENT_TO_JSON_RRULE: ${recurrenceRule?.toJson()}"); } if (reminders != null) { data['reminders'] = reminders?.map((r) => r.toJson()).toList(); } - + // debugPrint("EVENT_TO_JSON: $data"); return data; } @@ -250,6 +297,7 @@ class Event { case 'NONE': return EventStatus.None; } + return null; } bool updateStartLocation(String? newStartLocation) { @@ -273,4 +321,9 @@ class Event { return false; } } -} + + void updateEventColor(EventColor? eventColor) { + color = eventColor?.color; + colorKey = eventColor?.colorKey; + } +} \ No newline at end of file diff --git a/lib/src/models/event_color.dart b/lib/src/models/event_color.dart new file mode 100644 index 00000000..0851bf23 --- /dev/null +++ b/lib/src/models/event_color.dart @@ -0,0 +1,9 @@ +class EventColor { + final int color; + final int colorKey; + + EventColor(this.color, this.colorKey); + + @override + String toString() => 'EventColor(color: $color, colorKey: $colorKey)'; +} \ No newline at end of file diff --git a/lib/src/models/recurrence_rule.dart b/lib/src/models/recurrence_rule.dart deleted file mode 100644 index 02825262..00000000 --- a/lib/src/models/recurrence_rule.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:device_calendar/src/common/calendar_enums.dart'; - -import '../common/error_messages.dart'; -import '../common/recurrence_frequency.dart'; - -class RecurrenceRule { - int? totalOccurrences; - - /// The interval between instances of a recurring event - int? interval; - - /// The date a series of recurring events should end - DateTime? endDate; - - /// The frequency of recurring events - RecurrenceFrequency? recurrenceFrequency; - - /// The days of the week that this event occurs on. Only applicable to recurrence rules with a weekly, monthly or yearly frequency - List? daysOfWeek = []; - - /// A day of the month that this event occurs on. Only applicable to recurrence rules with a monthly or yearly frequency - int? dayOfMonth; - - /// A month of the year that the event occurs on. Only applicable to recurrence rules with a yearly frequency - MonthOfYear? monthOfYear; - - /// Filters which recurrences to include in the recurrence rule’s frequency. Only applicable when _isByDayOfMonth is false - WeekNumber? weekOfMonth; - - final String _totalOccurrencesKey = 'totalOccurrences'; - final String _recurrenceFrequencyKey = 'recurrenceFrequency'; - final String _intervalKey = 'interval'; - final String _endDateKey = 'endDate'; - final String _daysOfWeekKey = 'daysOfWeek'; - final String _dayOfMonthKey = 'dayOfMonth'; - final String _monthOfYearKey = 'monthOfYear'; - final String _weekOfMonthKey = 'weekOfMonth'; - - RecurrenceRule(this.recurrenceFrequency, - {this.totalOccurrences, - this.interval, - this.endDate, - this.daysOfWeek, - this.dayOfMonth, - this.monthOfYear, - this.weekOfMonth}) - : assert(!(endDate != null && totalOccurrences != null), - 'Cannot specify both an end date and total occurrences for a recurring event'); - - RecurrenceRule.fromJson(Map? json) { - if (json == null) { - throw ArgumentError(ErrorMessages.fromJsonMapIsNull); - } - - int? recurrenceFrequencyIndex = json[_recurrenceFrequencyKey]; - if (recurrenceFrequencyIndex == null || - recurrenceFrequencyIndex >= RecurrenceFrequency.values.length) { - throw ArgumentError(ErrorMessages.invalidRecurrencyFrequency); - } - recurrenceFrequency = RecurrenceFrequency.values[recurrenceFrequencyIndex]; - - totalOccurrences = json[_totalOccurrencesKey]; - - interval = json[_intervalKey]; - - int? endDateMillisecondsSinceEpoch = json[_endDateKey]; - if (endDateMillisecondsSinceEpoch != null) { - endDate = - DateTime.fromMillisecondsSinceEpoch(endDateMillisecondsSinceEpoch); - } - - List? daysOfWeekValues = json[_daysOfWeekKey]; - if (daysOfWeekValues != null && daysOfWeekValues is! List) { - daysOfWeek = daysOfWeekValues - .cast() - .map((value) => value.getDayOfWeekEnumValue) - .toList(); - } - - dayOfMonth = json[_dayOfMonthKey]; - monthOfYear = - convertDynamicToInt(json[_monthOfYearKey])?.getMonthOfYearEnumValue; - weekOfMonth = - convertDynamicToInt(json[_weekOfMonthKey])?.getWeekNumberEnumValue; - } - - int? convertDynamicToInt(dynamic value) { - value = value?.toString(); - return value != null ? int.tryParse(value) : null; - } - - Map toJson() { - final data = {}; - - if (totalOccurrences != null) { - data[_totalOccurrencesKey] = totalOccurrences; - } - - if (interval != null) { - data[_intervalKey] = interval; - } - - if (endDate != null) { - data[_endDateKey] = endDate!.millisecondsSinceEpoch; - } - - data[_recurrenceFrequencyKey] = recurrenceFrequency?.index; - - if (daysOfWeek?.isNotEmpty == true) { - data[_daysOfWeekKey] = daysOfWeek!.map((d) => d.value).toList(); - } - - if (monthOfYear != null && - recurrenceFrequency == RecurrenceFrequency.Yearly) { - data[_monthOfYearKey] = monthOfYear!.value; - } - - if (recurrenceFrequency == RecurrenceFrequency.Monthly || - recurrenceFrequency == RecurrenceFrequency.Yearly) { - if (weekOfMonth != null) { - data[_weekOfMonthKey] = weekOfMonth!.value; - } else { - // Days of the month should not be added to the recurrence parameter when WeekOfMonth is used - if (dayOfMonth != null) { - data[_dayOfMonthKey] = dayOfMonth; - } - } - } - - return data; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index ec0732c1..101681fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,22 +1,20 @@ name: device_calendar description: A cross platform plugin for modifying calendars on the user's device. -version: 4.3.0 +version: 4.3.1 homepage: https://github.com/builttoroam/device_calendar/tree/master dependencies: flutter: sdk: flutter collection: ^1.16.0 - sprintf: ^7.0.0 timezone: ^0.9.0 - flutter_native_timezone: ^2.0.0 + rrule: ^0.2.15 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.1 -# The following section is specific to Flutter. flutter: plugin: platforms: @@ -28,4 +26,4 @@ flutter: environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=1.12.13" + flutter: ">=1.20.0" diff --git a/test/device_calendar_test.dart b/test/device_calendar_test.dart index 97c963bb..0d91e738 100644 --- a/test/device_calendar_test.dart +++ b/test/device_calendar_test.dart @@ -12,7 +12,8 @@ void main() { final log = []; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { print('Calling channel method ${methodCall.method}'); log.add(methodCall); @@ -23,7 +24,8 @@ void main() { }); test('HasPermissions_Returns_Successfully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return true; }); @@ -34,7 +36,8 @@ void main() { }); test('RequestPermissions_Returns_Successfully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return true; }); @@ -46,7 +49,8 @@ void main() { test('RetrieveCalendars_Returns_Successfully', () async { const fakeCalendarName = 'fakeCalendarName'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return '[{"id":"1","isReadOnly":false,"name":"$fakeCalendarName"}]'; }); @@ -114,7 +118,8 @@ void main() { test('CreateEvent_Returns_Successfully', () async { const fakeNewEventId = 'fakeNewEventId'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return fakeNewEventId; }); @@ -133,7 +138,8 @@ void main() { test('UpdateEvent_Returns_Successfully', () async { const fakeNewEventId = 'fakeNewEventId'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { final arguments = methodCall.arguments as Map; if (!arguments.containsKey('eventId') || arguments['eventId'] == null) { return null; @@ -196,7 +202,7 @@ void main() { emailAddress: 'test@t.com', role: AttendeeRole.Required, isOrganiser: true); - final recurrence = RecurrenceRule(RecurrenceFrequency.Daily); + final recurrence = RecurrenceRule(frequency: Frequency.daily); final reminder = Reminder(minutes: 10); var event = Event('calendarId', eventId: 'eventId', @@ -211,6 +217,7 @@ void main() { reminders: [reminder], availability: Availability.Busy, status: EventStatus.Confirmed); + event.updateEventColor(EventColor(0xffff00ff, 1)); final stringEvent = event.toJson(); expect(stringEvent, isNotNull); @@ -229,11 +236,13 @@ void main() { expect(newEvent.attendees, isNotNull); expect(newEvent.attendees?.length, equals(1)); expect(newEvent.recurrenceRule, isNotNull); - expect(newEvent.recurrenceRule?.recurrenceFrequency, - equals(event.recurrenceRule?.recurrenceFrequency)); + expect(newEvent.recurrenceRule?.frequency, + equals(event.recurrenceRule?.frequency)); expect(newEvent.reminders, isNotNull); expect(newEvent.reminders?.length, equals(1)); expect(newEvent.availability, equals(event.availability)); expect(newEvent.status, equals(event.status)); + expect(newEvent.color, equals(event.color)); + expect(newEvent.colorKey, equals(event.colorKey)); }); }