├── .github └── workflows │ ├── build-kdoc.yml │ ├── dependent-issues.yml │ └── test-dev.yml ├── .gitignore ├── .idea └── copyright │ ├── GPL.xml │ └── profiles_settings.xml ├── AUTHORS ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── lib ├── build.gradle.kts ├── consumer-rules.pro └── src │ ├── androidTest │ ├── kotlin │ │ └── at │ │ │ └── bitfire │ │ │ └── ical4android │ │ │ ├── AndroidCalendarTest.kt │ │ │ ├── AndroidCompatTimeZoneRegistryTest.kt │ │ │ ├── AndroidEventTest.kt │ │ │ ├── AndroidTimeZonesTest.kt │ │ │ ├── AospTest.kt │ │ │ ├── AttendeeMappingsTest.kt │ │ │ ├── BatchOperationTest.kt │ │ │ ├── Css3ColorTest.kt │ │ │ ├── DmfsStyleProvidersTaskTest.kt │ │ │ ├── DmfsTaskListTest.kt │ │ │ ├── DmfsTaskTest.kt │ │ │ ├── EventTest.kt │ │ │ ├── ICalPreprocessorTest.kt │ │ │ ├── ICalendarTest.kt │ │ │ ├── Ical4jSettingsTest.kt │ │ │ ├── Ical4jTest.kt │ │ │ ├── JtxCollectionTest.kt │ │ │ ├── JtxICalObjectTest.kt │ │ │ ├── LocaleNonWesternDigitsTest.kt │ │ │ ├── TaskTest.kt │ │ │ ├── UnknownPropertyTest.kt │ │ │ ├── impl │ │ │ ├── TestCalendar.kt │ │ │ ├── TestEvent.kt │ │ │ ├── TestJtxCollection.kt │ │ │ ├── TestJtxIcalObject.kt │ │ │ ├── TestTask.kt │ │ │ └── TestTaskList.kt │ │ │ └── util │ │ │ ├── AndroidTimeUtilsTest.kt │ │ │ ├── DateUtilsTest.kt │ │ │ ├── MiscUtilsTest.kt │ │ │ └── TimeApiExtensionsTest.kt │ └── resources │ │ ├── events │ │ ├── all-day-0sec.ics │ │ ├── all-day-10days.ics │ │ ├── all-day-1day.ics │ │ ├── dst-only-vtimezone.ics │ │ ├── event-on-that-day.ics │ │ ├── latin1.ics │ │ ├── multiple.ics │ │ ├── one-event-with-exception-one-without.ics │ │ ├── one-event-with-multiple-exceptions-one-without.ics │ │ ├── outlook1.ics │ │ ├── recurring-only-exception.ics │ │ ├── recurring-with-exception1.ics │ │ ├── two-events-without-exceptions.ics │ │ ├── two-line-description-without-crlf.ics │ │ ├── utf8.ics │ │ └── vienna-evolution.ics │ │ ├── jtx │ │ ├── vjournal │ │ │ ├── all-day.ics │ │ │ ├── default-example-note.ics │ │ │ ├── default-example.ics │ │ │ ├── dst-only-vtimezone.ics │ │ │ ├── journal-on-that-day.ics │ │ │ ├── latin1.ics │ │ │ ├── outlook-theoretical.ics │ │ │ ├── outlook-theoretical2.ics │ │ │ ├── recurring.ics │ │ │ ├── two-events-without-exceptions.ics │ │ │ ├── two-line-description-without-crlf.ics │ │ │ └── utf8.ics │ │ └── vtodo │ │ │ ├── empty-priority.ics │ │ │ ├── latin1.ics │ │ │ ├── most-fields1.ics │ │ │ ├── most-fields2.ics │ │ │ ├── rfc5545-sample1.ics │ │ │ └── utf8.ics │ │ ├── tasks │ │ ├── empty-priority.ics │ │ ├── latin1.ics │ │ ├── most-fields1.ics │ │ ├── most-fields2.ics │ │ ├── rfc5545-sample1.ics │ │ └── utf8.ics │ │ └── tz │ │ ├── Karachi.ics │ │ ├── Mogadishu.ics │ │ └── Vienna.ics │ ├── main │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── at │ │ │ ├── bitfire │ │ │ └── ical4android │ │ │ │ ├── AndroidCalendar.kt │ │ │ │ ├── AndroidCalendarFactory.kt │ │ │ │ ├── AndroidCompatTimeZoneRegistry.kt │ │ │ │ ├── AndroidEvent.kt │ │ │ │ ├── AndroidEventFactory.kt │ │ │ │ ├── AttendeeMappings.kt │ │ │ │ ├── BatchOperation.kt │ │ │ │ ├── CalendarStorageException.kt │ │ │ │ ├── Css3Color.kt │ │ │ │ ├── DmfsTask.kt │ │ │ │ ├── DmfsTaskFactory.kt │ │ │ │ ├── DmfsTaskList.kt │ │ │ │ ├── DmfsTaskListFactory.kt │ │ │ │ ├── Event.kt │ │ │ │ ├── ICalendar.kt │ │ │ │ ├── Ical4jVersion.kt │ │ │ │ ├── InvalidCalendarException.kt │ │ │ │ ├── JtxCollection.kt │ │ │ │ ├── JtxCollectionFactory.kt │ │ │ │ ├── JtxICalObject.kt │ │ │ │ ├── JtxICalObjectFactory.kt │ │ │ │ ├── Task.kt │ │ │ │ ├── TaskProvider.kt │ │ │ │ ├── UnknownProperty.kt │ │ │ │ ├── util │ │ │ │ ├── AndroidTimeUtils.kt │ │ │ │ ├── DateUtils.kt │ │ │ │ ├── MiscUtils.kt │ │ │ │ └── TimeApiExtensions.kt │ │ │ │ └── validation │ │ │ │ ├── EventValidator.kt │ │ │ │ ├── FixInvalidDayOffsetPreprocessor.kt │ │ │ │ ├── FixInvalidUtcOffsetPreprocessor.kt │ │ │ │ ├── ICalPreprocessor.kt │ │ │ │ └── StreamPreprocessor.kt │ │ │ └── techbee │ │ │ └── jtx │ │ │ └── JtxContract.kt │ └── resources │ │ ├── ical4j.properties │ │ └── tz.alias │ └── test │ ├── README.txt │ └── kotlin │ └── at │ └── bitfire │ └── ical4android │ ├── Ical4jServiceLoaderTest.kt │ └── validation │ ├── EventValidatorTest.kt │ ├── FixInvalidDayOffsetPreprocessorTest.kt │ └── FixInvalidUtcOffsetPreprocessorTest.kt ├── opentasks-contract ├── .gitignore └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── org │ └── dmfs │ └── tasks │ └── contract │ ├── TaskContract.java │ └── UriFactory.java └── settings.gradle /.github/workflows/build-kdoc.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish KDoc 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 7 | permissions: 8 | contents: read 9 | pages: write 10 | id-token: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-java@v4 18 | with: 19 | distribution: temurin 20 | java-version: 17 21 | - uses: gradle/actions/setup-gradle@v4 22 | 23 | - name: Build KDoc 24 | run: ./gradlew --no-configuration-cache dokkaHtml 25 | 26 | - uses: actions/upload-pages-artifact@v3 27 | with: 28 | path: lib/build/dokka/html 29 | 30 | deploy: 31 | environment: 32 | name: github-pages 33 | url: ${{ steps.deployment.outputs.page_url }} 34 | runs-on: ubuntu-latest 35 | needs: build 36 | steps: 37 | - name: Deploy to GitHub Pages 38 | id: deployment 39 | uses: actions/deploy-pages@v4 40 | -------------------------------------------------------------------------------- /.github/workflows/dependent-issues.yml: -------------------------------------------------------------------------------- 1 | name: Dependent Issues 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - edited 8 | - closed 9 | - reopened 10 | pull_request_target: 11 | types: 12 | - opened 13 | - edited 14 | - closed 15 | - reopened 16 | # Makes sure we always add status check for PRs. Useful only if 17 | # this action is required to pass before merging. Otherwise, it 18 | # can be removed. 19 | - synchronize 20 | 21 | # Schedule a daily check. Useful if you reference cross-repository 22 | # issues or pull requests. Otherwise, it can be removed. 23 | schedule: 24 | - cron: '12 9 * * *' 25 | 26 | permissions: write-all 27 | 28 | jobs: 29 | check: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: z0al/dependent-issues@v1 33 | env: 34 | # (Required) The token to use to make API calls to GitHub. 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | # (Optional) The token to use to make API calls to GitHub for remote repos. 37 | GITHUB_READ_TOKEN: ${{ secrets.DEPENDENT_ISSUES_READ_TOKEN }} 38 | 39 | with: 40 | # (Optional) The label to use to mark dependent issues 41 | # label: dependent 42 | 43 | # (Optional) Enable checking for dependencies in issues. 44 | # Enable by setting the value to "on". Default "off" 45 | check_issues: on 46 | 47 | # (Optional) A comma-separated list of keywords. Default 48 | # "depends on, blocked by" 49 | keywords: depends on, blocked by 50 | 51 | # (Optional) A custom comment body. It supports `{{ dependencies }}` token. 52 | comment: > 53 | This PR/issue depends on: 54 | 55 | {{ dependencies }} 56 | -------------------------------------------------------------------------------- /.github/workflows/test-dev.yml: -------------------------------------------------------------------------------- 1 | name: Development tests 2 | on: push 3 | jobs: 4 | compile: 5 | name: Compile and cache 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-java@v4 10 | with: 11 | distribution: temurin 12 | java-version: 21 13 | 14 | # See https://community.gradle.org/github-actions/docs/setup-gradle/ for more information 15 | - uses: gradle/actions/setup-gradle@v4 # creates build cache when on main branch 16 | with: 17 | cache-encryption-key: ${{ secrets.gradle_encryption_key }} 18 | gradle-home-cache-cleanup: true # clean up unused files 19 | dependency-graph: generate-and-submit # submit Github Dependency Graph info 20 | 21 | - run: ./gradlew --build-cache --configuration-cache compileDebugSources 22 | 23 | test: 24 | needs: compile 25 | name: Tests without emulator 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: actions/setup-java@v4 30 | with: 31 | distribution: temurin 32 | java-version: 21 33 | - uses: gradle/actions/setup-gradle@v4 34 | with: 35 | cache-encryption-key: ${{ secrets.gradle_encryption_key }} 36 | cache-read-only: true 37 | 38 | - name: Run lint and unit tests 39 | run: ./gradlew --build-cache --configuration-cache lintDebug testDebugUnitTest 40 | 41 | test_on_emulator: 42 | needs: compile 43 | name: Tests with emulator 44 | runs-on: ubuntu-latest 45 | strategy: 46 | matrix: 47 | api-level: [ 31 ] 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: actions/setup-java@v4 51 | with: 52 | distribution: temurin 53 | java-version: 21 54 | - uses: gradle/actions/setup-gradle@v4 55 | with: 56 | cache-encryption-key: ${{ secrets.gradle_encryption_key }} 57 | cache-read-only: true 58 | 59 | - name: Enable KVM group perms 60 | run: | 61 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 62 | sudo udevadm control --reload-rules 63 | sudo udevadm trigger --name-match=kvm 64 | 65 | - name: Cache AVD and APKs 66 | uses: actions/cache@v4 67 | id: avd-cache 68 | with: 69 | path: | 70 | ~/.android/avd/* 71 | ~/.android/adb* 72 | ~/.apk 73 | key: avd-${{ matrix.api-level }} 74 | 75 | - name: Create AVD and generate snapshot for caching 76 | if: steps.avd-cache.outputs.cache-hit != 'true' 77 | uses: reactivecircus/android-emulator-runner@v2 78 | with: 79 | api-level: ${{ matrix.api-level }} 80 | arch: x86_64 81 | force-avd-creation: false 82 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 83 | disable-animations: false 84 | script: echo "Generated AVD snapshot for caching." 85 | 86 | - name: Install task apps and run tests 87 | uses: reactivecircus/android-emulator-runner@v2 88 | env: 89 | version_at_techbee_jtx: v2.9.0 90 | version_org_tasks: 131104 91 | version_org_dmfs_tasks: 82200 92 | with: 93 | api-level: ${{ matrix.api-level }} 94 | arch: x86_64 95 | force-avd-creation: false 96 | emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 97 | disable-animations: true 98 | script: | 99 | mkdir .apk && cd .apk 100 | (wget -cq -O org.dmfs.tasks.apk https://f-droid.org/repo/org.dmfs.tasks_${{ env.version_org_dmfs_tasks }}.apk || wget -cq -O org.dmfs.tasks.apk https://f-droid.org/archive/org.dmfs.tasks_${{ env.version_org_dmfs_tasks }}.apk) && adb install org.dmfs.tasks.apk 101 | (wget -cq -O org.tasks.apk https://f-droid.org/repo/org.tasks_${{ env.version_org_tasks }}.apk || wget -cq -O org.tasks.apk https://f-droid.org/archive/org.tasks_${{ env.version_org_tasks }}.apk) && adb install org.tasks.apk 102 | (wget -cq -O at.techbee.jtx.apk https://github.com/TechbeeAT/jtxBoard/releases/download/${{ env.version_at_techbee_jtx }}/jtxBoard-${{ env.version_at_techbee_jtx }}.apk) && adb install at.techbee.jtx.apk 103 | cd .. 104 | ./gradlew --build-cache --configuration-cache connectedCheck -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.FlakyTest 105 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Android ### 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the Dalvik VM 9 | *.dex 10 | 11 | # Java/Kotlin files 12 | *.class 13 | .kotlin/ 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | 19 | # Gradle files 20 | .gradle/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | 33 | ### Intellij ### 34 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 35 | 36 | *.iml 37 | 38 | ## Directory-based project format: 39 | .idea/ 40 | # if you remove the above rule, at least ignore the following: 41 | 42 | # User-specific stuff: 43 | # .idea/workspace.xml 44 | # .idea/tasks.xml 45 | # .idea/dictionaries 46 | 47 | # Sensitive or high-churn files: 48 | # .idea/dataSources.ids 49 | # .idea/dataSources.xml 50 | # .idea/sqlDataSources.xml 51 | # .idea/dynamic.xml 52 | # .idea/uiDesigner.xml 53 | 54 | # Gradle: 55 | # .idea/gradle.xml 56 | # .idea/libraries 57 | 58 | # Mongo Explorer plugin: 59 | # .idea/mongoSettings.xml 60 | 61 | ## File-based project format: 62 | *.ipr 63 | *.iws 64 | 65 | ## Plugin-specific files: 66 | 67 | # IntelliJ 68 | out/ 69 | 70 | # mpeltonen/sbt-idea plugin 71 | .idea_modules/ 72 | 73 | # JIRA plugin 74 | atlassian-ide-plugin.xml 75 | 76 | # Crashlytics plugin (for Android Studio and IntelliJ) 77 | com_crashlytics_export_strings.xml 78 | crashlytics.properties 79 | crashlytics-build.properties 80 | 81 | # Ignore Gradle GUI config 82 | gradle-app.setting 83 | 84 | ### external libs ###a 85 | .attach* 86 | .svn 87 | -------------------------------------------------------------------------------- /.idea/copyright/GPL.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | You can view the list of people who have contributed to the code base in the version control history: 2 | https://github.com/bitfireAT/ical4android/graphs/contributors 3 | 4 | Every contribution is welcome. There are many other forms of contributing besides writing code! 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | > [!IMPORTANT] 3 | > This library has been merged into / superseded by [bitfireAT/synctools](https://github.com/bitfireAT/synctools). 4 | 5 | --- 6 | 7 | [![Development tests](https://github.com/bitfireAT/ical4android/actions/workflows/test-dev.yml/badge.svg)](https://github.com/bitfireAT/ical4android/actions/workflows/test-dev.yml) 8 | [![Documentation](https://img.shields.io/badge/documentation-kdoc-brightgreen)](https://bitfireat.github.io/ical4android/) 9 | 10 | 11 | # ical4android 12 | 13 | ical4android is a library for Android that brings together iCalendar and Android. 14 | It's a framework for 15 | 16 | * parsing and generating iCalendar resources (using [ical4j](https://github.com/ical4j/ical4j)) 17 | from/into data classes that are compatible with the Android Calendar Provider and 18 | third-party task providers, 19 | * accessing the Android Calendar Provider (and third-party task providers) over a unified API. 20 | 21 | It has been primarily developed for: 22 | 23 | * [DAVx⁵](https://www.davx5.com) 24 | * [ICSx⁵](https://icsx5.bitfire.at) 25 | 26 | and is currently used as git submodule. 27 | 28 | Generated KDoc: https://bitfireat.github.io/ical4android/ 29 | 30 | For questions, suggestions etc. use [Github discussions](https://github.com/bitfireAT/ical4android/discussions). 31 | We're happy about contributions! In case of bigger changes, please let us know in the discussions before. 32 | Then make the changes in your own repository and send a pull request. 33 | 34 | _This software is not affiliated to, nor has it been authorized, sponsored or otherwise approved 35 | by Google LLC. Android is a trademark of Google LLC._ 36 | 37 | 38 | ## How to use 39 | 40 | 1. Add the [jitpack.io](https://jitpack.io) repository to your project's level `build.gradle`: 41 | ```groovy 42 | allprojects { 43 | repositories { 44 | // ... more repos 45 | maven { url "https://jitpack.io" } 46 | } 47 | } 48 | ``` 49 | or if you are using `settings.gradle`: 50 | ```groovy 51 | dependencyResolutionManagement { 52 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 53 | repositories { 54 | // ... more repos 55 | maven { url "https://jitpack.io" } 56 | } 57 | } 58 | ``` 59 | 2. Add the dependency to your module's `build.gradle` file: 60 | ```groovy 61 | dependencies { 62 | implementation 'com.github.bitfireAT:ical4android:' 63 | } 64 | ``` 65 | 66 | To view the available gradle tasks for the library: `./gradlew ical4android:tasks` 67 | (the `ical4android` module is defined in `settings.gradle`). 68 | 69 | 70 | ## Contact 71 | 72 | ``` 73 | bitfire web engineering GmbH 74 | Florastraße 27 75 | 2540 Bad Vöslau, AUSTRIA 76 | ``` 77 | 78 | 79 | ## License 80 | 81 | Copyright (C) Ricki Hirner and [contributors](https://github.com/bitfireAT/ical4android/graphs/contributors). 82 | 83 | This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome 84 | to redistribute it under the conditions of the [GNU GPL v3](https://www.gnu.org/licenses/gpl-3.0.html). 85 | 86 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. 3 | **************************************************************************************************/ 4 | 5 | plugins { 6 | alias(libs.plugins.android.library) apply false 7 | alias(libs.plugins.kotlin.android) apply false 8 | alias(libs.plugins.dokka) apply false 9 | } 10 | 11 | group = "at.bitfire" 12 | version = System.getenv("GIT_COMMIT") 13 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # [https://developer.android.com/build/optimize-your-build#optimize] 2 | org.gradle.daemon=true 3 | org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g 4 | org.gradle.parallel=true 5 | 6 | # configuration cache [https://developer.android.com/build/optimize-your-build#use-the-configuration-cache-experimental] 7 | org.gradle.unsafe.configuration-cache=true 8 | org.gradle.unsafe.configuration-cache-problems=warn 9 | 10 | # https://docs.gradle.org/current/userguide/build_cache.html 11 | org.gradle.caching=true 12 | 13 | # Android 14 | android.useAndroidX=true 15 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.9.2" 3 | android-desugar = "2.1.5" 4 | androidx-core = "1.16.0" 5 | androidx-test-rules = "1.6.1" 6 | androidx-test-runner = "1.6.2" 7 | dokka = "1.9.20" 8 | # noinspection GradleDependency 9 | ical4j = "3.2.19" # final version; update to 4.x will require much work 10 | junit = "4.13.2" 11 | kotlin = "2.1.20" 12 | mockk = "1.14.2" 13 | slf4j = "2.0.17" 14 | 15 | [libraries] 16 | android-desugaring = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar" } 17 | androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } 18 | androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } 19 | androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } 20 | ical4j = { module = "org.mnode.ical4j:ical4j", version.ref = "ical4j" } 21 | junit = { module = "junit:junit", version.ref = "junit" } 22 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 23 | mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } 24 | slf4j-jdk = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" } 25 | 26 | [plugins] 27 | android-library = { id = "com.android.library", version.ref = "agp" } 28 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 29 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } 30 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfireAT/ical4android/01bce4a8ee8395402a53c06263839ce3afe97e2b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk21 3 | before_install: 4 | - sdk install java 21.0.4-tem 5 | - sdk use java 21.0.4-tem -------------------------------------------------------------------------------- /lib/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. 3 | **************************************************************************************************/ 4 | 5 | plugins { 6 | alias(libs.plugins.android.library) 7 | alias(libs.plugins.kotlin.android) 8 | alias(libs.plugins.dokka) 9 | `maven-publish` 10 | } 11 | 12 | android { 13 | compileSdk = 35 14 | 15 | namespace = "at.bitfire.ical4android" 16 | 17 | defaultConfig { 18 | minSdk = 23 // Android 6 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | 22 | buildConfigField("String", "version_ical4j", "\"${libs.versions.ical4j.get()}\"") 23 | 24 | aarMetadata { 25 | minCompileSdk = 29 26 | } 27 | 28 | // These ProGuard/R8 rules will be included in the final APK. 29 | consumerProguardFiles("consumer-rules.pro") 30 | } 31 | 32 | compileOptions { 33 | // ical4j >= 3.x uses the Java 8 Time API 34 | isCoreLibraryDesugaringEnabled = true 35 | } 36 | kotlin { 37 | jvmToolchain(21) 38 | } 39 | 40 | buildFeatures.buildConfig = true 41 | 42 | sourceSets["main"].apply { 43 | kotlin { 44 | srcDir("${projectDir}/src/main/kotlin") 45 | } 46 | java { 47 | srcDir("${rootDir}/opentasks-contract/src/main/java") 48 | } 49 | } 50 | 51 | packaging { 52 | resources { 53 | excludes += listOf("META-INF/DEPENDENCIES", "META-INF/LICENSE", "META-INF/*.md") 54 | } 55 | } 56 | 57 | buildTypes { 58 | release { 59 | // Android libraries shouldn't be minified: 60 | // https://developer.android.com/studio/projects/android-library#Considerations 61 | isMinifyEnabled = false 62 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt")) 63 | } 64 | } 65 | 66 | lint { 67 | disable += listOf("AllowBackup", "InvalidPackage") 68 | } 69 | 70 | publishing { 71 | // Configure publish variant 72 | singleVariant("release") { 73 | withSourcesJar() 74 | } 75 | } 76 | } 77 | 78 | publishing { 79 | // Configure publishing data 80 | publications { 81 | register("release", MavenPublication::class.java) { 82 | groupId = "com.github.bitfireAT" 83 | artifactId = "ical4android" 84 | version = System.getenv("GIT_COMMIT") 85 | 86 | afterEvaluate { 87 | from(components["release"]) 88 | } 89 | } 90 | } 91 | } 92 | 93 | dependencies { 94 | implementation(libs.kotlin.stdlib) 95 | coreLibraryDesugaring(libs.android.desugaring) 96 | 97 | implementation(libs.androidx.core) 98 | api(libs.ical4j) 99 | implementation(libs.slf4j.jdk) // ical4j uses slf4j, this module uses java.util.Logger 100 | 101 | androidTestImplementation(libs.androidx.test.rules) 102 | androidTestImplementation(libs.androidx.test.runner) 103 | androidTestImplementation(libs.mockk.android) 104 | testImplementation(libs.junit) 105 | } -------------------------------------------------------------------------------- /lib/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | 2 | # keep all iCalendar properties/parameters (referenced over ServiceLoader) 3 | -keep class net.fortuna.ical4j.** { *; } 4 | 5 | # don't warn when these are missing 6 | -dontwarn com.github.erosb.jsonsKema.** 7 | -dontwarn groovy.** 8 | -dontwarn java.beans.Transient 9 | -dontwarn javax.cache.** 10 | -dontwarn org.codehaus.groovy.** 11 | -dontwarn org.jparsec.** 12 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCalendarTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.Manifest 10 | import android.accounts.Account 11 | import android.content.ContentProviderClient 12 | import android.content.ContentUris 13 | import android.content.ContentValues 14 | import android.provider.CalendarContract 15 | import android.provider.CalendarContract.Calendars 16 | import android.provider.CalendarContract.Colors 17 | import androidx.test.platform.app.InstrumentationRegistry 18 | import androidx.test.rule.GrantPermissionRule 19 | import at.bitfire.ical4android.impl.TestCalendar 20 | import at.bitfire.ical4android.impl.TestEvent 21 | import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter 22 | import at.bitfire.ical4android.util.MiscUtils.closeCompat 23 | import net.fortuna.ical4j.model.property.DtEnd 24 | import net.fortuna.ical4j.model.property.DtStart 25 | import org.junit.AfterClass 26 | import org.junit.Assert.assertEquals 27 | import org.junit.Assert.assertNotNull 28 | import org.junit.Assert.assertTrue 29 | import org.junit.Before 30 | import org.junit.BeforeClass 31 | import org.junit.ClassRule 32 | import org.junit.Test 33 | 34 | class AndroidCalendarTest { 35 | 36 | companion object { 37 | 38 | @JvmField 39 | @ClassRule 40 | val permissionRule = GrantPermissionRule.grant( 41 | Manifest.permission.READ_CALENDAR, 42 | Manifest.permission.WRITE_CALENDAR 43 | ) 44 | 45 | lateinit var provider: ContentProviderClient 46 | 47 | @BeforeClass 48 | @JvmStatic 49 | fun connectProvider() { 50 | provider = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! 51 | } 52 | 53 | @AfterClass 54 | @JvmStatic 55 | fun closeProvider() { 56 | provider.closeCompat() 57 | } 58 | 59 | } 60 | 61 | private val testAccount = Account("ical4android.AndroidCalendarTest", CalendarContract.ACCOUNT_TYPE_LOCAL) 62 | 63 | @Before 64 | fun prepare() { 65 | // make sure there are no colors for testAccount 66 | AndroidCalendar.removeColors(provider, testAccount) 67 | assertEquals(0, countColors(testAccount)) 68 | } 69 | 70 | 71 | @Test 72 | fun testManageCalendars() { 73 | // create calendar 74 | val info = ContentValues() 75 | info.put(Calendars.NAME, "TestCalendar") 76 | info.put(Calendars.CALENDAR_DISPLAY_NAME, "ical4android Test Calendar") 77 | info.put(Calendars.VISIBLE, 0) 78 | info.put(Calendars.SYNC_EVENTS, 0) 79 | val uri = AndroidCalendar.create(testAccount, provider, info) 80 | assertNotNull(uri) 81 | 82 | // query calendar 83 | val calendar = AndroidCalendar.findByID(testAccount, provider, TestCalendar.Factory, ContentUris.parseId(uri)) 84 | assertNotNull(calendar) 85 | 86 | // delete calendar 87 | assertTrue(calendar.delete()) 88 | } 89 | 90 | 91 | @Test 92 | fun testInsertColors() { 93 | AndroidCalendar.insertColors(provider, testAccount) 94 | assertEquals(Css3Color.values().size, countColors(testAccount)) 95 | } 96 | 97 | @Test 98 | fun testInsertColors_AlreadyThere() { 99 | AndroidCalendar.insertColors(provider, testAccount) 100 | AndroidCalendar.insertColors(provider, testAccount) 101 | assertEquals(Css3Color.values().size, countColors(testAccount)) 102 | } 103 | 104 | @Test 105 | fun testRemoveColors() { 106 | AndroidCalendar.insertColors(provider, testAccount) 107 | 108 | // insert an event with that color 109 | val cal = TestCalendar.findOrCreate(testAccount, provider) 110 | try { 111 | // add event with color 112 | TestEvent(cal, Event().apply { 113 | dtStart = DtStart("20210314T204200Z") 114 | dtEnd = DtEnd("20210314T204230Z") 115 | color = Css3Color.limegreen 116 | summary = "Test event with color" 117 | }).add() 118 | 119 | AndroidCalendar.removeColors(provider, testAccount) 120 | assertEquals(0, countColors(testAccount)) 121 | } finally { 122 | cal.delete() 123 | } 124 | } 125 | 126 | private fun countColors(account: Account): Int { 127 | val uri = Colors.CONTENT_URI.asSyncAdapter(account) 128 | provider.query(uri, null, null, null, null)!!.use { cursor -> 129 | cursor.moveToNext() 130 | return cursor.count 131 | } 132 | } 133 | 134 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistryTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory 10 | import net.fortuna.ical4j.model.TimeZone 11 | import net.fortuna.ical4j.model.TimeZoneRegistry 12 | import org.junit.Assert.assertEquals 13 | import org.junit.Assert.assertFalse 14 | import org.junit.Assert.assertNull 15 | import org.junit.Assume 16 | import org.junit.Before 17 | import org.junit.Test 18 | import java.time.ZoneId 19 | import java.time.zone.ZoneRulesException 20 | 21 | class AndroidCompatTimeZoneRegistryTest { 22 | 23 | lateinit var ical4jRegistry: TimeZoneRegistry 24 | lateinit var registry: AndroidCompatTimeZoneRegistry 25 | 26 | private val systemKnowsKyiv = 27 | try { 28 | ZoneId.of("Europe/Kyiv") 29 | true 30 | } catch (e: ZoneRulesException) { 31 | false 32 | } 33 | 34 | @Before 35 | fun createRegistry() { 36 | ical4jRegistry = DefaultTimeZoneRegistryFactory().createRegistry() 37 | registry = AndroidCompatTimeZoneRegistry.Factory().createRegistry() 38 | } 39 | 40 | 41 | @Test 42 | fun getTimeZone_Existing() { 43 | assertEquals( 44 | ical4jRegistry.getTimeZone("Europe/Vienna"), 45 | registry.getTimeZone("Europe/Vienna") 46 | ) 47 | } 48 | 49 | @Test 50 | fun getTimeZone_Existing_ButNotInIcal4j() { 51 | val reg = AndroidCompatTimeZoneRegistry(object: TimeZoneRegistry { 52 | override fun register(timezone: TimeZone?) = throw NotImplementedError() 53 | override fun register(timezone: TimeZone?, update: Boolean) = throw NotImplementedError() 54 | override fun clear() = throw NotImplementedError() 55 | override fun getTimeZone(id: String?) = null 56 | 57 | }) 58 | assertNull(reg.getTimeZone("Europe/Berlin")) 59 | } 60 | 61 | @Test 62 | fun getTimeZone_Existing_Kiev() { 63 | Assume.assumeFalse(systemKnowsKyiv) 64 | val tz = registry.getTimeZone("Europe/Kiev") 65 | assertFalse(tz === ical4jRegistry.getTimeZone("Europe/Kiev")) // we have made a copy 66 | assertEquals("Europe/Kiev", tz?.id) 67 | assertEquals("Europe/Kiev", tz?.vTimeZone?.timeZoneId?.value) 68 | } 69 | 70 | @Test 71 | fun getTimeZone_Existing_Kyiv() { 72 | Assume.assumeFalse(systemKnowsKyiv) 73 | 74 | /* Unfortunately, AndroidCompatTimeZoneRegistry can't rewrite to Europy/Kyiv to anything because 75 | it doesn't know a valid Android name for it. */ 76 | assertEquals( 77 | ical4jRegistry.getTimeZone("Europe/Kyiv"), 78 | registry.getTimeZone("Europe/Kyiv") 79 | ) 80 | } 81 | 82 | @Test 83 | fun getTimeZone_Copenhagen_NoBerlin() { 84 | val tz = registry.getTimeZone("Europe/Copenhagen")!! 85 | assertEquals("Europe/Copenhagen", tz.id) 86 | assertFalse(tz.vTimeZone.toString().contains("Berlin")) 87 | } 88 | 89 | @Test 90 | fun getTimeZone_NotExisting() { 91 | assertNull(registry.getTimeZone("Test/NotExisting")) 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/AndroidTimeZonesTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import at.bitfire.ical4android.util.DateUtils 10 | import org.junit.Assert 11 | import org.junit.Assert.assertNotNull 12 | import org.junit.Test 13 | import java.time.ZoneId 14 | import java.time.format.TextStyle 15 | import java.util.Locale 16 | 17 | class AndroidTimeZonesTest { 18 | 19 | @Test 20 | fun testLoadSystemTimezones() { 21 | for (id in ZoneId.getAvailableZoneIds()) { 22 | val name = ZoneId.of(id).getDisplayName(TextStyle.FULL, Locale.US) 23 | val info = try { 24 | DateUtils.ical4jTimeZone(id) 25 | } catch(e: Exception) { 26 | Assert.fail("Invalid system timezone $name ($id)") 27 | } 28 | if (info == null) 29 | assertNotNull("ical4j can't load system timezone $name ($id)", info) 30 | } 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/AospTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.Manifest 10 | import android.accounts.Account 11 | import android.content.ContentUris 12 | import android.content.ContentValues 13 | import android.net.Uri 14 | import android.provider.CalendarContract 15 | import androidx.test.platform.app.InstrumentationRegistry 16 | import androidx.test.rule.GrantPermissionRule 17 | import at.bitfire.ical4android.util.MiscUtils.closeCompat 18 | import org.junit.After 19 | import org.junit.Assert.assertNotNull 20 | import org.junit.Before 21 | import org.junit.Rule 22 | import org.junit.Test 23 | 24 | class AospTest { 25 | 26 | @JvmField 27 | @Rule 28 | val permissionRule = GrantPermissionRule.grant( 29 | Manifest.permission.READ_CALENDAR, 30 | Manifest.permission.WRITE_CALENDAR 31 | )!! 32 | 33 | private val testAccount = Account("test@example.com", CalendarContract.ACCOUNT_TYPE_LOCAL) 34 | 35 | private val provider by lazy { 36 | InstrumentationRegistry.getInstrumentation().targetContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! 37 | } 38 | 39 | private lateinit var calendarUri: Uri 40 | 41 | @Before 42 | fun prepare() { 43 | calendarUri = provider.insert( 44 | CalendarContract.Calendars.CONTENT_URI.asSyncAdapter(), ContentValues().apply { 45 | put(CalendarContract.Calendars.ACCOUNT_NAME, testAccount.name) 46 | put(CalendarContract.Calendars.ACCOUNT_TYPE, testAccount.type) 47 | put(CalendarContract.Calendars.NAME, "Test Calendar") 48 | } 49 | )!! 50 | } 51 | 52 | @After 53 | fun shutdown() { 54 | provider.delete(calendarUri, null, null) 55 | provider.closeCompat() 56 | } 57 | 58 | private fun Uri.asSyncAdapter() = 59 | buildUpon() 60 | .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "1") 61 | .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, testAccount.name) 62 | .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, testAccount.type) 63 | .build() 64 | 65 | 66 | @Test 67 | fun testInfiniteRRule() { 68 | assertNotNull(provider.insert(CalendarContract.Events.CONTENT_URI.asSyncAdapter(), ContentValues().apply { 69 | put(CalendarContract.Events.CALENDAR_ID, ContentUris.parseId(calendarUri)) 70 | put(CalendarContract.Events.DTSTART, 1643192678000) 71 | put(CalendarContract.Events.DURATION, "P1H") 72 | put(CalendarContract.Events.RRULE, "FREQ=YEARLY") 73 | put(CalendarContract.Events.TITLE, "Test event with infinite RRULE") 74 | })) 75 | } 76 | 77 | @Test(expected = AssertionError::class) 78 | fun testInfiniteRRulePlusRDate() { 79 | // see https://issuetracker.google.com/issues/37116691 80 | 81 | assertNotNull(provider.insert(CalendarContract.Events.CONTENT_URI.asSyncAdapter(), ContentValues().apply { 82 | put(CalendarContract.Events.CALENDAR_ID, ContentUris.parseId(calendarUri)) 83 | put(CalendarContract.Events.DTSTART, 1643192678000) 84 | put(CalendarContract.Events.DURATION, "PT1H") 85 | put(CalendarContract.Events.RRULE, "FREQ=YEARLY") 86 | put(CalendarContract.Events.RDATE, "20230101T000000Z") 87 | put(CalendarContract.Events.TITLE, "Test event with infinite RRULE and RDATE") 88 | })) 89 | 90 | /** FAILS: 91 | W RecurrenceProcessor: DateException with r=FREQ=YEARLY;WKST=MO rangeStart=135697573414 rangeEnd=9223372036854775807 92 | W CalendarProvider2: Could not calculate last date. 93 | W CalendarProvider2: com.android.calendarcommon2.DateException: No range end provided for a recurrence that has no UNTIL or COUNT. 94 | W CalendarProvider2: at com.android.calendarcommon2.RecurrenceProcessor.expand(RecurrenceProcessor.java:766) 95 | W CalendarProvider2: at com.android.calendarcommon2.RecurrenceProcessor.expand(RecurrenceProcessor.java:661) 96 | W CalendarProvider2: at com.android.calendarcommon2.RecurrenceProcessor.getLastOccurence(RecurrenceProcessor.java:130) 97 | W CalendarProvider2: at com.android.calendarcommon2.RecurrenceProcessor.getLastOccurence(RecurrenceProcessor.java:61) 98 | */ 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/BatchOperationTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.Manifest 10 | import android.accounts.Account 11 | import android.content.ContentProviderClient 12 | import android.content.ContentUris 13 | import android.net.Uri 14 | import android.provider.CalendarContract 15 | import androidx.test.filters.FlakyTest 16 | import androidx.test.platform.app.InstrumentationRegistry 17 | import androidx.test.rule.GrantPermissionRule 18 | import at.bitfire.ical4android.impl.TestCalendar 19 | import at.bitfire.ical4android.impl.TestEvent 20 | import at.bitfire.ical4android.util.MiscUtils.closeCompat 21 | import net.fortuna.ical4j.model.property.Attendee 22 | import net.fortuna.ical4j.model.property.DtEnd 23 | import net.fortuna.ical4j.model.property.DtStart 24 | import org.junit.After 25 | import org.junit.AfterClass 26 | import org.junit.Assert.assertEquals 27 | import org.junit.Assert.assertNotNull 28 | import org.junit.Before 29 | import org.junit.BeforeClass 30 | import org.junit.ClassRule 31 | import org.junit.Test 32 | import java.net.URI 33 | import java.util.Arrays 34 | 35 | class BatchOperationTest { 36 | 37 | companion object { 38 | 39 | @JvmField 40 | @ClassRule 41 | val permissionRule = GrantPermissionRule.grant( 42 | Manifest.permission.READ_CALENDAR, 43 | Manifest.permission.WRITE_CALENDAR 44 | ) 45 | 46 | lateinit var provider: ContentProviderClient 47 | 48 | @BeforeClass 49 | @JvmStatic 50 | fun connectProvider() { 51 | provider = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!! 52 | } 53 | 54 | @AfterClass 55 | @JvmStatic 56 | fun closeProvider() { 57 | provider.closeCompat() 58 | } 59 | 60 | } 61 | 62 | private val testAccount = Account("ical4android@example.com", CalendarContract.ACCOUNT_TYPE_LOCAL) 63 | 64 | private lateinit var calendarUri: Uri 65 | private lateinit var calendar: TestCalendar 66 | 67 | @Before 68 | fun prepare() { 69 | System.gc() 70 | calendar = TestCalendar.findOrCreate(testAccount, provider) 71 | assertNotNull(calendar) 72 | calendarUri = ContentUris.withAppendedId(CalendarContract.Calendars.CONTENT_URI, calendar.id) 73 | } 74 | 75 | @After 76 | fun shutdown() { 77 | calendar.delete() 78 | System.gc() 79 | } 80 | 81 | @Test 82 | fun testTransactionSplitting() { 83 | val event = Event() 84 | event.uid = "sample1@testLargeTransaction" 85 | event.summary = "Large event" 86 | event.dtStart = DtStart("20150502T120000Z") 87 | event.dtEnd = DtEnd("20150502T130000Z") 88 | for (i in 0 until 2000) // 2000 attendees are enough for a transaction split to happen 89 | event.attendees += Attendee(URI("mailto:att$i@example.com")) 90 | val uri = TestEvent(calendar, event).add() 91 | val testEvent = calendar.findById(ContentUris.parseId(uri)) 92 | try { 93 | assertEquals(2000, testEvent.event!!.attendees.size) 94 | } finally { 95 | testEvent.delete() 96 | } 97 | } 98 | 99 | @FlakyTest 100 | @Test 101 | fun testLargeTransactionSplitting() { 102 | // with 4000 attendees, this test has been observed to fail on the CI server docker emulator. 103 | // Too many Binders are sent to SYSTEM (see issue #42). Asking for GC in @Before/@After might help. 104 | val event = Event() 105 | event.uid = "sample1@testLargeTransaction" 106 | event.summary = "Large event" 107 | event.dtStart = DtStart("20150502T120000Z") 108 | event.dtEnd = DtEnd("20150502T130000Z") 109 | for (i in 0 until 4000) 110 | event.attendees += Attendee(URI("mailto:att$i@example.com")) 111 | val uri = TestEvent(calendar, event).add() 112 | val testEvent = calendar.findById(ContentUris.parseId(uri)) 113 | try { 114 | assertEquals(4000, testEvent.event!!.attendees.size) 115 | } finally { 116 | testEvent.delete() 117 | } 118 | } 119 | 120 | @Test(expected = CalendarStorageException::class) 121 | fun testLargeTransactionSingleRow() { 122 | val event = Event() 123 | event.uid = "sample1@testLargeTransaction" 124 | event.dtStart = DtStart("20150502T120000Z") 125 | event.dtEnd = DtEnd("20150502T130000Z") 126 | 127 | // 1 MB SUMMARY ... have fun 128 | val data = CharArray(1024*1024) 129 | Arrays.fill(data, 'x') 130 | event.summary = String(data) 131 | 132 | TestEvent(calendar, event).add() 133 | } 134 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/Css3ColorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import org.junit.Assert.assertEquals 10 | import org.junit.Assert.assertNull 11 | import org.junit.Test 12 | 13 | class Css3ColorTest { 14 | 15 | @Test 16 | fun testColorFromString() { 17 | // color name 18 | assertEquals(0xffffff00.toInt(), Css3Color.colorFromString("yellow")) 19 | 20 | // RGB value 21 | assertEquals(0xffffff00.toInt(), Css3Color.colorFromString("#ffff00")) 22 | 23 | // ARGB value 24 | assertEquals(0xffffff00.toInt(), Css3Color.colorFromString("#ffffff00")) 25 | 26 | // empty value 27 | assertNull(Css3Color.colorFromString("")) 28 | 29 | // invalid value 30 | assertNull(Css3Color.colorFromString("DoesNotExist")) 31 | } 32 | 33 | @Test 34 | fun testFromString() { 35 | // lower case 36 | assertEquals(0xffffff00.toInt(), Css3Color.fromString("yellow")?.argb) 37 | 38 | // capitalized 39 | assertEquals(0xffffff00.toInt(), Css3Color.fromString("Yellow")?.argb) 40 | 41 | // not-existing color 42 | assertNull(Css3Color.fromString("DoesNotExist")) 43 | } 44 | 45 | @Test 46 | fun testNearestMatch() { 47 | // every color is its own nearest match 48 | Css3Color.values().forEach { 49 | assertEquals(it.argb, Css3Color.nearestMatch(it.argb).argb) 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsStyleProvidersTaskTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import androidx.test.platform.app.InstrumentationRegistry 10 | import androidx.test.rule.GrantPermissionRule 11 | import org.junit.After 12 | import org.junit.Assume 13 | import org.junit.Before 14 | import org.junit.Rule 15 | import org.junit.runner.RunWith 16 | import org.junit.runners.Parameterized 17 | import java.util.logging.Logger 18 | 19 | @RunWith(Parameterized::class) 20 | 21 | abstract class DmfsStyleProvidersTaskTest( 22 | val providerName: TaskProvider.ProviderName 23 | ) { 24 | 25 | companion object { 26 | @Parameterized.Parameters(name="{0}") 27 | @JvmStatic 28 | fun taskProviders() = listOf(TaskProvider.ProviderName.OpenTasks,TaskProvider.ProviderName.TasksOrg) 29 | } 30 | 31 | @JvmField 32 | @Rule 33 | val permissionRule = GrantPermissionRule.grant(*providerName.permissions) 34 | 35 | var providerOrNull: TaskProvider? = null 36 | lateinit var provider: TaskProvider 37 | 38 | @Before 39 | open fun prepare() { 40 | providerOrNull = TaskProvider.acquire(InstrumentationRegistry.getInstrumentation().context, providerName) 41 | Assume.assumeNotNull(providerOrNull) // will halt here if providerOrNull is null 42 | 43 | provider = providerOrNull!! 44 | Logger.getLogger(javaClass.name).fine("Using task provider: $provider") 45 | } 46 | 47 | @After 48 | open fun shutdown() { 49 | providerOrNull?.close() 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/DmfsTaskListTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.accounts.Account 10 | import android.content.ContentUris 11 | import android.content.ContentValues 12 | import android.database.DatabaseUtils 13 | import at.bitfire.ical4android.impl.TestTask 14 | import at.bitfire.ical4android.impl.TestTaskList 15 | import net.fortuna.ical4j.model.property.RelatedTo 16 | import org.dmfs.tasks.contract.TaskContract 17 | import org.dmfs.tasks.contract.TaskContract.Properties 18 | import org.dmfs.tasks.contract.TaskContract.Property.Relation 19 | import org.dmfs.tasks.contract.TaskContract.Tasks 20 | import org.junit.Assert.assertEquals 21 | import org.junit.Assert.assertNotNull 22 | import org.junit.Assert.assertTrue 23 | import org.junit.Test 24 | 25 | class DmfsTaskListTest(providerName: TaskProvider.ProviderName): 26 | DmfsStyleProvidersTaskTest(providerName) { 27 | 28 | private val testAccount = Account("AndroidTaskListTest", TaskContract.LOCAL_ACCOUNT_TYPE) 29 | 30 | 31 | private fun createTaskList(): TestTaskList { 32 | val info = ContentValues() 33 | info.put(TaskContract.TaskLists.LIST_NAME, "Test Task List") 34 | info.put(TaskContract.TaskLists.LIST_COLOR, 0xffff0000) 35 | info.put(TaskContract.TaskLists.OWNER, "test@example.com") 36 | info.put(TaskContract.TaskLists.SYNC_ENABLED, 1) 37 | info.put(TaskContract.TaskLists.VISIBLE, 1) 38 | 39 | val uri = DmfsTaskList.create(testAccount, provider.client, providerName, info) 40 | assertNotNull(uri) 41 | 42 | return DmfsTaskList.findByID(testAccount, provider.client, providerName, TestTaskList.Factory, ContentUris.parseId(uri)) 43 | } 44 | 45 | 46 | @Test 47 | fun testManageTaskLists() { 48 | val taskList = createTaskList() 49 | 50 | try { 51 | // sync URIs 52 | assertEquals("true", taskList.taskListSyncUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER)) 53 | assertEquals(testAccount.type, taskList.taskListSyncUri().getQueryParameter(TaskContract.ACCOUNT_TYPE)) 54 | assertEquals(testAccount.name, taskList.taskListSyncUri().getQueryParameter(TaskContract.ACCOUNT_NAME)) 55 | 56 | assertEquals("true", taskList.tasksSyncUri().getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER)) 57 | assertEquals(testAccount.type, taskList.tasksSyncUri().getQueryParameter(TaskContract.ACCOUNT_TYPE)) 58 | assertEquals(testAccount.name, taskList.tasksSyncUri().getQueryParameter(TaskContract.ACCOUNT_NAME)) 59 | } finally { 60 | // delete task list 61 | assertTrue(taskList.delete()) 62 | } 63 | } 64 | 65 | @Test 66 | fun testTouchRelations() { 67 | val taskList = createTaskList() 68 | try { 69 | val parent = Task() 70 | parent.uid = "parent" 71 | parent.summary = "Parent task" 72 | 73 | val child = Task() 74 | child.uid = "child" 75 | child.summary = "Child task" 76 | child.relatedTo.add(RelatedTo(parent.uid)) 77 | 78 | // insert child before parent 79 | val childContentUri = TestTask(taskList, child).add() 80 | val childId = ContentUris.parseId(childContentUri) 81 | val parentContentUri = TestTask(taskList, parent).add() 82 | val parentId = ContentUris.parseId(parentContentUri) 83 | 84 | // OpenTasks should provide the correct relation 85 | taskList.provider.query(taskList.tasksPropertiesSyncUri(), null, 86 | "${Properties.TASK_ID}=?", arrayOf(childId.toString()), 87 | null, null)!!.use { cursor -> 88 | assertEquals(1, cursor.count) 89 | cursor.moveToNext() 90 | 91 | val row = ContentValues() 92 | DatabaseUtils.cursorRowToContentValues(cursor, row) 93 | 94 | assertEquals(Relation.CONTENT_ITEM_TYPE, row.getAsString(Properties.MIMETYPE)) 95 | assertEquals(parentId, row.getAsLong(Relation.RELATED_ID)) 96 | assertEquals(parent.uid, row.getAsString(Relation.RELATED_UID)) 97 | assertEquals(Relation.RELTYPE_PARENT, row.getAsInteger(Relation.RELATED_TYPE)) 98 | } 99 | 100 | // touch the relations to update parent_id values 101 | taskList.touchRelations() 102 | 103 | // now parent_id should bet set 104 | taskList.provider.query(childContentUri, arrayOf(Tasks.PARENT_ID), 105 | null, null, null)!!.use { cursor -> 106 | assertTrue(cursor.moveToNext()) 107 | assertEquals(parentId, cursor.getLong(0)) 108 | } 109 | } finally { 110 | taskList.delete() 111 | } 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/ICalPreprocessorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import androidx.test.filters.SdkSuppress 10 | import at.bitfire.ical4android.validation.FixInvalidDayOffsetPreprocessor 11 | import at.bitfire.ical4android.validation.FixInvalidUtcOffsetPreprocessor 12 | import at.bitfire.ical4android.validation.ICalPreprocessor 13 | import io.mockk.mockkObject 14 | import io.mockk.verify 15 | import net.fortuna.ical4j.data.CalendarBuilder 16 | import net.fortuna.ical4j.model.Component 17 | import net.fortuna.ical4j.model.component.VEvent 18 | import org.junit.Assert.assertEquals 19 | import org.junit.Test 20 | import java.io.InputStreamReader 21 | import java.io.StringReader 22 | 23 | class ICalPreprocessorTest { 24 | 25 | @Test 26 | @SdkSuppress(minSdkVersion = 28) 27 | fun testPreprocessStream_appliesStreamProcessors() { 28 | // Can only run on API Level 28 or newer because mockkObject doesn't support Android < P 29 | mockkObject(FixInvalidDayOffsetPreprocessor, FixInvalidUtcOffsetPreprocessor) { 30 | ICalPreprocessor.preprocessStream(StringReader("")) 31 | 32 | // verify that the required stream processors have been called 33 | verify { 34 | FixInvalidDayOffsetPreprocessor.preprocess(any()) 35 | FixInvalidUtcOffsetPreprocessor.preprocess(any()) 36 | } 37 | } 38 | } 39 | 40 | 41 | @Test 42 | fun testPreprocessCalendar_MsTimeZones() { 43 | javaClass.classLoader!!.getResourceAsStream("events/outlook1.ics").use { stream -> 44 | val reader = InputStreamReader(stream, Charsets.UTF_8) 45 | val calendar = CalendarBuilder().build(reader) 46 | val vEvent = calendar.getComponent(Component.VEVENT) as VEvent 47 | 48 | assertEquals("W. Europe Standard Time", vEvent.startDate.timeZone.id) 49 | ICalPreprocessor.preprocessCalendar(calendar) 50 | assertEquals("Europe/Vienna", vEvent.startDate.timeZone.id) 51 | } 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jSettingsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import net.fortuna.ical4j.util.TimeZones 10 | import org.junit.Assert.assertEquals 11 | import org.junit.Test 12 | 13 | class Ical4jSettingsTest { 14 | 15 | @Test 16 | fun testDatesAreUtc() { 17 | /* ical4j can treat DATE values either as 18 | - floating (= system time zone), or 19 | - UTC. 20 | 21 | This is controlled by the "net.fortuna.ical4j.timezone.date.floating" setting. 22 | 23 | The Calendar provider requires date timestamps to be in UTC, so we will test that. 24 | */ 25 | assertEquals(TimeZones.getUtcTimeZone(), TimeZones.getDateTimeZone()) 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/Ical4jTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import net.fortuna.ical4j.data.CalendarBuilder 10 | import net.fortuna.ical4j.model.Component 11 | import net.fortuna.ical4j.model.DateTime 12 | import net.fortuna.ical4j.model.Parameter 13 | import net.fortuna.ical4j.model.TemporalAmountAdapter 14 | import net.fortuna.ical4j.model.TimeZone 15 | import net.fortuna.ical4j.model.TimeZoneRegistryFactory 16 | import net.fortuna.ical4j.model.component.VTimeZone 17 | import net.fortuna.ical4j.model.parameter.Email 18 | import org.junit.Assert.assertEquals 19 | import org.junit.Assert.assertNotEquals 20 | import org.junit.Test 21 | import java.io.StringReader 22 | import java.time.Period 23 | 24 | class Ical4jTest { 25 | 26 | val tzReg = TimeZoneRegistryFactory.getInstance().createRegistry() 27 | 28 | @Test 29 | fun testEmailParameter() { 30 | // https://github.com/ical4j/ical4j/issues/418 31 | val e = Event.eventsFromReader( 32 | StringReader( 33 | "BEGIN:VCALENDAR\n" + 34 | "VERSION:2.0\n" + 35 | "BEGIN:VEVENT\n" + 36 | "SUMMARY:Test\n" + 37 | "DTSTART;VALUE=DATE:20200702\n" + 38 | "ATTENDEE;EMAIL=attendee1@example.virtual:sample:attendee1\n" + 39 | "END:VEVENT\n" + 40 | "END:VCALENDAR" 41 | ) 42 | ).first() 43 | assertEquals("attendee1@example.virtual", e.attendees.first().getParameter(Parameter.EMAIL).value) 44 | } 45 | 46 | @Test 47 | fun testTemporalAmountAdapter_durationToString_DropsMinutes() { 48 | // https://github.com/ical4j/ical4j/issues/420 49 | assertEquals("P1DT1H4M", TemporalAmountAdapter.parse("P1DT1H4M").toString()) 50 | } 51 | 52 | @Test(expected = AssertionError::class) 53 | fun testTemporalAmountAdapter_Months() { 54 | // https://github.com/ical4j/ical4j/issues/419 55 | // A month usually doesn't have 4 weeks = 4*7 days = 28 days (except February in non-leap years). 56 | assertNotEquals("P4W", TemporalAmountAdapter(Period.ofMonths(1)).toString()) 57 | } 58 | 59 | @Test(expected = AssertionError::class) 60 | fun testTemporalAmountAdapter_Year() { 61 | // https://github.com/ical4j/ical4j/issues/419 62 | // A year has 365 or 366 days, but never 52 weeks = 52*7 days = 364 days. 63 | assertNotEquals("P52W", TemporalAmountAdapter(Period.ofYears(1)).toString()) 64 | } 65 | 66 | @Test(expected = AssertionError::class) 67 | fun testTzDarwin() { 68 | val darwin = tzReg.getTimeZone("Australia/Darwin") 69 | 70 | val ts1 = 1616720400000 71 | val dt1 = DateTime(ts1).apply { isUtc = true } 72 | assertEquals(9.5, darwin.getOffset(ts1)/3600000.0, .01) 73 | 74 | val dt2 = DateTime("20210326T103000", darwin) 75 | assertEquals(1616720400000, dt2.time) 76 | } 77 | 78 | @Test 79 | fun testTzDublin_negativeDst() { 80 | // https://github.com/ical4j/ical4j/issues/493 81 | // fixed by enabling net.fortuna.ical4j.timezone.offset.negative_dst_supported in ical4j.properties 82 | val vtzFromGoogle = "BEGIN:VCALENDAR\n" + 83 | "CALSCALE:GREGORIAN\n" + 84 | "VERSION:2.0\n" + 85 | "PRODID:-//Google Inc//Google Calendar 70.9054//EN\n" + 86 | "BEGIN:VTIMEZONE\n" + 87 | "TZID:Europe/Dublin\n" + 88 | "BEGIN:STANDARD\n" + 89 | "TZOFFSETFROM:+0000\n" + 90 | "TZOFFSETTO:+0100\n" + 91 | "TZNAME:IST\n" + 92 | "DTSTART:19700329T010000\n" + 93 | "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\n" + 94 | "END:STANDARD\n" + 95 | "BEGIN:DAYLIGHT\n" + 96 | "TZOFFSETFROM:+0100\n" + 97 | "TZOFFSETTO:+0000\n" + 98 | "TZNAME:GMT\n" + 99 | "DTSTART:19701025T020000\n" + 100 | "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\n" + 101 | "END:DAYLIGHT\n" + 102 | "END:VTIMEZONE\n" + 103 | "END:VCALENDAR" 104 | val iCalFromGoogle = CalendarBuilder().build(StringReader(vtzFromGoogle)) 105 | val dublinFromGoogle = iCalFromGoogle.getComponent(Component.VTIMEZONE) as VTimeZone 106 | val dt = DateTime("20210108T151500", TimeZone(dublinFromGoogle)) 107 | assertEquals("20210108T151500", dt.toString()) 108 | } 109 | 110 | @Test 111 | fun testTzKarachi() { 112 | // https://github.com/ical4j/ical4j/issues/491 113 | val karachi = tzReg.getTimeZone("Asia/Karachi") 114 | 115 | val ts1 = 1609945200000 116 | val dt1 = DateTime(ts1).apply { isUtc = true } 117 | assertEquals(5, karachi.getOffset(ts1)/3600000) 118 | 119 | val dt2 = DateTime("20210106T200000", karachi) 120 | assertEquals(1609945200000, dt2.time) 121 | } 122 | 123 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/JtxCollectionTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.accounts.Account 10 | import android.content.ContentProviderClient 11 | import android.content.ContentValues 12 | import androidx.test.platform.app.InstrumentationRegistry 13 | import androidx.test.rule.GrantPermissionRule 14 | import at.bitfire.ical4android.impl.TestJtxCollection 15 | import at.bitfire.ical4android.util.MiscUtils.closeCompat 16 | import at.techbee.jtx.JtxContract 17 | import at.techbee.jtx.JtxContract.asSyncAdapter 18 | import junit.framework.TestCase.assertEquals 19 | import junit.framework.TestCase.assertNotNull 20 | import junit.framework.TestCase.assertNull 21 | import junit.framework.TestCase.assertTrue 22 | import org.junit.After 23 | import org.junit.AfterClass 24 | import org.junit.Assume 25 | import org.junit.BeforeClass 26 | import org.junit.ClassRule 27 | import org.junit.Test 28 | 29 | class JtxCollectionTest { 30 | 31 | companion object { 32 | 33 | val context = InstrumentationRegistry.getInstrumentation().targetContext 34 | val contentResolver = context.contentResolver 35 | 36 | private lateinit var client: ContentProviderClient 37 | 38 | @JvmField 39 | @ClassRule 40 | val permissionRule = GrantPermissionRule.grant(*TaskProvider.PERMISSIONS_JTX) 41 | 42 | @BeforeClass 43 | @JvmStatic 44 | fun openProvider() { 45 | val clientOrNull = contentResolver.acquireContentProviderClient(JtxContract.AUTHORITY) 46 | Assume.assumeNotNull(clientOrNull) 47 | 48 | client = clientOrNull!! 49 | } 50 | 51 | @AfterClass 52 | @JvmStatic 53 | fun closeProvider() { 54 | client.closeCompat() 55 | } 56 | 57 | } 58 | 59 | private val testAccount = Account("TEST", JtxContract.JtxCollection.TEST_ACCOUNT_TYPE) 60 | 61 | private val url = "https://jtx.techbee.at" 62 | private val displayname = "jtx" 63 | private val syncversion = JtxContract.VERSION 64 | 65 | private val cv = ContentValues().apply { 66 | put(JtxContract.JtxCollection.ACCOUNT_TYPE, testAccount.type) 67 | put(JtxContract.JtxCollection.ACCOUNT_NAME, testAccount.name) 68 | put(JtxContract.JtxCollection.URL, url) 69 | put(JtxContract.JtxCollection.DISPLAYNAME, displayname) 70 | put(JtxContract.JtxCollection.SYNC_VERSION, syncversion) 71 | } 72 | 73 | @After 74 | fun tearDown() { 75 | var collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) 76 | collections.forEach { collection -> 77 | collection.delete() 78 | } 79 | collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) 80 | assertEquals(0, collections.size) 81 | } 82 | 83 | 84 | @Test 85 | fun create_populate_find() { 86 | val collectionUri = JtxCollection.create(testAccount, client, cv) 87 | assertNotNull(collectionUri) 88 | val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) 89 | 90 | assertEquals(1, collections.size) 91 | assertEquals(testAccount.type, collections[0].account.type) 92 | assertEquals(testAccount.name, collections[0].account.name) 93 | assertEquals(url, collections[0].url) 94 | assertEquals(displayname, collections[0].displayname) 95 | assertEquals(syncversion.toString(), collections[0].syncstate) 96 | } 97 | 98 | @Test 99 | fun queryICalObjects() { 100 | val collectionUri = JtxCollection.create(testAccount, client, cv) 101 | assertNotNull(collectionUri) 102 | 103 | val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) 104 | val items = collections[0].queryICalObjects(null, null) 105 | assertEquals(0, items.size) 106 | 107 | val cv = ContentValues().apply { 108 | put(JtxContract.JtxICalObject.SUMMARY, "summary") 109 | put(JtxContract.JtxICalObject.COMPONENT, JtxContract.JtxICalObject.Component.VJOURNAL.name) 110 | put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collections[0].id) 111 | } 112 | client.insert(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv) 113 | val icalobjects = collections[0].queryICalObjects(null, null) 114 | 115 | assertEquals(1, icalobjects.size) 116 | } 117 | 118 | @Test 119 | fun queryRecur_test() { 120 | val collectionUri = JtxCollection.create(testAccount, client, cv) 121 | assertNotNull(collectionUri) 122 | 123 | val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) 124 | val item = collections[0].queryRecur("abc1234", "xyz5678") 125 | assertNull(item) 126 | 127 | val cv = ContentValues().apply { 128 | put(JtxContract.JtxICalObject.UID, "abc1234") 129 | put(JtxContract.JtxICalObject.RECURID, "xyz5678") 130 | put(JtxContract.JtxICalObject.RECURID_TIMEZONE, "Europe/Vienna") 131 | put(JtxContract.JtxICalObject.SUMMARY, "summary") 132 | put(JtxContract.JtxICalObject.COMPONENT, JtxContract.JtxICalObject.Component.VJOURNAL.name) 133 | put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collections[0].id) 134 | } 135 | client.insert(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv) 136 | val contentValues = collections[0].queryRecur("abc1234", "xyz5678") 137 | 138 | assertEquals("abc1234", contentValues?.getAsString(JtxContract.JtxICalObject.UID)) 139 | assertEquals("xyz5678", contentValues?.getAsString(JtxContract.JtxICalObject.RECURID)) 140 | assertEquals("Europe/Vienna", contentValues?.getAsString(JtxContract.JtxICalObject.RECURID_TIMEZONE)) 141 | } 142 | 143 | @Test 144 | fun getICSForCollection_test() { 145 | val collectionUri = JtxCollection.create(testAccount, client, cv) 146 | assertNotNull(collectionUri) 147 | 148 | val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) 149 | val items = collections[0].queryICalObjects(null, null) 150 | assertEquals(0, items.size) 151 | 152 | val cv1 = ContentValues().apply { 153 | put(JtxContract.JtxICalObject.SUMMARY, "summary") 154 | put(JtxContract.JtxICalObject.COMPONENT, JtxContract.JtxICalObject.Component.VJOURNAL.name) 155 | put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collections[0].id) 156 | } 157 | val cv2 = ContentValues().apply { 158 | put(JtxContract.JtxICalObject.SUMMARY, "entry2") 159 | put(JtxContract.JtxICalObject.COMPONENT, JtxContract.JtxICalObject.Component.VTODO.name) 160 | put(JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID, collections[0].id) 161 | } 162 | client.insert(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv1) 163 | client.insert(JtxContract.JtxICalObject.CONTENT_URI.asSyncAdapter(testAccount), cv2) 164 | 165 | val ics = collections[0].getICSForCollection() 166 | assertTrue(ics.contains(Regex("BEGIN:VCALENDAR(\\n*|\\r*|\\t*|.*)*END:VCALENDAR"))) 167 | assertTrue(ics.contains("PRODID:+//IDN bitfire.at//ical4android")) 168 | assertTrue(ics.contains("SUMMARY:summary")) 169 | assertTrue(ics.contains("SUMMARY:entry2")) 170 | assertTrue(ics.contains(Regex("BEGIN:VJOURNAL(\\n*|\\r*|\\t*|.*)*END:VJOURNAL"))) 171 | assertTrue(ics.contains(Regex("BEGIN:VTODO(\\n*|\\r*|\\t*|.*)*END:VTODO"))) 172 | } 173 | 174 | 175 | @Test 176 | fun updateLastSync_test() { 177 | val collectionUri = JtxCollection.create(testAccount, client, cv) 178 | assertNotNull(collectionUri) 179 | val collections = JtxCollection.find(testAccount, client, context, TestJtxCollection.Factory, null, null) 180 | 181 | collections.forEach { collection -> 182 | client.query(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(testAccount), arrayOf(JtxContract.JtxCollection.LAST_SYNC), null, emptyArray(), null).use { 183 | assertNotNull(it) 184 | assertTrue(it!!.moveToFirst()) 185 | assertTrue(it.isNull(0)) 186 | } 187 | collection.updateLastSync() 188 | client.query(JtxContract.JtxCollection.CONTENT_URI.asSyncAdapter(testAccount), arrayOf(JtxContract.JtxCollection.LAST_SYNC), null, emptyArray(), null).use { 189 | assertNotNull(it) 190 | assertTrue(it!!.moveToFirst()) 191 | assertTrue(!it.isNull(0)) 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/LocaleNonWesternDigitsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import net.fortuna.ical4j.model.property.TzOffsetFrom 10 | import org.junit.AfterClass 11 | import org.junit.Assert.assertEquals 12 | import org.junit.BeforeClass 13 | import org.junit.Test 14 | import java.time.ZoneOffset 15 | import java.util.Locale 16 | 17 | class LocaleNonWesternDigitsTest { 18 | 19 | companion object { 20 | val origLocale = Locale.getDefault() 21 | val testLocale = Locale("fa", "ir", "u-un-arabext") 22 | 23 | @BeforeClass 24 | @JvmStatic 25 | fun setFaIrArabLocale() { 26 | assertEquals("Persian (Iran) locale not available", "fa", testLocale.language) 27 | Locale.setDefault(testLocale) 28 | } 29 | 30 | @AfterClass 31 | @JvmStatic 32 | fun resetLocale() { 33 | Locale.setDefault(origLocale) 34 | } 35 | 36 | } 37 | 38 | @Test 39 | fun testLocale_StringFormat() { 40 | // does not fail if the Locale with Persian digits is available 41 | assertEquals("۲۰۲۰", String.format("%d", 2020)) 42 | } 43 | 44 | @Test 45 | fun testLocale_StringFormat_Root() { 46 | assertEquals("2020", String.format(Locale.ROOT, "%d", 2020)) 47 | } 48 | 49 | @Test() 50 | fun testLocale_ical4j() { 51 | val offset = TzOffsetFrom(ZoneOffset.ofHours(1)) 52 | val iCal = offset.toString() 53 | assertEquals("TZOFFSETFROM:+0100\r\n", iCal) // fails: is "TZOFFSETFROM:+۰۱۰۰\r\n" instead 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/UnknownPropertyTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import androidx.test.filters.SmallTest 10 | import net.fortuna.ical4j.model.Parameter 11 | import net.fortuna.ical4j.model.parameter.Rsvp 12 | import net.fortuna.ical4j.model.parameter.XParameter 13 | import net.fortuna.ical4j.model.property.Attendee 14 | import net.fortuna.ical4j.model.property.Uid 15 | import org.json.JSONException 16 | import org.junit.Assert.assertEquals 17 | import org.junit.Assert.assertTrue 18 | import org.junit.Test 19 | 20 | class UnknownPropertyTest { 21 | 22 | @Test 23 | @SmallTest 24 | fun testFromJsonString() { 25 | val prop = UnknownProperty.fromJsonString("[ \"UID\", \"PropValue\" ]") 26 | assertTrue(prop is Uid) 27 | assertEquals("UID", prop.name) 28 | assertEquals("PropValue", prop.value) 29 | } 30 | 31 | @Test 32 | @SmallTest 33 | fun testFromJsonStringWithParameters() { 34 | val prop = UnknownProperty.fromJsonString("[ \"ATTENDEE\", \"PropValue\", { \"x-param1\": \"value1\", \"x-param2\": \"value2\" } ]") 35 | assertTrue(prop is Attendee) 36 | assertEquals("ATTENDEE", prop.name) 37 | assertEquals("PropValue", prop.value) 38 | assertEquals(2, prop.parameters.size()) 39 | assertEquals("value1", prop.parameters.getParameter("x-param1").value) 40 | assertEquals("value2", prop.parameters.getParameter("x-param2").value) 41 | } 42 | 43 | @Test(expected = JSONException::class) 44 | @SmallTest 45 | fun testFromInvalidJsonString() { 46 | UnknownProperty.fromJsonString("This isn't JSON") 47 | } 48 | 49 | 50 | @Test 51 | @SmallTest 52 | fun testToJsonString() { 53 | val attendee = Attendee("mailto:test@test.at") 54 | assertEquals( 55 | "ATTENDEE:mailto:test@test.at", 56 | attendee.toString().trim() 57 | ) 58 | 59 | attendee.parameters.add(Rsvp(true)) 60 | attendee.parameters.add(XParameter("X-My-Param", "SomeValue")) 61 | assertEquals( 62 | "ATTENDEE;RSVP=TRUE;X-My-Param=SomeValue:mailto:test@test.at", 63 | attendee.toString().trim() 64 | ) 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestCalendar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.impl 8 | 9 | import android.accounts.Account 10 | import android.content.ContentProviderClient 11 | import android.content.ContentUris 12 | import android.content.ContentValues 13 | import android.provider.CalendarContract 14 | import at.bitfire.ical4android.AndroidCalendar 15 | import at.bitfire.ical4android.AndroidCalendarFactory 16 | 17 | class TestCalendar( 18 | account: Account, 19 | providerClient: ContentProviderClient, 20 | id: Long 21 | ): AndroidCalendar(account, providerClient, TestEvent.Factory, id) { 22 | 23 | companion object { 24 | fun findOrCreate(account: Account, provider: ContentProviderClient): TestCalendar { 25 | val calendars = AndroidCalendar.find(account, provider, Factory, null, null) 26 | return if (calendars.isEmpty()) { 27 | val values = ContentValues(3) 28 | values.put(CalendarContract.Calendars.NAME, "TestCalendar") 29 | values.put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, "ical4android Test Calendar") 30 | values.put(CalendarContract.Calendars.ALLOWED_REMINDERS, CalendarContract.Reminders.METHOD_DEFAULT) 31 | val uri = AndroidCalendar.create(account, provider, values) 32 | 33 | TestCalendar(account, provider, ContentUris.parseId(uri)) 34 | } else 35 | calendars.first() 36 | } 37 | } 38 | 39 | 40 | object Factory: AndroidCalendarFactory { 41 | override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) = 42 | TestCalendar(account, provider, id) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestEvent.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.impl 8 | 9 | import android.content.ContentValues 10 | import android.provider.CalendarContract.Events 11 | import at.bitfire.ical4android.AndroidCalendar 12 | import at.bitfire.ical4android.AndroidEvent 13 | import at.bitfire.ical4android.AndroidEventFactory 14 | import at.bitfire.ical4android.BatchOperation 15 | import at.bitfire.ical4android.Event 16 | import java.util.UUID 17 | 18 | class TestEvent: AndroidEvent { 19 | 20 | constructor(calendar: AndroidCalendar, values: ContentValues) 21 | : super(calendar, values) 22 | 23 | constructor(calendar: TestCalendar, event: Event) 24 | : super(calendar, event) 25 | 26 | val syncId by lazy { UUID.randomUUID().toString() } 27 | 28 | 29 | override fun buildEvent(recurrence: Event?, builder: BatchOperation.CpoBuilder) { 30 | if (recurrence != null) 31 | builder.withValue(Events.ORIGINAL_SYNC_ID, syncId) 32 | else 33 | builder.withValue(Events._SYNC_ID, syncId) 34 | 35 | super.buildEvent(recurrence, builder) 36 | } 37 | 38 | 39 | object Factory: AndroidEventFactory { 40 | override fun fromProvider(calendar: AndroidCalendar, values: ContentValues) = 41 | TestEvent(calendar, values) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxCollection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.impl 8 | 9 | import android.accounts.Account 10 | import android.content.ContentProviderClient 11 | import at.bitfire.ical4android.JtxCollection 12 | import at.bitfire.ical4android.JtxCollectionFactory 13 | import at.bitfire.ical4android.JtxICalObject 14 | import at.bitfire.ical4android.util.MiscUtils.toValues 15 | import at.techbee.jtx.JtxContract 16 | import java.util.LinkedList 17 | 18 | class TestJtxCollection( 19 | account: Account, 20 | provider: ContentProviderClient, 21 | id: Long 22 | ): JtxCollection(account, provider, TestJtxIcalObject.Factory, id) { 23 | 24 | /** 25 | * Queries [JtxContract.JtxICalObject] from this collection. Adds a WHERE clause that restricts the 26 | * query to [JtxContract.JtxCollection.ID] = [id]. 27 | * @param _where selection 28 | * @param _whereArgs arguments for selection 29 | * @return events from this calendar which match the selection 30 | */ 31 | fun queryICalObjects(_where: String? = null, _whereArgs: Array? = null): List { 32 | val where = "(${_where ?: "1"}) AND ${JtxContract.JtxICalObject.ICALOBJECT_COLLECTIONID} = ?" 33 | val whereArgs = (_whereArgs ?: arrayOf()) + id.toString() 34 | 35 | val iCalObjects = LinkedList() 36 | client.query(jtxSyncURI(), null, where, whereArgs, null)?.use { cursor -> 37 | while (cursor.moveToNext()) 38 | iCalObjects += TestJtxIcalObject.Factory.fromProvider(this, cursor.toValues()) 39 | } 40 | return iCalObjects 41 | } 42 | 43 | 44 | object Factory: JtxCollectionFactory { 45 | 46 | override fun newInstance( 47 | account: Account, 48 | client: ContentProviderClient, 49 | id: Long 50 | ): TestJtxCollection = TestJtxCollection(account, client, id) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestJtxIcalObject.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.impl 8 | 9 | import android.content.ContentValues 10 | import at.bitfire.ical4android.JtxCollection 11 | import at.bitfire.ical4android.JtxICalObject 12 | import at.bitfire.ical4android.JtxICalObjectFactory 13 | 14 | class TestJtxIcalObject(testCollection: JtxCollection): JtxICalObject(testCollection) { 15 | 16 | object Factory: JtxICalObjectFactory { 17 | 18 | override fun fromProvider( 19 | collection: JtxCollection, 20 | values: ContentValues 21 | ): JtxICalObject = TestJtxIcalObject(collection) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTask.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.impl 8 | 9 | import android.content.ContentValues 10 | 11 | import at.bitfire.ical4android.DmfsTask 12 | import at.bitfire.ical4android.DmfsTaskFactory 13 | import at.bitfire.ical4android.DmfsTaskList 14 | import at.bitfire.ical4android.Task 15 | 16 | class TestTask: DmfsTask { 17 | 18 | constructor(taskList: DmfsTaskList, values: ContentValues) 19 | : super(taskList, values) 20 | 21 | constructor(taskList: TestTaskList, task: Task) 22 | : super(taskList, task) 23 | 24 | object Factory: DmfsTaskFactory { 25 | override fun fromProvider(taskList: DmfsTaskList, values: ContentValues) = 26 | TestTask(taskList, values) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/impl/TestTaskList.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.impl 8 | 9 | import android.accounts.Account 10 | import android.content.ContentProviderClient 11 | import android.content.ContentUris 12 | import android.content.ContentValues 13 | import at.bitfire.ical4android.DmfsTaskList 14 | import at.bitfire.ical4android.DmfsTaskListFactory 15 | import at.bitfire.ical4android.TaskProvider 16 | import org.dmfs.tasks.contract.TaskContract 17 | 18 | class TestTaskList( 19 | account: Account, 20 | provider: ContentProviderClient, 21 | providerName: TaskProvider.ProviderName, 22 | id: Long 23 | ): DmfsTaskList(account, provider, providerName, TestTask.Factory, id) { 24 | 25 | companion object { 26 | 27 | fun create( 28 | account: Account, 29 | provider: TaskProvider, 30 | ): TestTaskList { 31 | val values = ContentValues(4) 32 | values.put(TaskContract.TaskListColumns.LIST_NAME, "Test Task List") 33 | values.put(TaskContract.TaskListColumns.LIST_COLOR, 0xffff0000) 34 | values.put(TaskContract.TaskListColumns.SYNC_ENABLED, 1) 35 | values.put(TaskContract.TaskListColumns.VISIBLE, 1) 36 | val uri = DmfsTaskList.create(account, provider.client, provider.name, values) 37 | 38 | return TestTaskList(account, provider.client, provider.name, ContentUris.parseId(uri)) 39 | } 40 | 41 | } 42 | 43 | 44 | object Factory: DmfsTaskListFactory { 45 | override fun newInstance(account: Account, provider: ContentProviderClient, providerName: TaskProvider.ProviderName, id: Long) = 46 | TestTaskList(account, provider, providerName, id) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/util/DateUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.util 8 | 9 | import net.fortuna.ical4j.model.Date 10 | import net.fortuna.ical4j.model.DateTime 11 | import net.fortuna.ical4j.model.property.DtEnd 12 | import net.fortuna.ical4j.model.property.DtStart 13 | import org.junit.Assert.assertEquals 14 | import org.junit.Assert.assertFalse 15 | import org.junit.Assert.assertNotNull 16 | import org.junit.Assert.assertNull 17 | import org.junit.Assert.assertTrue 18 | import org.junit.Test 19 | import java.time.ZoneId 20 | import java.util.TimeZone 21 | 22 | class DateUtilsTest { 23 | 24 | private val tzIdToronto = "America/Toronto" 25 | private val tzToronto = DateUtils.ical4jTimeZone(tzIdToronto)!! 26 | 27 | @Test 28 | fun testTimeZoneRegistry() { 29 | assertNotNull(DateUtils.ical4jTimeZone("Europe/Vienna")) 30 | 31 | // https://github.com/ical4j/ical4j/issues/207 32 | assertNotNull(DateUtils.ical4jTimeZone("EST")) 33 | } 34 | 35 | 36 | @Test 37 | fun testFindAndroidTimezoneID() { 38 | assertEquals("Europe/Vienna", DateUtils.findAndroidTimezoneID("Europe/Vienna")) 39 | assertEquals("Europe/Vienna", DateUtils.findAndroidTimezoneID("Vienna")) 40 | assertEquals("Europe/Vienna", DateUtils.findAndroidTimezoneID("Something with Europe/Vienna in between")) 41 | assertEquals(TimeZone.getDefault().id, DateUtils.findAndroidTimezoneID(null)) 42 | assertEquals(TimeZone.getDefault().id, DateUtils.findAndroidTimezoneID("nothing-to-be-found")) 43 | } 44 | 45 | 46 | @Test 47 | fun testGetZoneId() { 48 | assertNull(DateUtils.getZoneId(null)) 49 | assertNull(DateUtils.getZoneId("not/available")) 50 | assertEquals(ZoneId.of("Europe/Vienna"), DateUtils.getZoneId("Europe/Vienna")) 51 | } 52 | 53 | 54 | @Test 55 | fun testIsDate() { 56 | assertTrue(DateUtils.isDate(DtStart(Date("20200101")))) 57 | assertFalse(DateUtils.isDate(DtStart(DateTime("20200101T010203Z")))) 58 | assertFalse(DateUtils.isDate(null)) 59 | } 60 | 61 | @Test 62 | fun testIsDateTime() { 63 | assertFalse(DateUtils.isDateTime(DtEnd(Date("20200101")))) 64 | assertTrue(DateUtils.isDateTime(DtEnd(DateTime("20200101T010203Z")))) 65 | assertFalse(DateUtils.isDateTime(null)) 66 | } 67 | 68 | 69 | @Test 70 | fun testParseVTimeZone() { 71 | val vtz = """ 72 | BEGIN:VCALENDAR 73 | VERSION:2.0 74 | PRODID:DAVx5 75 | BEGIN:VTIMEZONE 76 | TZID:Asia/Shanghai 77 | END:VTIMEZONE 78 | END:VCALENDAR""".trimIndent() 79 | assertEquals("Asia/Shanghai", DateUtils.parseVTimeZone(vtz)?.timeZoneId?.value) 80 | } 81 | 82 | @Test 83 | fun testParseVTimeZone_Invalid() { 84 | assertNull(DateUtils.parseVTimeZone("Invalid")) 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/util/MiscUtilsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.util 8 | 9 | import android.accounts.Account 10 | import android.content.ContentValues 11 | import android.database.MatrixCursor 12 | import android.net.Uri 13 | import androidx.test.filters.SmallTest 14 | import at.bitfire.ical4android.util.MiscUtils.asSyncAdapter 15 | import at.bitfire.ical4android.util.MiscUtils.removeBlankStrings 16 | import at.bitfire.ical4android.util.MiscUtils.toValues 17 | import org.junit.Assert.assertEquals 18 | import org.junit.Assert.assertNull 19 | import org.junit.Test 20 | 21 | class MiscUtilsTest { 22 | 23 | @Test 24 | @SmallTest 25 | fun testCursorToValues() { 26 | val columns = arrayOf("col1", "col2") 27 | val c = MatrixCursor(columns) 28 | c.addRow(arrayOf("row1_val1", "row1_val2")) 29 | c.moveToFirst() 30 | val values = c.toValues() 31 | assertEquals("row1_val1", values.getAsString("col1")) 32 | assertEquals("row1_val2", values.getAsString("col2")) 33 | } 34 | 35 | @Test 36 | @SmallTest 37 | fun testRemoveEmptyAndBlankStrings() { 38 | val values = ContentValues() 39 | values.put("key1", "value") 40 | values.put("key2", 1L) 41 | values.put("key3", "") 42 | values.put("key4", "\n") 43 | values.put("key5", " \n ") 44 | values.put("key6", " ") 45 | values.removeBlankStrings() 46 | assertEquals("value", values.getAsString("key1")) 47 | assertEquals(1L, values.getAsLong("key2")) 48 | assertNull(values.get("key3")) 49 | assertNull(values.get("key4")) 50 | assertNull(values.get("key5")) 51 | assertNull(values.get("key6")) 52 | } 53 | 54 | 55 | @Test 56 | fun testUriHelper_asSyncAdapter() { 57 | val account = Account("testName", "testType") 58 | val baseUri = Uri.parse("test://example.com/") 59 | assertEquals( 60 | Uri.parse("$baseUri?account_name=testName&account_type=testType&caller_is_syncadapter=true"), 61 | baseUri.asSyncAdapter(account) 62 | ) 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /lib/src/androidTest/kotlin/at/bitfire/ical4android/util/TimeApiExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.util 8 | 9 | import at.bitfire.ical4android.util.TimeApiExtensions.requireTimeZone 10 | import at.bitfire.ical4android.util.TimeApiExtensions.toDuration 11 | import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate 12 | import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDateTime 13 | import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate 14 | import at.bitfire.ical4android.util.TimeApiExtensions.toLocalTime 15 | import at.bitfire.ical4android.util.TimeApiExtensions.toRfc5545Duration 16 | import at.bitfire.ical4android.util.TimeApiExtensions.toZoneIdCompat 17 | import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime 18 | import net.fortuna.ical4j.model.Date 19 | import net.fortuna.ical4j.model.DateTime 20 | import net.fortuna.ical4j.model.TimeZone 21 | import net.fortuna.ical4j.util.TimeZones 22 | import org.junit.Assert.assertEquals 23 | import org.junit.Assert.assertTrue 24 | import org.junit.Test 25 | import java.time.DayOfWeek 26 | import java.time.Duration 27 | import java.time.Instant 28 | import java.time.LocalDate 29 | import java.time.LocalTime 30 | import java.time.Period 31 | import java.time.ZoneId 32 | import java.time.ZoneOffset 33 | import java.time.ZonedDateTime 34 | 35 | class TimeApiExtensionsTest { 36 | 37 | val tzBerlin: TimeZone = DateUtils.ical4jTimeZone("Europe/Berlin")!! 38 | 39 | 40 | @Test 41 | fun testTimeZone_toZoneIdCompat_NotUtc() { 42 | assertEquals(ZoneId.of("Europe/Berlin"), tzBerlin.toZoneId()) 43 | } 44 | 45 | @Test 46 | fun testTimeZone_toZoneIdCompat_Utc() { 47 | assertEquals(ZoneOffset.UTC, TimeZones.getUtcTimeZone().toZoneIdCompat()) 48 | } 49 | 50 | 51 | @Test 52 | fun testDate_toLocalDate() { 53 | val date = Date("20200620").toLocalDate() 54 | assertEquals(2020, date.year) 55 | assertEquals(6, date.monthValue) 56 | assertEquals(20, date.dayOfMonth) 57 | assertEquals(DayOfWeek.SATURDAY, date.dayOfWeek) 58 | } 59 | 60 | 61 | @Test 62 | fun testDateTime_requireTimeZone() { 63 | val time = DateTime("2020707T010203", tzBerlin) 64 | assertEquals(tzBerlin, time.requireTimeZone()) 65 | } 66 | 67 | @Test 68 | fun testDateTime_requireTimeZone_Floating() { 69 | val time = DateTime("2020707T010203") 70 | assertEquals(TimeZone.getDefault(), time.requireTimeZone()) 71 | } 72 | 73 | @Test 74 | fun testDateTime_requireTimeZone_Utc() { 75 | val time = DateTime("2020707T010203Z").apply { isUtc = true } 76 | assertTrue(time.isUtc) 77 | assertEquals(TimeZones.getUtcTimeZone(), time.requireTimeZone()) 78 | } 79 | 80 | 81 | @Test 82 | fun testDateTime_toLocalDate_TimezoneBoundary() { 83 | val date = DateTime("20200620T000000", tzBerlin).toLocalDate() 84 | assertEquals(2020, date.year) 85 | assertEquals(6, date.monthValue) 86 | assertEquals(20, date.dayOfMonth) 87 | assertEquals(DayOfWeek.SATURDAY, date.dayOfWeek) 88 | } 89 | 90 | @Test 91 | fun testDateTime_toLocalDate_TimezoneDuringDay() { 92 | val date = DateTime("20200620T123000", tzBerlin).toLocalDate() 93 | assertEquals(2020, date.year) 94 | assertEquals(6, date.monthValue) 95 | assertEquals(20, date.dayOfMonth) 96 | assertEquals(DayOfWeek.SATURDAY, date.dayOfWeek) 97 | } 98 | 99 | @Test 100 | fun testDateTime_toLocalDate_UtcDuringDay() { 101 | val date = DateTime("20200620T123000Z").apply { isUtc = true }.toLocalDate() 102 | assertEquals(2020, date.year) 103 | assertEquals(6, date.monthValue) 104 | assertEquals(20, date.dayOfMonth) 105 | assertEquals(DayOfWeek.SATURDAY, date.dayOfWeek) 106 | } 107 | 108 | 109 | @Test 110 | fun testDateTime_toLocalTime() { 111 | assertEquals(LocalTime.of(12, 30), DateTime("20200620T123000", tzBerlin).toLocalTime()) 112 | } 113 | 114 | @Test 115 | fun testDateTime_toLocalTime_Floating() { 116 | assertEquals(LocalTime.of(12, 30), DateTime("20200620T123000").toLocalTime()) 117 | } 118 | 119 | @Test 120 | fun testDateTime_toLocalTime_Utc() { 121 | assertEquals(LocalTime.of(12, 30), DateTime("20200620T123000Z").apply { isUtc = true }.toLocalTime()) 122 | } 123 | 124 | 125 | @Test 126 | fun testDateTime_toZonedDateTime() { 127 | assertEquals( 128 | ZonedDateTime.of(2020, 7, 7, 10, 30, 0, 0, tzBerlin.toZoneIdCompat()), 129 | DateTime("20200707T103000", tzBerlin).toZonedDateTime() 130 | ) 131 | } 132 | 133 | @Test 134 | fun testDateTime_toZonedDateTime_Floating() { 135 | assertEquals( 136 | ZonedDateTime.of(2020, 7, 7, 10, 30, 0, 0, ZoneId.systemDefault()), 137 | DateTime("20200707T103000").toZonedDateTime() 138 | ) 139 | } 140 | 141 | @Test 142 | fun testDateTime_toZonedDateTime_UTC() { 143 | assertEquals( 144 | ZonedDateTime.of(2020, 7, 7, 10, 30, 0, 0, ZoneOffset.UTC), 145 | DateTime("20200707T103000Z").apply { isUtc = true }.toZonedDateTime() 146 | ) 147 | } 148 | 149 | 150 | @Test 151 | fun testLocalDate_toIcal4jDate() { 152 | assertEquals(Date("19000118"), LocalDate.of(1900, 1, 18).toIcal4jDate()) 153 | assertEquals(Date("20200620"), LocalDate.of(2020, 6, 20).toIcal4jDate()) 154 | } 155 | 156 | @Test 157 | fun testZonedDateTime_toIcal4jDateTime_NotUtc() { 158 | val tzBerlin = DateUtils.ical4jTimeZone("Europe/Berlin") 159 | assertEquals( 160 | DateTime("20200705T010203", tzBerlin), 161 | ZonedDateTime.of(2020, 7, 5, 1, 2, 3, 0, ZoneId.of("Europe/Berlin")).toIcal4jDateTime() 162 | ) 163 | } 164 | 165 | @Test 166 | fun testZonedDateTime_toIcal4jDateTime_Utc() { 167 | assertEquals( 168 | DateTime("20200705T010203Z"), 169 | ZonedDateTime.of(2020, 7, 5, 1, 2, 3, 0, ZoneOffset.UTC).toIcal4jDateTime() 170 | ) 171 | } 172 | 173 | 174 | @Test 175 | fun testTemporalAmount_toDuration() { 176 | assertEquals(Duration.ofHours(1), Duration.ofHours(1).toDuration(Instant.EPOCH)) 177 | assertEquals(Duration.ofDays(1), Duration.ofDays(1).toDuration(Instant.EPOCH)) 178 | assertEquals(Duration.ofDays(1), Period.ofDays(1).toDuration(Instant.EPOCH)) 179 | assertEquals(Duration.ofDays(7), Period.ofWeeks(1).toDuration(Instant.EPOCH)) 180 | assertEquals(Duration.ofDays(365), Period.ofYears(1).toDuration(Instant.EPOCH)) 181 | assertEquals(Duration.ofDays(366), Period.ofYears(1).toDuration(Instant.ofEpochSecond(1577836800))) 182 | } 183 | 184 | @Test 185 | fun testTemporalAmount_toRfc5545Duration_Duration() { 186 | assertEquals("P0S", Duration.ofDays(0).toRfc5545Duration(Instant.EPOCH)) 187 | assertEquals("P2W", Duration.ofDays(14).toRfc5545Duration(Instant.EPOCH)) 188 | assertEquals("P15D", Duration.ofDays(15).toRfc5545Duration(Instant.EPOCH)) 189 | assertEquals("P16DT1H", Duration.parse("P16DT1H").toRfc5545Duration(Instant.EPOCH)) 190 | assertEquals("P16DT1H4M", Duration.parse("P16DT1H4M").toRfc5545Duration(Instant.EPOCH)) 191 | assertEquals("P2DT1H4M5S", Duration.parse("P2DT1H4M5S").toRfc5545Duration(Instant.EPOCH)) 192 | assertEquals("PT1M20S", Duration.parse("PT80S").toRfc5545Duration(Instant.EPOCH)) 193 | 194 | assertEquals("P0D", Period.ofWeeks(0).toRfc5545Duration(Instant.EPOCH)) 195 | 196 | val date20200601 = Instant.ofEpochSecond(1590969600L) 197 | // 2020/06/01 + 1 year = 2021/06/01 (365 days) 198 | // 2021/06/01 + 2 months = 2020/08/01 (30 days + 31 days = 61 days) 199 | // 2020/08/01 + 3 days = 2020/08/04 (3 days) 200 | // total: 365 days + 61 days + 3 days = 429 days 201 | assertEquals("P429D", Period.of(1, 2, 3).toRfc5545Duration(date20200601)) 202 | assertEquals("P2W", Period.ofWeeks(2).toRfc5545Duration(date20200601)) 203 | assertEquals("P2W", Period.ofDays(14).toRfc5545Duration(date20200601)) 204 | assertEquals("P15D", Period.ofDays(15).toRfc5545Duration(date20200601)) 205 | assertEquals("P30D", Period.ofMonths(1).toRfc5545Duration(date20200601)) 206 | } 207 | 208 | } -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/all-day-0sec.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//hacksw/handcal//NONSGML v1.0//EN 4 | BEGIN:VEVENT 5 | UID:all-day-0sec@example.com 6 | DTSTAMP:20140101T000000Z 7 | DTSTART;VALUE=DATE:19970714 8 | DTEND;VALUE=DATE:19970714 9 | SUMMARY:0 Sec Event 10 | END:VEVENT 11 | END:VCALENDAR -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/all-day-10days.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//hacksw/handcal//NONSGML v1.0//EN 4 | BEGIN:VEVENT 5 | UID:all-day-10days@example.com 6 | DTSTAMP:20140101T000000Z 7 | DTSTART;VALUE=DATE:19970714 8 | DTEND;VALUE=DATE:19970724 9 | SUMMARY:All-Day 10 Days 10 | END:VEVENT 11 | END:VCALENDAR -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/all-day-1day.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//hacksw/handcal//NONSGML v1.0//EN 4 | BEGIN:VEVENT 5 | UID:all-day-1day@example.com 6 | DTSTAMP:20140101T000000Z 7 | DTSTART;VALUE=DATE:19970714 8 | DTEND;VALUE=DATE:19970715 9 | SUMMARY:All-Day 1 Day 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/dst-only-vtimezone.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VTIMEZONE 4 | TZID:Europe/Berlin 5 | BEGIN:DAYLIGHT 6 | TZOFFSETFROM:+0100 7 | TZOFFSETTO:+0200 8 | DTSTART:20180325T030000 9 | TZNAME:CEST 10 | END:DAYLIGHT 11 | END:VTIMEZONE 12 | BEGIN:VEVENT 13 | UID:only-dst@example.com 14 | DTSTAMP:20180329T084939Z 15 | DTSTART;TZID=Europe/Berlin:20180403T090000 16 | DTEND;TZID=Europe/Berlin:20180403T101500 17 | SUMMARY:Sample Event 18 | CREATED:20180329T084939Z 19 | LAST-MODIFIED:20180329T084939Z 20 | END:VEVENT 21 | END:VCALENDAR 22 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/event-on-that-day.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//hacksw/handcal//NONSGML v1.0//EN 4 | BEGIN:VEVENT 5 | UID:event-on-that-day@example.com 6 | DTSTAMP:19970714T170000Z 7 | ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com 8 | DTSTART;VALUE=DATE:19970714 9 | SUMMARY:Bastille Day Party 10 | END:VEVENT 11 | END:VCALENDAR -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/latin1.ics: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfireAT/ical4android/01bce4a8ee8395402a53c06263839ce3afe97e2b/lib/src/androidTest/resources/events/latin1.ics -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/multiple.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | X-WR-CALNAME:Test-Kalender 4 | BEGIN:VEVENT 5 | UID:multiple-0@ical4android.EventTest 6 | DTSTAMP:20150826T132300Z 7 | SUMMARY:Event 0 8 | DTSTART:20130910T170000T 9 | DTEND:20130910T180000T 10 | END:VEVENT 11 | BEGIN:VEVENT 12 | UID:multiple-1@ical4android.EventTest 13 | DTSTAMP:20150826T132300Z 14 | SUMMARY:Event 1 15 | RRULE:FREQ=DAILY;COUNT=10 16 | DTSTART;VALUE=DATE:20131009 17 | DTEND;VALUE=DATE:20131010 18 | END:VEVENT 19 | BEGIN:VEVENT 20 | UID:multiple-1@ical4android.EventTest 21 | RECURRENCE-ID;VALUE=DATE:202131012 22 | SUMMARY:Event 1 Exception 23 | DTSTART:20131012T170000T 24 | DTEND:20131012T180000T 25 | END:VEVENT 26 | BEGIN:VEVENT 27 | UID:multiple-2@ical4android.EventTest 28 | DTSTAMP:20150826T132300Z 29 | SUMMARY:Event 2 30 | DTSTART;VALUE=DATE:20131009 31 | DTEND;VALUE=DATE:20131010 32 | RRULE:FREQ=DAILY;COUNT=10 33 | END:VEVENT 34 | BEGIN:VEVENT 35 | UID:multiple-2@ical4android.EventTest 36 | RECURRENCE-ID:20131014 37 | SEQUENCE:1 38 | DTSTAMP:20150826T132300Z 39 | SUMMARY:Event 2 Updated Exception 1 40 | DTSTART:20131010T170000T 41 | DTEND:20131010T180000T 42 | END:VEVENT 43 | BEGIN:VEVENT 44 | UID:multiple-2@ical4android.EventTest 45 | RECURRENCE-ID:20131014 46 | DTSTAMP:20150826T132300Z 47 | SUMMARY:Event 2 Original Exception 1 48 | DTSTART:20131010T170000T 49 | DTEND:20131010T180000T 50 | END:VEVENT 51 | BEGIN:VEVENT 52 | UID:multiple-2@ical4android.EventTest 53 | RECURRENCE-ID:20131015 54 | DTSTAMP:20150826T132300Z 55 | SUMMARY:Event 2 Exception 2 56 | DTSTART:20131010T170000T 57 | DTEND:20131010T180000T 58 | END:VEVENT 59 | END:VCALENDAR 60 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/one-event-with-exception-one-without.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:event1 5 | RECURRENCE-ID:20131009T170000T 6 | DTSTAMP:20150826T132300Z 7 | SUMMARY:Event 1 Exception 8 | DTSTART:20131009T170000T 9 | DTEND:20131009T180000T 10 | END:VEVENT 11 | BEGIN:VEVENT 12 | UID:event1 13 | DTSTAMP:20150826T132300Z 14 | SUMMARY:Event 1 15 | DTSTART:20131009T170000T 16 | DTEND:20131009T180000T 17 | END:VEVENT 18 | BEGIN:VEVENT 19 | UID:event2 20 | DTSTAMP:20150826T132300Z 21 | SUMMARY:Event 2 22 | DTSTART:20131009T180000T 23 | DTEND:20131009T190000T 24 | END:VEVENT 25 | END:VCALENDAR 26 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/one-event-with-multiple-exceptions-one-without.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:event1 5 | RECURRENCE-ID;VALUE=DATE:20150503 6 | SEQUENCE:2 7 | DTSTART;VALUE=DATE:20150503 8 | DTEND;VALUE=DATE:20150504 9 | SUMMARY:Final summary 10 | END:VEVENT 11 | BEGIN:VEVENT 12 | UID:event1 13 | SUMMARY:Recurring event with one exception 14 | RRULE:FREQ=DAILY;COUNT=5 15 | DTSTART;VALUE=DATE:20150501 16 | DTEND;VALUE=DATE:20150502 17 | END:VEVENT 18 | BEGIN:VEVENT 19 | UID:event1 20 | RECURRENCE-ID;VALUE=DATE:20150503 21 | DTSTART;VALUE=DATE:20150503 22 | DTEND;VALUE=DATE:20150504 23 | SUMMARY:Another summary for the third day 24 | END:VEVENT 25 | BEGIN:VEVENT 26 | UID:event1 27 | RECURRENCE-ID;VALUE=DATE:20150504 28 | DTSTART;VALUE=DATE:20150504 29 | DTEND;VALUE=DATE:20150505 30 | SUMMARY:Another summary for the fourth day 31 | END:VEVENT 32 | BEGIN:VEVENT 33 | UID:event2 34 | DTSTAMP:20150826T132300Z 35 | SUMMARY:Event 2 36 | DTSTART:20131009T180000T 37 | DTEND:20131009T190000T 38 | END:VEVENT 39 | END:VCALENDAR 40 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/outlook1.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Calendar 6 | BEGIN:VTIMEZONE 7 | TZID:China Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T000000 10 | TZOFFSETFROM:+0800 11 | TZOFFSETTO:+0800 12 | END:STANDARD 13 | BEGIN:DAYLIGHT 14 | DTSTART:16010101T000000 15 | TZOFFSETFROM:+0800 16 | TZOFFSETTO:+0800 17 | END:DAYLIGHT 18 | END:VTIMEZONE 19 | BEGIN:VTIMEZONE 20 | TZID:W. Europe Standard Time 21 | BEGIN:STANDARD 22 | DTSTART:16010101T030000 23 | TZOFFSETFROM:+0200 24 | TZOFFSETTO:+0100 25 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 26 | END:STANDARD 27 | BEGIN:DAYLIGHT 28 | DTSTART:16010101T020000 29 | TZOFFSETFROM:+0100 30 | TZOFFSETTO:+0200 31 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 32 | END:DAYLIGHT 33 | END:VTIMEZONE 34 | BEGIN:VTIMEZONE 35 | TZID:India Standard Time 36 | BEGIN:STANDARD 37 | DTSTART:16010101T000000 38 | TZOFFSETFROM:+0530 39 | TZOFFSETTO:+0530 40 | END:STANDARD 41 | BEGIN:DAYLIGHT 42 | DTSTART:16010101T000000 43 | TZOFFSETFROM:+0530 44 | TZOFFSETTO:+0530 45 | END:DAYLIGHT 46 | END:VTIMEZONE 47 | BEGIN:VEVENT 48 | DESCRIPTION:\n............................................................. 49 | .................................\n\n\n 50 | RRULE:FREQ=WEEKLY;UNTIL=20191218T080000Z;INTERVAL=1;BYDAY=WE;WKST=MO 51 | UID:040000008200E00074C5B7101A82E00800000000907682BE2B88D501000000000000000 52 | 01000000077F6CDA3B634104B9BC1ED539119F558 53 | SUMMARY:Weekly Meeting - Test 54 | DTSTART;TZID=W. Europe Standard Time:20191023T090000 55 | DTEND;TZID=W. Europe Standard Time:20191023T093000 56 | CLASS:PUBLIC 57 | PRIORITY:5 58 | DTSTAMP:20191119T090349Z 59 | TRANSP:OPAQUE 60 | STATUS:CONFIRMED 61 | SEQUENCE:1 62 | LOCATION:Skype-Besprechung 63 | X-MICROSOFT-CDO-APPT-SEQUENCE:1 64 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 65 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 66 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 67 | X-MICROSOFT-CDO-IMPORTANCE:1 68 | X-MICROSOFT-CDO-INSTTYPE:1 69 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 70 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 71 | END:VEVENT 72 | END:VCALENDAR 73 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/recurring-only-exception.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:fcb42e4d-bc6e-4499-97f0-6616a02da7bc 5 | RECURRENCE-ID:20150503T010203Z 6 | DTSTART:20150503T010203Z 7 | DTEND:20150504T010203Z 8 | SUMMARY:This is an exception 9 | DESCRIPTION:The main event is not visible for us. 10 | END:VEVENT 11 | END:VCALENDAR 12 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/recurring-with-exception1.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:fcb42e4d-bc6e-4499-97f0-6616a02da7bc 5 | SUMMARY:Recurring event with one exception 6 | RRULE:FREQ=DAILY;COUNT=5 7 | DTSTART;VALUE=DATE:20150501 8 | DTEND;VALUE=DATE:20150502 9 | END:VEVENT 10 | BEGIN:VEVENT 11 | UID:fcb42e4d-bc6e-4499-97f0-6616a02da7bc 12 | RECURRENCE-ID;VALUE=DATE:20150503 13 | DTSTART;VALUE=DATE:20150503 14 | DTEND;VALUE=DATE:20150504 15 | SUMMARY:Another summary for the third day 16 | END:VEVENT 17 | END:VCALENDAR -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/two-events-without-exceptions.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:event1 5 | DTSTAMP:20150826T132300Z 6 | SUMMARY:Event 1 7 | DTSTART:20131009T170000T 8 | DTEND:20131009T180000T 9 | END:VEVENT 10 | BEGIN:VEVENT 11 | UID:event2 12 | DTSTAMP:20150826T132300Z 13 | SUMMARY:Event 2 14 | DTSTART:20131009T180000T 15 | DTEND:20131009T190000T 16 | END:VEVENT 17 | END:VCALENDAR 18 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/two-line-description-without-crlf.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:Blabla 4 | BEGIN:VEVENT 5 | CLASS:PUBLIC 6 | CREATED;VALUE=DATE-TIME:20131008T205713 7 | LAST-MODIFIED;VALUE=DATE-TIME:20131008T205740 8 | SUMMARY:online Anmeldung 9 | DESCRIPTION:http://www.tgbornheim.de/index.php?sessionid=&page=&id=&sportce 10 | ntergroup=&day=6 11 | UID:b99c41704b 12 | DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:20131019T060000 13 | END:VEVENT 14 | END:VCALENDAR 15 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/utf8.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VEVENT 4 | UID:utf8@ical4android.EventTest 5 | DTSTAMP:20150826T132300Z 6 | SUMMARY:© äö — üß 7 | DESCRIPTION:Test Description 8 | LOCATION:中华人民共和国 9 | COLOR:aliceblue 10 | ATTENDEE;CN=Cyrus Daboo;EMAIL=cyrus@example.com:mailto:opaque-token-1234@example.com 11 | X-UNKNOWN-PROP;param1=xxx:Unknown Value 12 | DTSTART:20131009T170000T 13 | DTEND:20131009T180000T 14 | END:VEVENT 15 | END:VCALENDAR 16 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/events/vienna-evolution.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//Ximian//NONSGML Evolution Calendar//EN 3 | VERSION:2.0 4 | METHOD:PUBLISH 5 | BEGIN:VTIMEZONE 6 | TZID:/freeassociation.sourceforge.net/Tzfile/Europe/Vienna 7 | X-LIC-LOCATION:Europe/Vienna 8 | BEGIN:STANDARD 9 | TZNAME:CET 10 | DTSTART:19701027T030000 11 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 12 | TZOFFSETFROM:+0200 13 | TZOFFSETTO:+0100 14 | END:STANDARD 15 | BEGIN:DAYLIGHT 16 | TZNAME:CEST 17 | DTSTART:19700331T020000 18 | RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 19 | TZOFFSETFROM:+0100 20 | TZOFFSETTO:+0200 21 | END:DAYLIGHT 22 | END:VTIMEZONE 23 | BEGIN:VEVENT 24 | UID:c252087c-7354-4722-aea9-0e7d86c01a25 25 | DTSTAMP:20130926T151211Z 26 | SUMMARY:Test-Ereignis im schönen Wien 27 | DTSTART;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T170000 28 | DTEND;TZID=/freeassociation.sourceforge.net/Tzfile/Europe/Vienna:20131009T180000 29 | X-RADICALE-NAME:97929342-291a-434e-bf1a-fa1749bf99d0.ics 30 | X-EVOLUTION-CALDAV-HREF:/radicale/rfc2822/default.ics/97929342-291a-434e-bf1a-fa1749bf99d0.ics 31 | X-EVOLUTION-CALDAV-ETAG:\"-3264224243575339985\" 32 | END:VEVENT 33 | END:VCALENDAR 34 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/all-day.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//hacksw/handcal//NONSGML v1.0//EN 4 | BEGIN:VJOURNAL 5 | UID:all-day-1day@example.com 6 | DTSTAMP:20140101T000000Z 7 | DTSTART;VALUE=DATE:19970714 8 | SUMMARY:All-Day 1 Day 9 | END:VJOURNAL 10 | END:VCALENDAR 11 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/default-example-note.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//hacksw/handcal//NONSGML v1.0//EN 4 | BEGIN:VJOURNAL 5 | UID:19970901T130000Z-123405@example.com 6 | DTSTAMP:19970901T130000Z 7 | SUMMARY:Staff meeting minutes 8 | DESCRIPTION:1. Staff meeting: Participants include Joe\, 9 | Lisa\, and Bob. Aurora project plans were reviewed. 10 | There is currently no budget reserves for this project. 11 | Lisa will escalate to management. Next meeting on Tuesday.\n 12 | 2. Telephone Conference: ABC Corp. sales representative 13 | called to discuss new printer. Promised to get us a demo by 14 | Friday.\n3. Henry Miller (Handsoff Insurance): Car was 15 | totaled by tree. Is looking into a loaner car. 555-2323 16 | (tel). 17 | END:VJOURNAL 18 | END:VCALENDAR -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/default-example.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//hacksw/handcal//NONSGML v1.0//EN 4 | BEGIN:VJOURNAL 5 | UID:19970901T130000Z-123405@example.com 6 | DTSTAMP:19970901T130000Z 7 | DTSTART;VALUE=DATE:19970317 8 | SUMMARY:Staff meeting minutes 9 | DESCRIPTION:1. Staff meeting: Participants include Joe\, 10 | Lisa\, and Bob. Aurora project plans were reviewed. 11 | There is currently no budget reserves for this project. 12 | Lisa will escalate to management. Next meeting on Tuesday.\n 13 | 2. Telephone Conference: ABC Corp. sales representative 14 | called to discuss new printer. Promised to get us a demo by 15 | Friday.\n3. Henry Miller (Handsoff Insurance): Car was 16 | totaled by tree. Is looking into a loaner car. 555-2323 17 | (tel). 18 | END:VJOURNAL 19 | END:VCALENDAR -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/dst-only-vtimezone.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VTIMEZONE 4 | TZID:Europe/Berlin 5 | BEGIN:DAYLIGHT 6 | TZOFFSETFROM:+0100 7 | TZOFFSETTO:+0200 8 | DTSTART:20180325T030000 9 | TZNAME:CEST 10 | END:DAYLIGHT 11 | END:VTIMEZONE 12 | BEGIN:VJOURNAL 13 | UID:only-dst@example.com 14 | DTSTAMP:20180329T084939Z 15 | DTSTART;TZID=Europe/Berlin:20180403T090000 16 | DTEND;TZID=Europe/Berlin:20180403T101500 17 | SUMMARY:Sample Event 18 | CREATED:20180329T084939Z 19 | LAST-MODIFIED:20180329T084939Z 20 | END:VJOURNAL 21 | END:VCALENDAR 22 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/journal-on-that-day.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//hacksw/handcal//NONSGML v1.0//EN 4 | BEGIN:VJOURNAL 5 | UID:event-on-that-day@example.com 6 | DTSTAMP:19970714T170000Z 7 | ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com 8 | DTSTART;VALUE=DATE:19970714 9 | SUMMARY:Bastille Day Party 10 | END:VJOURNAL 11 | END:VCALENDAR -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/latin1.ics: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfireAT/ical4android/01bce4a8ee8395402a53c06263839ce3afe97e2b/lib/src/androidTest/resources/jtx/vjournal/latin1.ics -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/outlook-theoretical.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Calendar 6 | BEGIN:VTIMEZONE 7 | TZID:China Standard Time 8 | BEGIN:STANDARD 9 | DTSTART:16010101T000000 10 | TZOFFSETFROM:+0800 11 | TZOFFSETTO:+0800 12 | END:STANDARD 13 | BEGIN:DAYLIGHT 14 | DTSTART:16010101T000000 15 | TZOFFSETFROM:+0800 16 | TZOFFSETTO:+0800 17 | END:DAYLIGHT 18 | END:VTIMEZONE 19 | BEGIN:VTIMEZONE 20 | TZID:W. Europe Standard Time 21 | BEGIN:STANDARD 22 | DTSTART:16010101T030000 23 | TZOFFSETFROM:+0200 24 | TZOFFSETTO:+0100 25 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10 26 | END:STANDARD 27 | BEGIN:DAYLIGHT 28 | DTSTART:16010101T020000 29 | TZOFFSETFROM:+0100 30 | TZOFFSETTO:+0200 31 | RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3 32 | END:DAYLIGHT 33 | END:VTIMEZONE 34 | BEGIN:VTIMEZONE 35 | TZID:India Standard Time 36 | BEGIN:STANDARD 37 | DTSTART:16010101T000000 38 | TZOFFSETFROM:+0530 39 | TZOFFSETTO:+0530 40 | END:STANDARD 41 | BEGIN:DAYLIGHT 42 | DTSTART:16010101T000000 43 | TZOFFSETFROM:+0530 44 | TZOFFSETTO:+0530 45 | END:DAYLIGHT 46 | END:VTIMEZONE 47 | BEGIN:VJOURNAL 48 | DESCRIPTION:\n............................................................. 49 | .................................\n\n\n 50 | RRULE:FREQ=WEEKLY;UNTIL=20191218T080000Z;INTERVAL=1;BYDAY=WE;WKST=MO 51 | UID:040000008200E00074C5B7101A82E00800000000907682BE2B88D501000000000000000 52 | 01000000077F6CDA3B634104B9BC1ED539119F558 53 | SUMMARY:Weekly Meeting - Test 54 | DTSTART;TZID=W. Europe Standard Time:20191023T090000 55 | CLASS:PUBLIC 56 | PRIORITY:5 57 | DTSTAMP:20191119T090349Z 58 | TRANSP:OPAQUE 59 | STATUS:DRAFT 60 | SEQUENCE:1 61 | LOCATION:Skype-Besprechung 62 | X-MICROSOFT-CDO-APPT-SEQUENCE:1 63 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 64 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 65 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 66 | X-MICROSOFT-CDO-IMPORTANCE:1 67 | X-MICROSOFT-CDO-INSTTYPE:1 68 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 69 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 70 | END:VJOURNAL 71 | END:VCALENDAR 72 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/outlook-theoretical2.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | METHOD:PUBLISH 3 | PRODID:Microsoft Exchange Server 2010 4 | VERSION:2.0 5 | X-WR-CALNAME:Calendar 6 | BEGIN:VJOURNAL 7 | DESCRIPTION:\n............................................................. 8 | .................................\n\n\n 9 | RRULE:FREQ=WEEKLY;UNTIL=20191218T080000Z;INTERVAL=1;BYDAY=WE;WKST=MO 10 | UID:040000008200E00074C5B7101A82E00800000000907682BE2B88D501000000000000000 11 | 01000000077F6CDA3B634104B9BC1ED539119F558 12 | SUMMARY:Weekly Meeting - Test 13 | DTSTART:20191023T090000 14 | CLASS:PUBLIC 15 | DTSTAMP:20191119T090349Z 16 | TRANSP:OPAQUE 17 | STATUS:DRAFT 18 | SEQUENCE:1 19 | LOCATION:Skype-Besprechung 20 | X-MICROSOFT-CDO-APPT-SEQUENCE:1 21 | X-MICROSOFT-CDO-BUSYSTATUS:BUSY 22 | X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY 23 | X-MICROSOFT-CDO-ALLDAYEVENT:FALSE 24 | X-MICROSOFT-CDO-IMPORTANCE:1 25 | X-MICROSOFT-CDO-INSTTYPE:1 26 | X-MICROSOFT-DONOTFORWARDMEETING:FALSE 27 | X-MICROSOFT-DISALLOW-COUNTER:FALSE 28 | END:VJOURNAL 29 | END:VCALENDAR 30 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/recurring.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VJOURNAL 4 | UID:fcb42e4d-bc6e-4499-97f0-6616a02da7bc 5 | SUMMARY:Recurring event with one exception 6 | RRULE:FREQ=DAILY;COUNT=5 7 | DTSTART;VALUE=DATE:20150501 8 | END:VJOURNAL 9 | END:VCALENDAR -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/two-events-without-exceptions.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VJOURNAL 4 | UID:event1 5 | DTSTAMP:20150826T132300Z 6 | SUMMARY:Event 1 7 | DTSTART:20131009T170000T 8 | END:VJOURNAL 9 | BEGIN:VJOURNAL 10 | UID:event2 11 | DTSTAMP:20150826T132300Z 12 | SUMMARY:Event 2 13 | DTSTART:20131009T180000T 14 | END:VJOURNAL 15 | END:VCALENDAR -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/two-line-description-without-crlf.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:Blabla 4 | BEGIN:VJOURNAL 5 | CLASS:PUBLIC 6 | CREATED;VALUE=DATE-TIME:20131008T205713 7 | LAST-MODIFIED;VALUE=DATE-TIME:20131008T205740 8 | SUMMARY:online Anmeldung 9 | DESCRIPTION:http://www.tgbornheim.de/index.php?sessionid=&page=&id=&sportce 10 | ntergroup=&day=6 11 | UID:b99c41704b 12 | DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:20131019T060000 13 | END:VJOURNAL 14 | END:VCALENDAR 15 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vjournal/utf8.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VJOURNAL 4 | UID:utf8@ical4android.EventTest 5 | DTSTAMP:20150826T132300Z 6 | SUMMARY:© äö — üß 7 | DESCRIPTION:Test Description 8 | LOCATION:中华人民共和国 9 | COLOR:aliceblue 10 | ATTENDEE;CN=Cyrus Daboo;EMAIL=cyrus@example.com:mailto:opaque-token-1234@example.com 11 | X-UNKNOWN-PROP;param1=xxx:Unknown Value 12 | END:VJOURNAL 13 | END:VCALENDAR 14 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vtodo/empty-priority.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | X-WR-CALNAME:Wieseclan 4 | PRODID:-//The Horde Project//Horde iCalendar Library//EN 5 | BEGIN:VTODO 6 | UID:1d58e8e0-4fac-4632-8d43-ec79757490d2.1519164997000 7 | SUMMARY:Alex Geschenkeliste 8 | DESCRIPTION:[ ] Tip Toi Rund um die Uhr\n[ ] Ravensburger Wort für Wort\n[ 9 | ] Hula Hoop Reifen\n[ ] Pfeil und Bogen mit Zielscheibe\n[ ] Tip Toi 10 | Spielwelt Autorennen 11 | PRIORITY: 12 | STATUS:NEEDS-ACTION 13 | END:VTODO 14 | END:VCALENDAR 15 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vtodo/latin1.ics: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfireAT/ical4android/01bce4a8ee8395402a53c06263839ce3afe97e2b/lib/src/androidTest/resources/jtx/vtodo/latin1.ics -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vtodo/most-fields1.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//ABC Corporation//NONSGML My Product//EN 4 | BEGIN:VTODO 5 | SEQUENCE:1 6 | UID:most-fields1@example.com 7 | LOCATION;ALTREP="http://xyzcorp.com/conf-rooms/f123.vcf": 8 | Conference Room - F123\, Bldg. 002 9 | GEO:37.386013;-122.082932 10 | DESCRIPTION:Meeting to provide technical review for "Phoenix" 11 | design.\nHappy Face Conference Room. Phoenix design team 12 | MUST attend this meeting.\nRSVP to team leader. 13 | URL:http://example.com/pub/calendars/jsmith/mytime.ics 14 | ORGANIZER:http://example.com/principals/jsmith 15 | PRIORITY:1 16 | CLASS:CONFIDENTIAL 17 | STATUS:IN-PROCESS 18 | PERCENT-COMPLETE:25 19 | DTSTART;VALUE=DATE:20100101 20 | DUE;VALUE=DATE:20101001 21 | CATEGORIES:Test,Sample 22 | RRULE:FREQ=YEARLY;INTERVAL=2 23 | EXDATE;VALUE=DATE:20120101 24 | EXDATE;VALUE=DATE:20140101,20180101 25 | RDATE;VALUE=DATE:20100310,20100315 26 | RDATE;VALUE=DATE:20100810 27 | RELATED-TO;RELTYPE=SIBLING:most-fields2@example.com 28 | X-UNKNOWN-PROP;param1=xxx:Unknown Value 29 | CREATED:19960329T133000Z 30 | LAST-MODIFIED:19960817T133000Z 31 | END:VTODO 32 | END:VCALENDAR 33 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vtodo/most-fields2.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//ABC Corporation//NONSGML My Product//EN 4 | BEGIN:VTODO 5 | UID:most-fields2@example.com 6 | DTSTART:20100101T101010Z 7 | DURATION:P4DT3H2M1S 8 | END:VTODO 9 | END:VCALENDAR 10 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vtodo/rfc5545-sample1.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//ABC Corporation//NONSGML My Product//EN 4 | BEGIN:VTODO 5 | DTSTAMP:19980130T134500Z 6 | SEQUENCE:2 7 | UID:uid4@example.com 8 | ORGANIZER:mailto:unclesam@example.com 9 | ATTENDEE;PARTSTAT=ACCEPTED:mailto:jqpublic@example.com 10 | DUE:19980415T000000 11 | STATUS:NEEDS-ACTION 12 | SUMMARY:Submit Income Taxes 13 | BEGIN:VALARM 14 | ACTION:AUDIO 15 | TRIGGER;VALUE=DATE-TIME:19980403T120000Z 16 | ATTACH;FMTTYPE=audio/basic:http://example.com/pub/audio- 17 | files/ssbanner.aud 18 | REPEAT:4 19 | DURATION:PT1H 20 | END:VALARM 21 | END:VTODO 22 | END:VCALENDAR 23 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/jtx/vtodo/utf8.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VTODO 4 | UID:utf8@ical4android.TaskTest 5 | DTSTAMP:20150826T132300Z 6 | SUMMARY:© äö — üß 7 | LOCATION:中华人民共和国 8 | DTSTART:20131009T170000T 9 | END:VTODO 10 | END:VCALENDAR 11 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/tasks/empty-priority.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | X-WR-CALNAME:Wieseclan 4 | PRODID:-//The Horde Project//Horde iCalendar Library//EN 5 | BEGIN:VTODO 6 | UID:1d58e8e0-4fac-4632-8d43-ec79757490d2.1519164997000 7 | SUMMARY:Alex Geschenkeliste 8 | DESCRIPTION:[ ] Tip Toi Rund um die Uhr\n[ ] Ravensburger Wort für Wort\n[ 9 | ] Hula Hoop Reifen\n[ ] Pfeil und Bogen mit Zielscheibe\n[ ] Tip Toi 10 | Spielwelt Autorennen 11 | PRIORITY: 12 | STATUS:NEEDS-ACTION 13 | END:VTODO 14 | END:VCALENDAR 15 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/tasks/latin1.ics: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bitfireAT/ical4android/01bce4a8ee8395402a53c06263839ce3afe97e2b/lib/src/androidTest/resources/tasks/latin1.ics -------------------------------------------------------------------------------- /lib/src/androidTest/resources/tasks/most-fields1.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//ABC Corporation//NONSGML My Product//EN 4 | BEGIN:VTODO 5 | SEQUENCE:1 6 | UID:most-fields1@example.com 7 | LOCATION;ALTREP="http://xyzcorp.com/conf-rooms/f123.vcf": 8 | Conference Room - F123\, Bldg. 002 9 | GEO:37.386013;-122.082932 10 | DESCRIPTION:Meeting to provide technical review for "Phoenix" 11 | design.\nHappy Face Conference Room. Phoenix design team 12 | MUST attend this meeting.\nRSVP to team leader. 13 | URL:http://example.com/pub/calendars/jsmith/mytime.ics 14 | ORGANIZER:http://example.com/principals/jsmith 15 | PRIORITY:1 16 | CLASS:CONFIDENTIAL 17 | STATUS:IN-PROCESS 18 | PERCENT-COMPLETE:25 19 | DTSTART;VALUE=DATE:20100101 20 | DUE;VALUE=DATE:20101001 21 | CATEGORIES:Test,Sample 22 | RRULE:FREQ=YEARLY;INTERVAL=2 23 | EXDATE;VALUE=DATE:20120101 24 | EXDATE;VALUE=DATE:20140101,20180101 25 | RDATE;VALUE=DATE:20100310,20100315 26 | RDATE;VALUE=DATE:20100810 27 | RELATED-TO;RELTYPE=SIBLING:most-fields2@example.com 28 | X-UNKNOWN-PROP;param1=xxx:Unknown Value 29 | CREATED:19960329T133000Z 30 | LAST-MODIFIED:19960817T133000Z 31 | END:VTODO 32 | END:VCALENDAR 33 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/tasks/most-fields2.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//ABC Corporation//NONSGML My Product//EN 4 | BEGIN:VTODO 5 | UID:most-fields2@example.com 6 | DTSTART:20100101T101010Z 7 | DURATION:P4DT3H2M1S 8 | END:VTODO 9 | END:VCALENDAR 10 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/tasks/rfc5545-sample1.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | PRODID:-//ABC Corporation//NONSGML My Product//EN 4 | BEGIN:VTODO 5 | DTSTAMP:19980130T134500Z 6 | SEQUENCE:2 7 | UID:uid4@example.com 8 | ORGANIZER:mailto:unclesam@example.com 9 | ATTENDEE;PARTSTAT=ACCEPTED:mailto:jqpublic@example.com 10 | DUE:19980415T000000 11 | STATUS:NEEDS-ACTION 12 | SUMMARY:Submit Income Taxes 13 | BEGIN:VALARM 14 | ACTION:AUDIO 15 | TRIGGER;VALUE=DATE-TIME:19980403T120000Z 16 | ATTACH;FMTTYPE=audio/basic:http://example.com/pub/audio- 17 | files/ssbanner.aud 18 | REPEAT:4 19 | DURATION:PT1H 20 | END:VALARM 21 | END:VTODO 22 | END:VCALENDAR 23 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/tasks/utf8.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | VERSION:2.0 3 | BEGIN:VTODO 4 | UID:utf8@ical4android.TaskTest 5 | DTSTAMP:20150826T132300Z 6 | SUMMARY:© äö — üß 7 | LOCATION:中华人民共和国 8 | DTSTART:20131009T170000T 9 | END:VTODO 10 | END:VCALENDAR 11 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/tz/Karachi.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//tzurl.org//NONSGML Olson 2018g-rearguard//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Asia/Karachi 6 | TZURL:http://tzurl.org/zoneinfo/Asia/Karachi 7 | X-LIC-LOCATION:Asia/Karachi 8 | BEGIN:STANDARD 9 | TZOFFSETFROM:+042812 10 | TZOFFSETTO:+0530 11 | TZNAME:+0530 12 | DTSTART:19070101T000000 13 | RDATE:19070101T000000 14 | END:STANDARD 15 | BEGIN:DAYLIGHT 16 | TZOFFSETFROM:+0530 17 | TZOFFSETTO:+0630 18 | TZNAME:+0630 19 | DTSTART:19420901T000000 20 | RDATE:19420901T000000 21 | END:DAYLIGHT 22 | BEGIN:STANDARD 23 | TZOFFSETFROM:+0630 24 | TZOFFSETTO:+0530 25 | TZNAME:+0530 26 | DTSTART:19451015T000000 27 | RDATE:19451015T000000 28 | END:STANDARD 29 | BEGIN:STANDARD 30 | TZOFFSETFROM:+0530 31 | TZOFFSETTO:+0500 32 | TZNAME:+05 33 | DTSTART:19510930T000000 34 | RDATE:19510930T000000 35 | END:STANDARD 36 | BEGIN:STANDARD 37 | TZOFFSETFROM:+0500 38 | TZOFFSETTO:+0500 39 | TZNAME:PKT 40 | DTSTART:19710326T000000 41 | RDATE:19710326T000000 42 | END:STANDARD 43 | BEGIN:DAYLIGHT 44 | TZOFFSETFROM:+0500 45 | TZOFFSETTO:+0600 46 | TZNAME:PKST 47 | DTSTART:20020407T000000 48 | RDATE:20020407T000000 49 | RDATE:20080601T000000 50 | RDATE:20090415T000000 51 | END:DAYLIGHT 52 | BEGIN:STANDARD 53 | TZOFFSETFROM:+0600 54 | TZOFFSETTO:+0500 55 | TZNAME:PKT 56 | DTSTART:20021006T000000 57 | RDATE:20021006T000000 58 | RDATE:20081101T000000 59 | RDATE:20091101T000000 60 | END:STANDARD 61 | END:VTIMEZONE 62 | END:VCALENDAR 63 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/tz/Mogadishu.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//tzurl.org//NONSGML Olson 2018g-rearguard//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Africa/Mogadishu 6 | TZURL:http://tzurl.org/zoneinfo/Africa/Mogadishu 7 | X-LIC-LOCATION:Africa/Mogadishu 8 | BEGIN:STANDARD 9 | TZOFFSETFROM:+022716 10 | TZOFFSETTO:+0300 11 | TZNAME:EAT 12 | DTSTART:19280701T000000 13 | RDATE:19280701T000000 14 | END:STANDARD 15 | BEGIN:STANDARD 16 | TZOFFSETFROM:+0300 17 | TZOFFSETTO:+0230 18 | TZNAME:+0230 19 | DTSTART:19300101T000000 20 | RDATE:19300101T000000 21 | END:STANDARD 22 | BEGIN:STANDARD 23 | TZOFFSETFROM:+0230 24 | TZOFFSETTO:+0245 25 | TZNAME:+0245 26 | DTSTART:19400101T000000 27 | RDATE:19400101T000000 28 | END:STANDARD 29 | BEGIN:STANDARD 30 | TZOFFSETFROM:+0245 31 | TZOFFSETTO:+0300 32 | TZNAME:EAT 33 | DTSTART:19600101T000000 34 | RDATE:19600101T000000 35 | END:STANDARD 36 | END:VTIMEZONE 37 | END:VCALENDAR 38 | -------------------------------------------------------------------------------- /lib/src/androidTest/resources/tz/Vienna.ics: -------------------------------------------------------------------------------- 1 | BEGIN:VCALENDAR 2 | PRODID:-//tzurl.org//NONSGML Olson 2018g-rearguard//EN 3 | VERSION:2.0 4 | BEGIN:VTIMEZONE 5 | TZID:Europe/Vienna 6 | TZURL:http://tzurl.org/zoneinfo/Europe/Vienna 7 | X-LIC-LOCATION:Europe/Vienna 8 | BEGIN:DAYLIGHT 9 | TZOFFSETFROM:+0100 10 | TZOFFSETTO:+0200 11 | TZNAME:CEST 12 | DTSTART:19810329T020000 13 | RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU 14 | END:DAYLIGHT 15 | BEGIN:STANDARD 16 | TZOFFSETFROM:+0200 17 | TZOFFSETTO:+0100 18 | TZNAME:CET 19 | DTSTART:19961027T030000 20 | RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU 21 | END:STANDARD 22 | BEGIN:STANDARD 23 | TZOFFSETFROM:+010521 24 | TZOFFSETTO:+0100 25 | TZNAME:CET 26 | DTSTART:18930401T000000 27 | RDATE:18930401T000000 28 | END:STANDARD 29 | BEGIN:DAYLIGHT 30 | TZOFFSETFROM:+0100 31 | TZOFFSETTO:+0200 32 | TZNAME:CEST 33 | DTSTART:19160430T230000 34 | RDATE:19160430T230000 35 | RDATE:19170416T020000 36 | RDATE:19180415T020000 37 | RDATE:19200405T020000 38 | RDATE:19400401T020000 39 | RDATE:19430329T020000 40 | RDATE:19440403T020000 41 | RDATE:19450402T020000 42 | RDATE:19460414T020000 43 | RDATE:19470406T020000 44 | RDATE:19480418T020000 45 | RDATE:19800406T000000 46 | END:DAYLIGHT 47 | BEGIN:STANDARD 48 | TZOFFSETFROM:+0200 49 | TZOFFSETTO:+0100 50 | TZNAME:CET 51 | DTSTART:19161001T010000 52 | RDATE:19161001T010000 53 | RDATE:19170917T030000 54 | RDATE:19180916T030000 55 | RDATE:19200913T030000 56 | RDATE:19421102T030000 57 | RDATE:19431004T030000 58 | RDATE:19441002T030000 59 | RDATE:19450412T030000 60 | RDATE:19461006T030000 61 | RDATE:19471005T030000 62 | RDATE:19481003T030000 63 | RDATE:19800928T000000 64 | RDATE:19810927T030000 65 | RDATE:19820926T030000 66 | RDATE:19830925T030000 67 | RDATE:19840930T030000 68 | RDATE:19850929T030000 69 | RDATE:19860928T030000 70 | RDATE:19870927T030000 71 | RDATE:19880925T030000 72 | RDATE:19890924T030000 73 | RDATE:19900930T030000 74 | RDATE:19910929T030000 75 | RDATE:19920927T030000 76 | RDATE:19930926T030000 77 | RDATE:19940925T030000 78 | RDATE:19950924T030000 79 | END:STANDARD 80 | BEGIN:STANDARD 81 | TZOFFSETFROM:+0100 82 | TZOFFSETTO:+0100 83 | TZNAME:CET 84 | DTSTART:19200101T000000 85 | RDATE:19200101T000000 86 | RDATE:19460101T000000 87 | RDATE:19810101T000000 88 | END:STANDARD 89 | END:VTIMEZONE 90 | END:VCALENDAR 91 | -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/AndroidCalendarFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.accounts.Account 10 | import android.content.ContentProviderClient 11 | 12 | interface AndroidCalendarFactory> { 13 | 14 | fun newInstance(account: Account, provider: ContentProviderClient, id: Long): T 15 | 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/AndroidCompatTimeZoneRegistry.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import java.time.ZoneId 10 | import java.util.logging.Logger 11 | import net.fortuna.ical4j.model.DefaultTimeZoneRegistryFactory 12 | import net.fortuna.ical4j.model.Property 13 | import net.fortuna.ical4j.model.PropertyList 14 | import net.fortuna.ical4j.model.TimeZone 15 | import net.fortuna.ical4j.model.TimeZoneRegistry 16 | import net.fortuna.ical4j.model.TimeZoneRegistryFactory 17 | import net.fortuna.ical4j.model.TimeZoneRegistryImpl 18 | import net.fortuna.ical4j.model.component.VTimeZone 19 | import net.fortuna.ical4j.model.property.TzId 20 | 21 | /** 22 | * Wrapper around default [TimeZoneRegistry] that uses the Android name if a time zone has a 23 | * different name in ical4j and Android. 24 | * 25 | * **This time zone registry is set as default registry for ical4android projects in 26 | * resources/ical4j.properties.** 27 | * 28 | * For instance, if a time zone is known as "Europe/Kyiv" (with alias "Europe/Kiev") in ical4j 29 | * and only "Europe/Kiev" in Android, this registry behaves like the default [TimeZoneRegistryImpl], 30 | * but the returned time zone for `getTimeZone("Europe/Kiev")` has an ID of "Europe/Kiev" and not 31 | * "Europe/Kyiv". 32 | */ 33 | class AndroidCompatTimeZoneRegistry( 34 | private val base: TimeZoneRegistry 35 | ): TimeZoneRegistry by base { 36 | 37 | private val logger 38 | get() = Logger.getLogger(javaClass.name) 39 | 40 | /** 41 | * Gets the time zone for a given ID. 42 | * 43 | * If a time zone with the given ID exists in Android, the icalj timezone for this ID 44 | * is returned, but the TZID is set to the Android name (and not the ical4j name, which 45 | * may not be known to Android). 46 | * 47 | * If a time zone with the given ID doesn't exist in Android, this method returns the 48 | * result of its [base] method. 49 | * 50 | * @param id 51 | * @return time zone 52 | */ 53 | override fun getTimeZone(id: String): TimeZone? { 54 | val tz: TimeZone = base.getTimeZone(id) 55 | ?: return null // ical4j doesn't know time zone, return null 56 | 57 | // check whether time zone is available on Android, too 58 | val androidTzId = 59 | try { 60 | ZoneId.of(id).id 61 | } catch (e: Exception) { 62 | /* Not available in Android, should return null in a later version. 63 | However, we return the ical4j timezone to keep the changes caused by AndroidCompatTimeZoneRegistry introduction 64 | as small as possible. */ 65 | return tz 66 | } 67 | 68 | /* Time zone known by Android. Unfortunately, we can't use the Android timezone database directly 69 | to generate ical4j timezone definitions (which are based on VTIMEZONE). 70 | So we have to use the timezone definition from ical4j (based on its own VTIMEZONE database), 71 | but we also need to use the Android TZ name (otherwise Android may not understand it later). 72 | 73 | Example: getTimeZone("Europe/Kiev") returns a TimeZone with TZID:Europe/Kyiv since ical4j/3.2.5, 74 | but most Android devices don't now Europe/Kyiv yet. 75 | */ 76 | if (tz.id != androidTzId) { 77 | logger.fine("Using ical4j timezone ${tz.id} data to construct Android timezone $androidTzId") 78 | 79 | // create a copy of the VTIMEZONE so that we don't modify the original registry values (which are not immutable) 80 | val vTimeZone = tz.vTimeZone 81 | val newVTimeZoneProperties = PropertyList() 82 | newVTimeZoneProperties += TzId(androidTzId) 83 | return TimeZone(VTimeZone( 84 | newVTimeZoneProperties, 85 | vTimeZone.observances 86 | )) 87 | } else 88 | return tz 89 | } 90 | 91 | 92 | class Factory : TimeZoneRegistryFactory() { 93 | 94 | override fun createRegistry(): AndroidCompatTimeZoneRegistry { 95 | val ical4jRegistry = DefaultTimeZoneRegistryFactory().createRegistry() 96 | return AndroidCompatTimeZoneRegistry(ical4jRegistry) 97 | } 98 | 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/AndroidEventFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.content.ContentValues 10 | 11 | interface AndroidEventFactory { 12 | 13 | fun fromProvider(calendar: AndroidCalendar, values: ContentValues): T 14 | 15 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/AttendeeMappings.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.content.ContentValues 10 | import android.provider.CalendarContract 11 | import android.provider.CalendarContract.Attendees 12 | import net.fortuna.ical4j.model.Parameter 13 | import net.fortuna.ical4j.model.parameter.CuType 14 | import net.fortuna.ical4j.model.parameter.Email 15 | import net.fortuna.ical4j.model.parameter.Role 16 | import net.fortuna.ical4j.model.property.Attendee 17 | 18 | /** 19 | * Defines mappings between Android [CalendarContract.Attendees] and iCalendar parameters. 20 | * 21 | * Because the available Android values are quite different from the one in iCalendar, the 22 | * mapping is very lossy. Some special mapping rules are defined: 23 | * 24 | * - ROLE=CHAIR ⇄ ATTENDEE_TYPE=TYPE_SPEAKER 25 | * - CUTYPE=GROUP ⇄ ATTENDEE_TYPE=TYPE_PERFORMER 26 | * - CUTYPE=ROOM ⇄ ATTENDEE_TYPE=TYPE_RESOURCE, ATTENDEE_RELATIONSHIP=RELATIONSHIP_PERFORMER 27 | */ 28 | object AttendeeMappings { 29 | 30 | /** 31 | * Maps Android [Attendees.ATTENDEE_TYPE] and [Attendees.ATTENDEE_RELATIONSHIP] to 32 | * iCalendar [CuType] and [Role] according to this matrix: 33 | * 34 | * TYPE ↓ / RELATIONSHIP → ATTENDEE¹ PERFORMER SPEAKER NONE 35 | * REQUIRED indᴰ,reqᴰ gro,reqᴰ indᴰ,cha unk,reqᴰ 36 | * OPTIONAL indᴰ,opt gro,opt indᴰ,cha unk,opt 37 | * NONE indᴰ,reqᴰ gro,reqᴰ indᴰ,cha unk,reqᴰ 38 | * RESOURCE res,reqᴰ roo,reqᴰ res,cha res,reqᴰ 39 | * 40 | * ᴰ default value 41 | * ¹ includes ORGANIZER 42 | * 43 | * @param row Android attendee row to map 44 | * @param attendee iCalendar attendee to fill 45 | */ 46 | fun androidToICalendar(row: ContentValues, attendee: Attendee) { 47 | val type = row.getAsInteger(Attendees.ATTENDEE_TYPE) ?: Attendees.TYPE_NONE 48 | val relationship = row.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP) ?: Attendees.RELATIONSHIP_NONE 49 | 50 | var cuType: CuType? = null 51 | val role: Role? 52 | 53 | if (relationship == Attendees.RELATIONSHIP_SPEAKER) { 54 | role = Role.CHAIR 55 | if (type == Attendees.TYPE_RESOURCE) 56 | cuType = CuType.RESOURCE 57 | 58 | } else /* relationship != Attendees.RELATIONSHIP_SPEAKER */ { 59 | 60 | cuType = when (relationship) { 61 | Attendees.RELATIONSHIP_PERFORMER -> CuType.GROUP 62 | Attendees.RELATIONSHIP_NONE -> CuType.UNKNOWN 63 | else -> CuType.INDIVIDUAL 64 | } 65 | 66 | when (type) { 67 | Attendees.TYPE_OPTIONAL -> role = Role.OPT_PARTICIPANT 68 | Attendees.TYPE_RESOURCE -> { 69 | cuType = 70 | if (relationship == Attendees.RELATIONSHIP_PERFORMER) 71 | CuType.ROOM 72 | else 73 | CuType.RESOURCE 74 | role = Role.REQ_PARTICIPANT 75 | } 76 | else /* Attendees.TYPE_REQUIRED, Attendees.TYPE_NONE */ -> 77 | role = Role.REQ_PARTICIPANT 78 | } 79 | 80 | } 81 | 82 | if (cuType != null && cuType != CuType.INDIVIDUAL) 83 | attendee.parameters.add(cuType) 84 | if (role != null && role != Role.REQ_PARTICIPANT) 85 | attendee.parameters.add(role) 86 | } 87 | 88 | 89 | /** 90 | * Maps iCalendar [CuType] and [Role] to Android [Attendees.ATTENDEE_TYPE] and 91 | * [Attendees.ATTENDEE_RELATIONSHIP] according to this matrix: 92 | * 93 | * CuType ↓ / Role → CHAIR REQ-PARTICIPANT¹ᴰ OPT-PARTICIPANT NON-PARTICIPANT 94 | * INDIVIDUALᴰ req,spk req,att opt,att non,att 95 | * UNKNOWN² req,spk req,non opt,non non,non 96 | * GROUP req,spk req,per opt,per non,per 97 | * RESOURCE res,spk res,non res,non res,non 98 | * ROOM ::: res,per :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 99 | * 100 | * ᴰ default value 101 | * ¹ custom/unknown ROLE values must be treated as REQ-PARTICIPANT 102 | * ² custom/unknown CUTYPE values must be treated as UNKNOWN 103 | * 104 | * When [attendee] is the [organizer], [CalendarContract.Attendees.ATTENDEE_RELATIONSHIP] = RELATIONSHIP_ATTENDEE 105 | * is replaced by [CalendarContract.Attendees.RELATIONSHIP_ORGANIZER]. 106 | * 107 | * @param attendee iCalendar attendee to map 108 | * @param row builder for the Android attendee row 109 | * @param organizer email address of iCalendar ORGANIZER; used to determine whether [attendee] is the organizer 110 | */ 111 | fun iCalendarToAndroid(attendee: Attendee, row: BatchOperation.CpoBuilder, organizer: String) { 112 | val type: Int 113 | var relationship: Int 114 | 115 | val cuType = attendee.getParameter(Parameter.CUTYPE) ?: CuType.INDIVIDUAL 116 | val role = attendee.getParameter(Parameter.ROLE) ?: Role.REQ_PARTICIPANT 117 | 118 | when (cuType) { 119 | CuType.RESOURCE -> { 120 | type = Attendees.TYPE_RESOURCE 121 | relationship = 122 | if (role == Role.CHAIR) 123 | Attendees.RELATIONSHIP_SPEAKER 124 | else 125 | Attendees.RELATIONSHIP_NONE 126 | } 127 | CuType.ROOM -> { 128 | type = Attendees.TYPE_RESOURCE 129 | relationship = Attendees.RELATIONSHIP_PERFORMER 130 | } 131 | 132 | else -> { 133 | // not a room and not a resource -> individual (default), group or unknown (includes x-custom) 134 | relationship = when (cuType) { 135 | CuType.GROUP -> 136 | Attendees.RELATIONSHIP_PERFORMER 137 | CuType.UNKNOWN -> 138 | Attendees.RELATIONSHIP_NONE 139 | else -> /* CuType.INDIVIDUAL and custom/unknown values */ 140 | Attendees.RELATIONSHIP_ATTENDEE 141 | } 142 | 143 | when (role) { 144 | Role.CHAIR -> { 145 | type = Attendees.TYPE_REQUIRED 146 | relationship = Attendees.RELATIONSHIP_SPEAKER 147 | } 148 | Role.OPT_PARTICIPANT -> 149 | type = Attendees.TYPE_OPTIONAL 150 | Role.NON_PARTICIPANT -> 151 | type = Attendees.TYPE_NONE 152 | else -> /* Role.REQ_PARTICIPANT and custom/unknown values */ 153 | type = Attendees.TYPE_REQUIRED 154 | } 155 | } 156 | } 157 | 158 | if (relationship == Attendees.RELATIONSHIP_ATTENDEE) { 159 | val uri = attendee.calAddress 160 | val email = if (uri.scheme.equals("mailto", true)) 161 | uri.schemeSpecificPart 162 | else 163 | attendee.getParameter(Parameter.EMAIL)?.value 164 | 165 | if (email == organizer) 166 | relationship = Attendees.RELATIONSHIP_ORGANIZER 167 | } 168 | 169 | row .withValue(Attendees.ATTENDEE_TYPE, type) 170 | .withValue(Attendees.ATTENDEE_RELATIONSHIP, relationship) 171 | } 172 | 173 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/CalendarStorageException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | /** 10 | * Indicates a problem with a calendar storage operation, like when a row can't be inserted or updated. 11 | * 12 | * Should not be used to wrap [android.os.RemoteException]. 13 | */ 14 | class CalendarStorageException: Exception { 15 | 16 | constructor(message: String): super(message) 17 | constructor(message: String, ex: Throwable): super(message, ex) 18 | 19 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/Css3Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.graphics.Color 10 | import java.util.logging.Logger 11 | import kotlin.math.sqrt 12 | 13 | /** 14 | * Represents an RGBA COLOR value, as specified in https://tools.ietf.org/html/rfc7986#section-5.9 15 | * 16 | * @property argb ARGB color value (0xAARRGGBB), alpha is 0xFF for all values 17 | */ 18 | @Suppress("EnumEntryName", "SpellCheckingInspection") 19 | enum class Css3Color(val argb: Int) { 20 | // values taken from https://www.w3.org/TR/2011/REC-css3-color-20110607/#svg-color 21 | aliceblue(0xfff0f8ff.toInt()), 22 | antiquewhite(0xfffaebd7.toInt()), 23 | aqua(0xff00ffff.toInt()), 24 | aquamarine(0xff7fffd4.toInt()), 25 | azure(0xfff0ffff.toInt()), 26 | beige(0xfff5f5dc.toInt()), 27 | bisque(0xffffe4c4.toInt()), 28 | black(0xff000000.toInt()), 29 | blanchedalmond(0xffffebcd.toInt()), 30 | blue(0xff0000ff.toInt()), 31 | blueviolet(0xff8a2be2.toInt()), 32 | brown(0xffa52a2a.toInt()), 33 | burlywood(0xffdeb887.toInt()), 34 | cadetblue(0xff5f9ea0.toInt()), 35 | chartreuse(0xff7fff00.toInt()), 36 | chocolate(0xffd2691e.toInt()), 37 | coral(0xffff7f50.toInt()), 38 | cornflowerblue(0xff6495ed.toInt()), 39 | cornsilk(0xfffff8dc.toInt()), 40 | crimson(0xffdc143c.toInt()), 41 | cyan(0xff00ffff.toInt()), 42 | darkblue(0xff00008b.toInt()), 43 | darkcyan(0xff008b8b.toInt()), 44 | darkgoldenrod(0xffb8860b.toInt()), 45 | darkgray(0xffa9a9a9.toInt()), 46 | darkgreen(0xff006400.toInt()), 47 | darkgrey(0xffa9a9a9.toInt()), 48 | darkkhaki(0xffbdb76b.toInt()), 49 | darkmagenta(0xff8b008b.toInt()), 50 | darkolivegreen(0xff556b2f.toInt()), 51 | darkorange(0xffff8c00.toInt()), 52 | darkorchid(0xff9932cc.toInt()), 53 | darkred(0xff8b0000.toInt()), 54 | darksalmon(0xffe9967a.toInt()), 55 | darkseagreen(0xff8fbc8f.toInt()), 56 | darkslateblue(0xff483d8b.toInt()), 57 | darkslategray(0xff2f4f4f.toInt()), 58 | darkslategrey(0xff2f4f4f.toInt()), 59 | darkturquoise(0xff00ced1.toInt()), 60 | darkviolet(0xff9400d3.toInt()), 61 | deeppink(0xffff1493.toInt()), 62 | deepskyblue(0xff00bfff.toInt()), 63 | dimgray(0xff696969.toInt()), 64 | dimgrey(0xff696969.toInt()), 65 | dodgerblue(0xff1e90ff.toInt()), 66 | firebrick(0xffb22222.toInt()), 67 | floralwhite(0xfffffaf0.toInt()), 68 | forestgreen(0xff228b22.toInt()), 69 | fuchsia(0xffff00ff.toInt()), 70 | gainsboro(0xffdcdcdc.toInt()), 71 | ghostwhite(0xfff8f8ff.toInt()), 72 | gold(0xffffd700.toInt()), 73 | goldenrod(0xffdaa520.toInt()), 74 | gray(0xff808080.toInt()), 75 | green(0xff008000.toInt()), 76 | greenyellow(0xffadff2f.toInt()), 77 | grey(0xff808080.toInt()), 78 | honeydew(0xfff0fff0.toInt()), 79 | hotpink(0xffff69b4.toInt()), 80 | indianred(0xffcd5c5c.toInt()), 81 | indigo(0xff4b0082.toInt()), 82 | ivory(0xfffffff0.toInt()), 83 | khaki(0xfff0e68c.toInt()), 84 | lavender(0xffe6e6fa.toInt()), 85 | lavenderblush(0xfffff0f5.toInt()), 86 | lawngreen(0xff7cfc00.toInt()), 87 | lemonchiffon(0xfffffacd.toInt()), 88 | lightblue(0xffadd8e6.toInt()), 89 | lightcoral(0xfff08080.toInt()), 90 | lightcyan(0xffe0ffff.toInt()), 91 | lightgoldenrodyellow(0xfffafad2.toInt()), 92 | lightgray(0xffd3d3d3.toInt()), 93 | lightgreen(0xff90ee90.toInt()), 94 | lightgrey(0xffd3d3d3.toInt()), 95 | lightpink(0xffffb6c1.toInt()), 96 | lightsalmon(0xffffa07a.toInt()), 97 | lightseagreen(0xff20b2aa.toInt()), 98 | lightskyblue(0xff87cefa.toInt()), 99 | lightslategray(0xff778899.toInt()), 100 | lightslategrey(0xff778899.toInt()), 101 | lightsteelblue(0xffb0c4de.toInt()), 102 | lightyellow(0xffffffe0.toInt()), 103 | lime(0xff00ff00.toInt()), 104 | limegreen(0xff32cd32.toInt()), 105 | linen(0xfffaf0e6.toInt()), 106 | magenta(0xffff00ff.toInt()), 107 | maroon(0xff800000.toInt()), 108 | mediumaquamarine(0xff66cdaa.toInt()), 109 | mediumblue(0xff0000cd.toInt()), 110 | mediumorchid(0xffba55d3.toInt()), 111 | mediumpurple(0xff9370db.toInt()), 112 | mediumseagreen(0xff3cb371.toInt()), 113 | mediumslateblue(0xff7b68ee.toInt()), 114 | mediumspringgreen(0xff00fa9a.toInt()), 115 | mediumturquoise(0xff48d1cc.toInt()), 116 | mediumvioletred(0xffc71585.toInt()), 117 | midnightblue(0xff191970.toInt()), 118 | mintcream(0xfff5fffa.toInt()), 119 | mistyrose(0xffffe4e1.toInt()), 120 | moccasin(0xffffe4b5.toInt()), 121 | navajowhite(0xffffdead.toInt()), 122 | navy(0xff000080.toInt()), 123 | oldlace(0xfffdf5e6.toInt()), 124 | olive(0xff808000.toInt()), 125 | olivedrab(0xff6b8e23.toInt()), 126 | orange(0xffffa500.toInt()), 127 | orangered(0xffff4500.toInt()), 128 | orchid(0xffda70d6.toInt()), 129 | palegoldenrod(0xffeee8aa.toInt()), 130 | palegreen(0xff98fb98.toInt()), 131 | paleturquoise(0xffafeeee.toInt()), 132 | palevioletred(0xffdb7093.toInt()), 133 | papayawhip(0xffffefd5.toInt()), 134 | peachpuff(0xffffdab9.toInt()), 135 | peru(0xffcd853f.toInt()), 136 | pink(0xffffc0cb.toInt()), 137 | plum(0xffdda0dd.toInt()), 138 | powderblue(0xffb0e0e6.toInt()), 139 | purple(0xff800080.toInt()), 140 | red(0xffff0000.toInt()), 141 | rosybrown(0xffbc8f8f.toInt()), 142 | royalblue(0xff4169e1.toInt()), 143 | saddlebrown(0xff8b4513.toInt()), 144 | salmon(0xfffa8072.toInt()), 145 | sandybrown(0xfff4a460.toInt()), 146 | seagreen(0xff2e8b57.toInt()), 147 | seashell(0xfffff5ee.toInt()), 148 | sienna(0xffa0522d.toInt()), 149 | silver(0xffc0c0c0.toInt()), 150 | skyblue(0xff87ceeb.toInt()), 151 | slateblue(0xff6a5acd.toInt()), 152 | slategray(0xff708090.toInt()), 153 | slategrey(0xff708090.toInt()), 154 | snow(0xfffffafa.toInt()), 155 | springgreen(0xff00ff7f.toInt()), 156 | steelblue(0xff4682b4.toInt()), 157 | tan(0xffd2b48c.toInt()), 158 | teal(0xff008080.toInt()), 159 | thistle(0xffd8bfd8.toInt()), 160 | tomato(0xffff6347.toInt()), 161 | turquoise(0xff40e0d0.toInt()), 162 | violet(0xffee82ee.toInt()), 163 | wheat(0xfff5deb3.toInt()), 164 | white(0xffffffff.toInt()), 165 | whitesmoke(0xfff5f5f5.toInt()), 166 | yellow(0xffffff00.toInt()), 167 | yellowgreen(0xff9acd32.toInt()); 168 | 169 | 170 | companion object { 171 | 172 | private val logger 173 | get() = Logger.getLogger(Css3Color::javaClass.name) 174 | 175 | /** 176 | * Parses the given color either as CSS3 color name or as (A)RGB hex value. 177 | * 178 | * @param color CSS3 color name like "blue" or (A)RGB hex value like #0000FF 179 | * @return ARGB color value or *null* if the color couldn't be parsed 180 | */ 181 | fun colorFromString(color: String): Int? = 182 | fromString(color)?.argb ?: 183 | try { 184 | Color.parseColor(color) 185 | } catch(e: Exception) { 186 | logger.warning("Invalid color value: $color") 187 | null 188 | } 189 | 190 | /** 191 | * Returns the Css3Color object of the given CSS3 color name. 192 | * 193 | * @param name CSS3 color name like "blue" 194 | * @return [Css3Color] object or *null* if no match was found 195 | */ 196 | fun fromString(name: String) = 197 | try { 198 | valueOf(name.lowercase()) 199 | } catch (e: IllegalArgumentException) { 200 | logger.warning("Invalid color name: $name") 201 | null 202 | } 203 | 204 | /** 205 | * Finds the best matching [Css3Color] for a given RGBA value using a weighted Euclidian 206 | * distance formula for RGB. 207 | * 208 | * @param argb (A)RGB color (A will be ignored) 209 | */ 210 | fun nearestMatch(argb: Int): Css3Color { 211 | val rgb = argb and 0xFFFFFF 212 | val distance = values().map { 213 | val cssColor = it.argb and 0xFFFFFF 214 | val r1 = rgb shr 16 215 | val r2 = cssColor shr 16 216 | val r = (r1 + r2)/2.0 217 | val deltaR = r1 - r2 218 | val deltaG = ((rgb shr 8) and 0xFF) - ((cssColor shr 8) and 0xFF) 219 | val deltaB = (rgb and 0xFF) - (cssColor and 0xFF) 220 | val deltaR2 = deltaR*deltaR 221 | val deltaG2 = deltaG*deltaG 222 | sqrt(2.0*deltaR2 + 4.0*deltaG2 + 3.0*deltaB*deltaB + (r*(deltaR2 - deltaG2))/256.0) 223 | } 224 | val idx = distance.withIndex().minByOrNull { it.value }!!.index 225 | return values()[idx] 226 | } 227 | 228 | } 229 | 230 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.content.ContentValues 10 | 11 | interface DmfsTaskFactory { 12 | 13 | fun fromProvider(taskList: DmfsTaskList, values: ContentValues): T 14 | 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/DmfsTaskListFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.accounts.Account 10 | import android.content.ContentProviderClient 11 | 12 | interface DmfsTaskListFactory> { 13 | 14 | fun newInstance(account: Account, provider: ContentProviderClient, providerName: TaskProvider.ProviderName, id: Long): T 15 | 16 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/Ical4jVersion.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | /** 10 | * The used version of ical4j. 11 | */ 12 | @Suppress("unused") 13 | const val ical4jVersion = BuildConfig.version_ical4j 14 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/InvalidCalendarException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | class InvalidCalendarException: Exception { 10 | 11 | constructor(message: String): super(message) 12 | constructor(message: String, ex: Throwable): super(message, ex) 13 | 14 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/JtxCollectionFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.accounts.Account 10 | import android.content.ContentProviderClient 11 | 12 | interface JtxCollectionFactory> { 13 | 14 | fun newInstance(account: Account, client: ContentProviderClient, id: Long): T 15 | 16 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/JtxICalObjectFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.content.ContentValues 10 | 11 | interface JtxICalObjectFactory { 12 | 13 | fun fromProvider(collection: JtxCollection, values: ContentValues): T 14 | 15 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/TaskProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.annotation.SuppressLint 10 | import android.content.ContentProviderClient 11 | import android.content.Context 12 | import android.content.pm.PackageManager 13 | import androidx.core.content.pm.PackageInfoCompat 14 | import at.bitfire.ical4android.util.MiscUtils.closeCompat 15 | import java.io.Closeable 16 | import java.util.logging.Level 17 | import java.util.logging.Logger 18 | import org.dmfs.tasks.contract.TaskContract 19 | 20 | 21 | class TaskProvider private constructor( 22 | val name: ProviderName, 23 | val client: ContentProviderClient 24 | ): Closeable { 25 | 26 | enum class ProviderName( 27 | val authority: String, 28 | val packageName: String, 29 | val minVersionCode: Long, 30 | val minVersionName: String, 31 | private val readPermission: String, 32 | private val writePermission: String 33 | ) { 34 | JtxBoard("at.techbee.jtx.provider", "at.techbee.jtx", 210000000, "2.10.00", PERMISSION_JTX_READ, PERMISSION_JTX_WRITE), 35 | TasksOrg("org.tasks.opentasks", "org.tasks", 100000, "10.0", PERMISSION_TASKS_ORG_READ, PERMISSION_TASKS_ORG_WRITE), 36 | OpenTasks("org.dmfs.tasks", "org.dmfs.tasks", 103, "1.1.8.2", PERMISSION_OPENTASKS_READ, PERMISSION_OPENTASKS_WRITE); 37 | 38 | companion object { 39 | fun fromAuthority(authority: String): ProviderName { 40 | for (provider in values()) 41 | if (provider.authority == authority) 42 | return provider 43 | throw IllegalArgumentException("Unknown tasks authority $authority") 44 | } 45 | } 46 | 47 | val permissions: Array 48 | get() = arrayOf(readPermission, writePermission) 49 | } 50 | 51 | 52 | companion object { 53 | 54 | private val logger 55 | get() = Logger.getLogger(TaskProvider::javaClass.name) 56 | 57 | val TASK_PROVIDERS = listOf( 58 | ProviderName.OpenTasks, 59 | ProviderName.TasksOrg, 60 | ProviderName.JtxBoard 61 | ) 62 | 63 | const val PERMISSION_OPENTASKS_READ = "org.dmfs.permission.READ_TASKS" 64 | const val PERMISSION_OPENTASKS_WRITE = "org.dmfs.permission.WRITE_TASKS" 65 | val PERMISSIONS_OPENTASKS = arrayOf(PERMISSION_OPENTASKS_READ, PERMISSION_OPENTASKS_WRITE) 66 | 67 | const val PERMISSION_TASKS_ORG_READ = "org.tasks.permission.READ_TASKS" 68 | const val PERMISSION_TASKS_ORG_WRITE = "org.tasks.permission.WRITE_TASKS" 69 | val PERMISSIONS_TASKS_ORG = arrayOf(PERMISSION_TASKS_ORG_READ, PERMISSION_TASKS_ORG_WRITE) 70 | 71 | const val PERMISSION_JTX_READ = "at.techbee.jtx.permission.READ" 72 | const val PERMISSION_JTX_WRITE = "at.techbee.jtx.permission.WRITE" 73 | val PERMISSIONS_JTX = arrayOf(PERMISSION_JTX_READ, PERMISSION_JTX_WRITE) 74 | 75 | /** 76 | * Acquires a content provider for a given task provider. The content provider will 77 | * be released when the TaskProvider is closed with [close]. 78 | * @param context will be used to acquire the content provider client 79 | * @param name task provider to acquire content provider for; *null* to try all supported providers 80 | * @return content provider for the given task provider (may be *null*) 81 | * @throws [ProviderTooOldException] if the tasks provider is installed, but doesn't meet the minimum version requirement 82 | */ 83 | @SuppressLint("Recycle") 84 | fun acquire(context: Context, name: ProviderName? = null): TaskProvider? { 85 | val providers = 86 | name?.let { arrayOf(it) } // provider name given? create array from it 87 | ?: ProviderName.values() // otherwise, try all providers 88 | for (provider in providers) 89 | try { 90 | checkVersion(context, provider) 91 | 92 | val client = context.contentResolver.acquireContentProviderClient(provider.authority) 93 | if (client != null) 94 | return TaskProvider(provider, client) 95 | } catch(e: SecurityException) { 96 | logger.log(Level.WARNING, "Not allowed to access task provider", e) 97 | } catch(e: PackageManager.NameNotFoundException) { 98 | logger.warning("Package ${provider.packageName} not installed") 99 | } 100 | return null 101 | } 102 | 103 | fun fromProviderClient( 104 | context: Context, 105 | provider: ProviderName, 106 | client: ContentProviderClient 107 | ): TaskProvider { 108 | checkVersion(context, provider) 109 | return TaskProvider(provider, client) 110 | } 111 | 112 | /** 113 | * Checks the version code of an installed tasks provider. 114 | * @throws PackageManager.NameNotFoundException if the tasks provider is not installed 115 | * @throws [ProviderTooOldException] if the tasks provider is installed, but doesn't meet the minimum version requirement 116 | * */ 117 | fun checkVersion(context: Context, name: ProviderName) { 118 | // check whether package is available with required minimum version 119 | val info = context.packageManager.getPackageInfo(name.packageName, 0) 120 | val installedVersionCode = PackageInfoCompat.getLongVersionCode(info) 121 | if (installedVersionCode < name.minVersionCode) { 122 | val exception = ProviderTooOldException(name, installedVersionCode, info.versionName) 123 | logger.log(Level.WARNING, "Task provider too old", exception) 124 | throw exception 125 | } 126 | } 127 | 128 | } 129 | 130 | 131 | fun taskListsUri() = TaskContract.TaskLists.getContentUri(name.authority)!! 132 | fun syncStateUri() = TaskContract.SyncState.getContentUri(name.authority)!! 133 | 134 | fun tasksUri() = TaskContract.Tasks.getContentUri(name.authority)!! 135 | fun propertiesUri() = TaskContract.Properties.getContentUri(name.authority)!! 136 | fun alarmsUri() = TaskContract.Alarms.getContentUri(name.authority)!! 137 | fun categoriesUri() = TaskContract.Categories.getContentUri(name.authority)!! 138 | 139 | 140 | override fun close() { 141 | client.closeCompat() 142 | } 143 | 144 | 145 | class ProviderTooOldException( 146 | val provider: ProviderName, 147 | installedVersionCode: Long, 148 | val installedVersionName: String? 149 | ): Exception("Package ${provider.packageName} has version $installedVersionName ($installedVersionCode), " + 150 | "required: ${provider.minVersionName} (${provider.minVersionCode})") 151 | 152 | } 153 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/UnknownProperty.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import android.content.ContentResolver 10 | import net.fortuna.ical4j.data.DefaultParameterFactorySupplier 11 | import net.fortuna.ical4j.data.DefaultPropertyFactorySupplier 12 | import net.fortuna.ical4j.model.Parameter 13 | import net.fortuna.ical4j.model.ParameterBuilder 14 | import net.fortuna.ical4j.model.ParameterFactory 15 | import net.fortuna.ical4j.model.Property 16 | import net.fortuna.ical4j.model.PropertyBuilder 17 | import net.fortuna.ical4j.model.PropertyFactory 18 | import org.json.JSONArray 19 | import org.json.JSONObject 20 | 21 | /** 22 | * Helpers to (de)serialize unknown properties as JSON to store it in an Android ExtendedProperty row. 23 | * 24 | * Format: `{ propertyName, propertyValue, { param1Name: param1Value, ... } }`, with the third 25 | * array (parameters) being optional. 26 | */ 27 | object UnknownProperty { 28 | 29 | /** 30 | * Use this value for [android.provider.CalendarContract.ExtendedProperties.NAME] and 31 | * [org.dmfs.tasks.contract.TaskContract.Properties.MIMETYPE]. 32 | */ 33 | const val CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/vnd.ical4android.unknown-property" 34 | 35 | /** 36 | * Recommended maximum size of properties for serialization. Won't be enforced by this 37 | * class (should be checked by caller). 38 | */ 39 | const val MAX_UNKNOWN_PROPERTY_SIZE = 25000 40 | 41 | val propertyFactorySupplier: List> = DefaultPropertyFactorySupplier().get() 42 | val parameterFactorySupplier: List> = DefaultParameterFactorySupplier().get() 43 | 44 | 45 | /** 46 | * Deserializes a JSON string from an ExtendedProperty value to an ical4j property. 47 | * 48 | * @param jsonString JSON representation of an ical4j property 49 | * @return ical4j property, generated from [jsonString] 50 | * @throws org.json.JSONException when the input value can't be parsed 51 | */ 52 | fun fromJsonString(jsonString: String): Property { 53 | val json = JSONArray(jsonString) 54 | val name = json.getString(0) 55 | val value = json.getString(1) 56 | 57 | val builder = PropertyBuilder(propertyFactorySupplier) 58 | .name(name) 59 | .value(value) 60 | 61 | json.optJSONObject(2)?.let { jsonParams -> 62 | for (paramName in jsonParams.keys()) 63 | builder.parameter( 64 | ParameterBuilder(parameterFactorySupplier) 65 | .name(paramName) 66 | .value(jsonParams.getString(paramName)) 67 | .build() 68 | ) 69 | } 70 | 71 | return builder.build() 72 | } 73 | 74 | /** 75 | * Serializes an ical4j property to a JSON string that can be stored in an ExtendedProperty. 76 | * 77 | * @param prop property to serialize as JSON 78 | * @return JSON representation of [prop] 79 | */ 80 | fun toJsonString(prop: Property): String { 81 | val json = JSONArray() 82 | json.put(prop.name) 83 | json.put(prop.value) 84 | 85 | if (!prop.parameters.isEmpty) { 86 | val jsonParams = JSONObject() 87 | for (param in prop.parameters) 88 | jsonParams.put(param.name, param.value) 89 | json.put(jsonParams) 90 | } 91 | 92 | return json.toString() 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/util/DateUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.util 8 | 9 | import net.fortuna.ical4j.data.CalendarBuilder 10 | import net.fortuna.ical4j.model.Date 11 | import net.fortuna.ical4j.model.DateTime 12 | import net.fortuna.ical4j.model.TimeZone 13 | import net.fortuna.ical4j.model.TimeZoneRegistryFactory 14 | import net.fortuna.ical4j.model.component.VTimeZone 15 | import net.fortuna.ical4j.model.property.DateProperty 16 | import java.io.StringReader 17 | import java.time.ZoneId 18 | import java.util.logging.Logger 19 | 20 | /** 21 | * Date/time utilities 22 | * 23 | * Before this object is accessed the first time, the accessing thread's contextClassLoader 24 | * must be set to an Android Context.classLoader! 25 | */ 26 | object DateUtils { 27 | 28 | private val logger 29 | get() = Logger.getLogger(javaClass.name) 30 | 31 | /** 32 | * Global ical4j time zone registry used for event/task processing. Do not 33 | * modify this registry or its entries! 34 | */ 35 | private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry() 36 | 37 | 38 | // time zones 39 | 40 | /** 41 | * Find the best matching Android (= available in system and Java timezone registry) 42 | * time zone ID for a given arbitrary time zone ID: 43 | * 44 | * 1. Use a case-insensitive match ("EUROPE/VIENNA" will return "Europe/Vienna", 45 | * assuming "Europe/Vienna") is available in Android. 46 | * 2. Find partial matches (case-sensitive) in both directions, so both "Vienna" 47 | * and "MyClient: Europe/Vienna" will return "Europe/Vienna". This shouln't be 48 | * case-insensitive, because that would for instance return "EST" for "Westeuropäische Sommerzeit". 49 | * 3. If nothing can be found or [tzId] is `null`, return the system default time zone. 50 | * 51 | * @param tzID time zone ID to be converted into Android time zone ID 52 | * 53 | * @return best matching Android time zone ID 54 | */ 55 | fun findAndroidTimezoneID(tzID: String?): String { 56 | val availableTZs = ZoneId.getAvailableZoneIds() 57 | var result: String? = null 58 | 59 | if (tzID != null) { 60 | // first, try to find an exact match (case insensitive) 61 | result = availableTZs.firstOrNull { it.equals(tzID, true) } 62 | 63 | // if that doesn't work, try to find something else that matches 64 | if (result == null) 65 | for (availableTZ in availableTZs) 66 | if (availableTZ.contains(tzID) || tzID.contains(availableTZ)) { 67 | result = availableTZ 68 | logger.warning("Couldn't find system time zone \"$tzID\", assuming $result") 69 | break 70 | } 71 | } 72 | 73 | // if that doesn't work, use device default as fallback 74 | return result ?: TimeZone.getDefault().id 75 | } 76 | 77 | /** 78 | * Gets a [ZoneId] from a given ID string. In opposite to [ZoneId.of], 79 | * this methods returns null when the zone is not available. 80 | * 81 | * @param id zone ID, like "Europe/Berlin" (may be null) 82 | * 83 | * @return ZoneId or null if the argument was null or no zone with this ID could be found 84 | */ 85 | fun getZoneId(id: String?): ZoneId? = 86 | id?.let { 87 | try { 88 | val zone = ZoneId.of(id) 89 | zone 90 | } catch (e: Exception) { 91 | null 92 | } 93 | } 94 | 95 | /** 96 | * Loads a time zone from the ical4j time zone registry (which contains the 97 | * VTIMEZONE definitions). 98 | * 99 | * All Android time zone IDs plus some other time zones should be available. 100 | * However, the possibility that the time zone is not available in ical4j should 101 | * be handled. 102 | * 103 | * @param id time zone ID (like `Europe/Vienna`) 104 | * @return the ical4j time zone (VTIMEZONE), or `null` if no VTIMEZONE is available 105 | */ 106 | fun ical4jTimeZone(id: String): TimeZone? = tzRegistry.getTimeZone(id) 107 | 108 | /** 109 | * Determines whether a given date represents a DATE value. 110 | * @param date date property to check 111 | * @return *true* if the date is a DATE value; *false* otherwise (for instance, when the argument is a DATE-TIME value or null) 112 | */ 113 | fun isDate(date: DateProperty?) = date != null && date.date is Date && date.date !is DateTime 114 | 115 | /** 116 | * Determines whether a given date represents a DATE-TIME value. 117 | * @param date date property to check 118 | * @return *true* if the date is a DATE-TIME value; *false* otherwise (for instance, when the argument is a DATE value or null) 119 | */ 120 | fun isDateTime(date: DateProperty?) = date != null && date.date is DateTime 121 | 122 | /** 123 | * Parses an iCalendar that only contains a `VTIMEZONE` definition to a VTimeZone object. 124 | * 125 | * @param timezoneDef iCalendar with only a `VTIMEZONE` definition 126 | * 127 | * @return parsed [VTimeZone], or `null` when the timezone definition can't be parsed 128 | */ 129 | fun parseVTimeZone(timezoneDef: String): VTimeZone? { 130 | val builder = CalendarBuilder(tzRegistry) 131 | try { 132 | val cal = builder.build(StringReader(timezoneDef)) 133 | return cal.getComponent(VTimeZone.VTIMEZONE) as VTimeZone 134 | } catch (_: Exception) { 135 | // Couldn't parse timezone definition 136 | return null 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/util/MiscUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.util 8 | 9 | import android.accounts.Account 10 | import android.content.ContentProviderClient 11 | import android.content.ContentValues 12 | import android.database.Cursor 13 | import android.database.DatabaseUtils 14 | import android.net.Uri 15 | import android.os.Build 16 | import android.provider.CalendarContract 17 | 18 | object MiscUtils { 19 | 20 | // various extension methods 21 | 22 | fun ContentProviderClient.closeCompat() { 23 | @Suppress("DEPRECATION") 24 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) 25 | close() 26 | else 27 | release() 28 | } 29 | 30 | /** 31 | * Removes blank (empty or only white-space) [String] values from [ContentValues]. 32 | * 33 | * @return the modified object (which is the same object as passed in; for chaining) 34 | */ 35 | fun ContentValues.removeBlankStrings(): ContentValues { 36 | val iter = keySet().iterator() 37 | while (iter.hasNext()) { 38 | val obj = this[iter.next()] 39 | if (obj is CharSequence && obj.isBlank()) 40 | iter.remove() 41 | } 42 | return this 43 | } 44 | 45 | /** 46 | * Returns the entire contents of the current row as a [ContentValues] object. 47 | * 48 | * @param removeBlankRows whether rows with blank values should be removed 49 | * @return entire contents of the current row 50 | */ 51 | fun Cursor.toValues(removeBlankRows: Boolean = false): ContentValues { 52 | val values = ContentValues(columnCount) 53 | DatabaseUtils.cursorRowToContentValues(this, values) 54 | 55 | if (removeBlankRows) 56 | values.removeBlankStrings() 57 | 58 | return values 59 | } 60 | 61 | fun Uri.asSyncAdapter(account: Account): Uri = buildUpon() 62 | .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_NAME, account.name) 63 | .appendQueryParameter(CalendarContract.Calendars.ACCOUNT_TYPE, account.type) 64 | .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") 65 | .build() 66 | 67 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/util/TimeApiExtensions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.util 8 | 9 | import net.fortuna.ical4j.model.Date 10 | import net.fortuna.ical4j.model.DateTime 11 | import net.fortuna.ical4j.util.TimeZones 12 | import java.time.Duration 13 | import java.time.Instant 14 | import java.time.LocalDate 15 | import java.time.LocalTime 16 | import java.time.Period 17 | import java.time.ZoneId 18 | import java.time.ZoneOffset 19 | import java.time.ZonedDateTime 20 | import java.time.temporal.TemporalAmount 21 | import java.util.Calendar 22 | import java.util.TimeZone 23 | 24 | object TimeApiExtensions { 25 | 26 | const val DAYS_PER_WEEK = 7 27 | 28 | const val SECONDS_PER_MINUTE = 60 29 | const val SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60 30 | const val SECONDS_PER_DAY = SECONDS_PER_HOUR * 24 31 | private const val SECONDS_PER_WEEK = SECONDS_PER_DAY * DAYS_PER_WEEK 32 | 33 | private const val MILLIS_PER_SECOND = 1000 34 | const val MILLIS_PER_DAY = SECONDS_PER_DAY * MILLIS_PER_SECOND 35 | 36 | val tzUTC: TimeZone by lazy { TimeZones.getUtcTimeZone() } 37 | 38 | 39 | /***** Desugaring compat *****/ 40 | 41 | /** 42 | * [TimeZone.toZoneId] can't be used with the current desugaring library yet! 43 | * 44 | * @return [ZoneId] of the time zone; [ZoneOffset.UTC] if the time zone equals to [TimeZones.getUtcTimeZone] 45 | */ 46 | fun TimeZone.toZoneIdCompat(): ZoneId { 47 | return if (this == TimeZones.getUtcTimeZone()) 48 | ZoneOffset.UTC 49 | else 50 | ZoneId.of(id) 51 | } 52 | 53 | 54 | /***** Dates *****/ 55 | 56 | fun Date.toLocalDate(): LocalDate { 57 | val utcDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneOffset.UTC) 58 | return utcDateTime.toLocalDate() 59 | } 60 | 61 | fun DateTime.requireTimeZone(): TimeZone = 62 | if (isUtc) 63 | TimeZones.getUtcTimeZone() 64 | else 65 | timeZone ?: TimeZone.getDefault() 66 | 67 | fun DateTime.requireZoneId(): ZoneId = 68 | if (isUtc) 69 | ZoneOffset.UTC 70 | else 71 | timeZone?.toZoneIdCompat() ?: ZoneId.systemDefault() 72 | 73 | fun DateTime.toLocalDate(): LocalDate = 74 | toZonedDateTime().toLocalDate() 75 | 76 | fun DateTime.toLocalTime(): LocalTime = 77 | toZonedDateTime().toLocalTime() 78 | 79 | fun DateTime.toZonedDateTime(): ZonedDateTime = 80 | ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), requireZoneId()) 81 | 82 | fun LocalDate.toIcal4jDate(): Date { 83 | val cal = Calendar.getInstance(TimeZones.getDateTimeZone()) 84 | cal.set(year, monthValue - 1, dayOfMonth, 0, 0, 0) 85 | return Date(cal) 86 | } 87 | 88 | /** 89 | * Converts this zoned date-time (date/time with specific time zone) to an 90 | * ical4j [DateTime] object. 91 | * 92 | * Sets UTC flag ([DateTime.isUtc], means `...ThhmmddZ` format) when this zone-date time object has a 93 | * time zone of [ZoneOffset.UTC]. 94 | * 95 | * @return ical4j [DateTime] of the given zoned date-time 96 | */ 97 | fun ZonedDateTime.toIcal4jDateTime(): DateTime { 98 | val date = DateTime(toEpochSecond() * MILLIS_PER_SECOND) 99 | if (zone == ZoneOffset.UTC) 100 | date.isUtc = true 101 | else 102 | date.timeZone = DateUtils.ical4jTimeZone(zone.id) 103 | return date 104 | } 105 | 106 | 107 | /***** Durations *****/ 108 | 109 | fun TemporalAmount.toDuration(position: Instant): Duration = 110 | when (this) { 111 | is Duration -> this 112 | is Period -> { 113 | val calEnd = Calendar.getInstance(tzUTC) 114 | calEnd.timeInMillis = position.toEpochMilli() 115 | calEnd.add(Calendar.DAY_OF_MONTH, days) 116 | calEnd.add(Calendar.MONTH, months) 117 | calEnd.add(Calendar.YEAR, years) 118 | Duration.ofMillis(calEnd.timeInMillis - position.toEpochMilli()) 119 | } 120 | else -> throw IllegalArgumentException("TemporalAmount must be Period or Duration") 121 | } 122 | 123 | /** 124 | * Converts a [TemporalAmount] to an RFC5545 duration value, which only uses 125 | * weeks, days, hours, minutes and seconds. Because years and months can't be used, 126 | * they're converted to weeks/days using the duration's position in the calendar. 127 | * 128 | * @param position the duration's position in the calendar 129 | * 130 | * @return RFC5545 duration value 131 | */ 132 | fun TemporalAmount.toRfc5545Duration(position: Instant): String { 133 | /* [RFC 5545 3.3.6 Duration] 134 | dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week) 135 | dur-date = dur-day [dur-time] 136 | dur-time = "T" (dur-hour / dur-minute / dur-second) 137 | dur-week = 1*DIGIT "W" 138 | dur-hour = 1*DIGIT "H" [dur-minute] 139 | dur-minute = 1*DIGIT "M" [dur-second] 140 | dur-second = 1*DIGIT "S" 141 | dur-day = 1*DIGIT "D" 142 | */ 143 | val builder = StringBuilder("P") 144 | if (this is Duration) { 145 | // TemporalAmountAdapter(Duration).toString() sometimes drops minutes: https://github.com/ical4j/ical4j/issues/420 146 | var secs = seconds 147 | 148 | if (secs == 0L) 149 | return "P0S" 150 | 151 | var weeks = secs / SECONDS_PER_WEEK 152 | secs -= weeks * SECONDS_PER_WEEK 153 | 154 | var days = secs / SECONDS_PER_DAY 155 | secs -= days * SECONDS_PER_DAY 156 | 157 | val hours = secs / SECONDS_PER_HOUR 158 | secs -= hours * SECONDS_PER_HOUR 159 | 160 | val minutes = secs / SECONDS_PER_MINUTE 161 | secs -= minutes * SECONDS_PER_MINUTE 162 | 163 | if (weeks != 0L && (days == 0L && hours == 0L && minutes == 0L && secs == 0L)) 164 | return "P${weeks}W" 165 | 166 | days += weeks * DAYS_PER_WEEK 167 | weeks = 0 168 | 169 | if (days != 0L) 170 | builder.append("${days}D") 171 | 172 | if (hours != 0L || minutes != 0L || secs != 0L) { 173 | builder.append("T") 174 | if (hours != 0L) 175 | builder.append("${hours}H") 176 | if (minutes != 0L) 177 | builder.append("${minutes}M") 178 | if (secs != 0L) 179 | builder.append("${secs}S") 180 | } 181 | 182 | } else if (this is Period) { 183 | // TemporalAmountAdapter(Period).toString() returns wrong values: https://github.com/ical4j/ical4j/issues/419 184 | var days = this.toDuration(position).toDays().toInt() 185 | 186 | if (days < 0) { 187 | builder.append("-") 188 | days = -days 189 | } 190 | 191 | if (days > 0L && days.rem(DAYS_PER_WEEK) == 0) { 192 | val weeks = days / DAYS_PER_WEEK 193 | builder.append("${weeks}W") 194 | } else 195 | builder.append("${days}D") 196 | } else 197 | throw NotImplementedError("Only Duration and Period is supported") 198 | return builder.toString() 199 | } 200 | 201 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/validation/EventValidator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.validation 8 | 9 | import androidx.annotation.VisibleForTesting 10 | import at.bitfire.ical4android.Event 11 | import at.bitfire.ical4android.util.DateUtils 12 | import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate 13 | import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate 14 | import at.bitfire.ical4android.util.TimeApiExtensions.toZoneIdCompat 15 | import net.fortuna.ical4j.model.DateTime 16 | import net.fortuna.ical4j.model.Recur 17 | import net.fortuna.ical4j.model.property.DtStart 18 | import net.fortuna.ical4j.model.property.RRule 19 | import net.fortuna.ical4j.util.TimeZones 20 | import java.time.LocalTime 21 | import java.time.ZonedDateTime 22 | import java.util.Calendar 23 | import java.util.TimeZone 24 | import java.util.logging.Logger 25 | 26 | /** 27 | * Validates events and tries to repair broken events, since sometimes CalendarStorage or servers 28 | * respond with invalid event definitions. 29 | * 30 | * This class should not throw exceptions, but try to repair as much as possible instead. 31 | * 32 | * This class is applied 33 | * 34 | * - once to every event after completely reading an iCalendar, and 35 | * - to every event when writing an iCalendar. 36 | */ 37 | object EventValidator { 38 | 39 | private val logger 40 | get() = Logger.getLogger(javaClass.name) 41 | 42 | /** 43 | * Searches for some invalid conditions and fixes them. 44 | * 45 | * @param event event to repair (including its exceptions) – may be modified! 46 | */ 47 | fun repair(event: Event) { 48 | val dtStart = correctStartAndEndTime(event) 49 | sameTypeForDtStartAndRruleUntil(dtStart, event.rRules) 50 | removeRRulesWithUntilBeforeDtStart(dtStart, event.rRules) 51 | removeRecurrenceOfExceptions(event.exceptions) 52 | } 53 | 54 | 55 | /** 56 | * Makes sure that event has a start time and that it's before the end time. 57 | * If the event doesn't have start time, 58 | * 59 | * 1. the end time is used as start time, if available, 60 | * 2. otherwise the current time is used as start time. 61 | * 62 | * If the event has an end time and it's before the start time, the end time is removed. 63 | * 64 | * @return the (potentially corrected) start time 65 | */ 66 | @VisibleForTesting 67 | internal fun correctStartAndEndTime(e: Event): DtStart { 68 | // make sure that event has a start time 69 | var dtStart: DtStart? = e.dtStart 70 | if (dtStart == null) { 71 | dtStart = 72 | e.dtEnd?.let { 73 | DtStart(it.date) 74 | } ?: DtStart(DateTime(/* current time */)) 75 | e.dtStart = dtStart 76 | } 77 | 78 | // validate end time 79 | e.dtEnd?.let { dtEnd -> 80 | if (dtStart.date > dtEnd.date) { 81 | logger.warning("DTSTART after DTEND; removing DTEND") 82 | e.dtEnd = null 83 | } 84 | } 85 | 86 | return dtStart 87 | } 88 | 89 | /** 90 | * Tries to make the value type of UNTIL and DTSTART the same (both DATE or DATETIME). 91 | */ 92 | @VisibleForTesting 93 | internal fun sameTypeForDtStartAndRruleUntil(dtStart: DtStart, rRules: MutableList) { 94 | if (DateUtils.isDate(dtStart)) { 95 | // DTSTART is a DATE 96 | val newRRules = mutableListOf() 97 | val rRuleIterator = rRules.iterator() 98 | while (rRuleIterator.hasNext()) { 99 | val rRule = rRuleIterator.next() 100 | rRule.recur.until?.let { until -> 101 | if (until is DateTime) { 102 | logger.warning("DTSTART has DATE, but UNTIL has DATETIME; making UNTIL have DATE only") 103 | 104 | val newUntil = until.toLocalDate().toIcal4jDate() 105 | 106 | // remove current RRULE and remember new one to be added 107 | val newRRule = RRule(Recur.Builder(rRule.recur) 108 | .until(newUntil) 109 | .build()) 110 | logger.info("New $newRRule (was ${rRule.toString().trim()})") 111 | newRRules += newRRule 112 | rRuleIterator.remove() 113 | } 114 | } 115 | } 116 | // add repaired RRULEs 117 | rRules += newRRules 118 | 119 | } else if (DateUtils.isDateTime(dtStart)) { 120 | // DTSTART is a DATE-TIME 121 | val newRRules = mutableListOf() 122 | val rRuleIterator = rRules.iterator() 123 | while (rRuleIterator.hasNext()) { 124 | val rRule = rRuleIterator.next() 125 | rRule.recur.until?.let { until -> 126 | if (until !is DateTime) { 127 | logger.warning("DTSTART has DATETIME, but UNTIL has DATE; copying time from DTSTART to UNTIL") 128 | val dtStartTimeZone = if (dtStart.timeZone != null) 129 | dtStart.timeZone 130 | else if (dtStart.isUtc) 131 | TimeZones.getUtcTimeZone() 132 | else /* floating time */ 133 | TimeZone.getDefault() 134 | 135 | val dtStartCal = Calendar.getInstance(dtStartTimeZone).apply { 136 | time = dtStart.date 137 | } 138 | val dtStartTime = LocalTime.of( 139 | dtStartCal.get(Calendar.HOUR_OF_DAY), 140 | dtStartCal.get(Calendar.MINUTE), 141 | dtStartCal.get(Calendar.SECOND) 142 | ) 143 | 144 | val newUntil = ZonedDateTime.of( 145 | until.toLocalDate(), // date from until 146 | dtStartTime, // time from dtStart 147 | dtStartTimeZone.toZoneIdCompat() 148 | ) 149 | 150 | // Android requires UNTIL in UTC as defined in RFC 2445. 151 | // https://android.googlesource.com/platform/frameworks/opt/calendar/+/refs/tags/android-12.1.0_r27/src/com/android/calendarcommon2/RecurrenceProcessor.java#93 152 | val newUntilUTC = DateTime(true).apply { 153 | time = newUntil.toInstant().toEpochMilli() 154 | } 155 | 156 | // remove current RRULE and remember new one to be added 157 | val newRRule = RRule(Recur.Builder(rRule.recur) 158 | .until(newUntilUTC) 159 | .build()) 160 | logger.info("New $newRRule (was ${rRule.toString().trim()})") 161 | newRRules += newRRule 162 | rRuleIterator.remove() 163 | } 164 | } 165 | } 166 | // add repaired RRULEs 167 | rRules += newRRules 168 | } 169 | } 170 | 171 | 172 | /** 173 | * Removes all recurrence information of exceptions of (potentially recurring) events. This is: 174 | * `RRULE`, `RDATE` and `EXDATE`. 175 | * Note: This repair step needs to be applied after all exceptions have been found. 176 | * 177 | * @param exceptions exceptions of an event 178 | */ 179 | @VisibleForTesting 180 | internal fun removeRecurrenceOfExceptions(exceptions: List) { 181 | for (exception in exceptions) { 182 | // Drop all RRULEs, RDATEs, EXDATEs for the exception 183 | exception.rRules.clear() 184 | exception.rDates.clear() 185 | exception.exDates.clear() 186 | } 187 | } 188 | 189 | 190 | /** 191 | * Will remove the RRULES of an event where UNTIL lies before DTSTART 192 | */ 193 | @VisibleForTesting 194 | internal fun removeRRulesWithUntilBeforeDtStart(dtStart: DtStart, rRules: MutableList) { 195 | val iter = rRules.iterator() 196 | while (iter.hasNext()) { 197 | val rRule = iter.next() 198 | 199 | // drop invalid RRULEs 200 | if (hasUntilBeforeDtStart(dtStart, rRule)) 201 | iter.remove() 202 | } 203 | } 204 | 205 | /** 206 | * Checks whether UNTIL of an RRULE lies before DTSTART 207 | */ 208 | @VisibleForTesting 209 | internal fun hasUntilBeforeDtStart(dtStart: DtStart, rRule: RRule): Boolean { 210 | val until = rRule.recur.until ?: return false 211 | return until < dtStart.date 212 | } 213 | 214 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.validation 8 | 9 | /** 10 | * Fixes durations with day offsets with the 'T' prefix. 11 | * See also https://github.com/bitfireAT/ical4android/issues/77 12 | */ 13 | object FixInvalidDayOffsetPreprocessor : StreamPreprocessor() { 14 | 15 | override fun regexpForProblem() = Regex( 16 | // Examples: 17 | // TRIGGER:-P2DT 18 | // TRIGGER:-PT2D 19 | // REFRESH-INTERVAL;VALUE=DURATION:-PT1D 20 | "(?:^|^(?:DURATION|REFRESH-INTERVAL|RELATED-TO|TRIGGER);VALUE=)(?:DURATION|TRIGGER):(-?P((T-?\\d+D)|(-?\\d+DT)))$", 21 | setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE) 22 | ) 23 | 24 | override fun fixString(original: String): String { 25 | var iCal: String = original 26 | 27 | // Find all instances matching the defined expression 28 | val found = regexpForProblem().findAll(iCal).toList() 29 | 30 | // ... and repair them. Use reversed order so that already replaced occurrences don't interfere with the following matches. 31 | for (match in found.reversed()) { 32 | match.groups[1]?.let { duration -> // first capturing group is the duration value, for instance: "-PT1D" 33 | val fixed = duration.value // fixed is then for instance: "-P1D" 34 | .replace("PT", "P") 35 | .replace("DT", "D") 36 | iCal = iCal.replaceRange(duration.range, fixed) 37 | } 38 | } 39 | return iCal 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.validation 8 | 9 | import at.bitfire.ical4android.validation.FixInvalidUtcOffsetPreprocessor.TZOFFSET_REGEXP 10 | import java.util.logging.Level 11 | import java.util.logging.Logger 12 | 13 | 14 | /** 15 | * Some servers modify UTC offsets in TZOFFSET(FROM,TO) like "+005730" to an invalid "+5730". 16 | * 17 | * Rewrites values of all TZOFFSETFROM and TZOFFSETTO properties which match [TZOFFSET_REGEXP] 18 | * so that an hour value of 00 is inserted. 19 | */ 20 | object FixInvalidUtcOffsetPreprocessor: StreamPreprocessor() { 21 | 22 | private val logger 23 | get() = Logger.getLogger(javaClass.name) 24 | 25 | private val TZOFFSET_REGEXP = Regex("^(TZOFFSET(FROM|TO):[+\\-]?)((18|19|[2-6]\\d)\\d\\d)$", 26 | setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE)) 27 | 28 | override fun regexpForProblem() = TZOFFSET_REGEXP 29 | 30 | override fun fixString(original: String) = 31 | original.replace(TZOFFSET_REGEXP) { 32 | logger.log(Level.FINE, "Applying Synology WebDAV fix to invalid utc-offset", it.value) 33 | "${it.groupValues[1]}00${it.groupValues[3]}" 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/validation/ICalPreprocessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.validation 8 | 9 | import net.fortuna.ical4j.model.Calendar 10 | import net.fortuna.ical4j.model.Property 11 | import net.fortuna.ical4j.transform.rfc5545.CreatedPropertyRule 12 | import net.fortuna.ical4j.transform.rfc5545.DateListPropertyRule 13 | import net.fortuna.ical4j.transform.rfc5545.DatePropertyRule 14 | import net.fortuna.ical4j.transform.rfc5545.Rfc5545PropertyRule 15 | import java.io.Reader 16 | import java.util.logging.Level 17 | import java.util.logging.Logger 18 | 19 | /** 20 | * Applies some rules to increase compatibility of parsed (incoming) iCalendars: 21 | * 22 | * - [CreatedPropertyRule] to make sure CREATED is UTC 23 | * - [DatePropertyRule], [DateListPropertyRule] to rename Outlook-specific TZID parameters 24 | * (like "W. Europe Standard Time" to an Android-friendly name like "Europe/Vienna") 25 | * 26 | */ 27 | object ICalPreprocessor { 28 | 29 | private val propertyRules = arrayOf( 30 | CreatedPropertyRule(), // make sure CREATED is UTC 31 | 32 | DatePropertyRule(), // These two rules also replace VTIMEZONEs of the iCalendar ... 33 | DateListPropertyRule() // ... by the ical4j VTIMEZONE with the same TZID! 34 | ) 35 | 36 | private val streamPreprocessors = arrayOf( 37 | FixInvalidUtcOffsetPreprocessor, // fix things like TZOFFSET(FROM,TO):+5730 38 | FixInvalidDayOffsetPreprocessor // fix things like DURATION:PT2D 39 | ) 40 | 41 | /** 42 | * Applies [streamPreprocessors] to a given [Reader] that reads an iCalendar object 43 | * in order to repair some things that must be fixed before parsing. 44 | * 45 | * @param original original iCalendar object 46 | * @return the potentially repaired iCalendar object 47 | */ 48 | fun preprocessStream(original: Reader): Reader { 49 | var reader = original 50 | for (preprocessor in streamPreprocessors) 51 | reader = preprocessor.preprocess(reader) 52 | return reader 53 | } 54 | 55 | 56 | /** 57 | * Applies the set of rules (see class definition) to a given calendar object. 58 | * 59 | * @param calendar the calendar object that is going to be modified 60 | */ 61 | fun preprocessCalendar(calendar: Calendar) { 62 | for (component in calendar.components) 63 | for (property in component.properties) 64 | applyRules(property) 65 | } 66 | 67 | @Suppress("UNCHECKED_CAST") 68 | private fun applyRules(property: Property) { 69 | propertyRules 70 | .filter { rule -> rule.supportedType.isAssignableFrom(property::class.java) } 71 | .forEach { 72 | val beforeStr = property.toString() 73 | (it as Rfc5545PropertyRule).applyTo(property) 74 | val afterStr = property.toString() 75 | if (beforeStr != afterStr) 76 | Logger.getLogger(javaClass.name).log(Level.FINER, "$beforeStr -> $afterStr") 77 | } 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /lib/src/main/kotlin/at/bitfire/ical4android/validation/StreamPreprocessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.validation 8 | 9 | import java.io.IOException 10 | import java.io.Reader 11 | import java.io.StringReader 12 | import java.util.Scanner 13 | 14 | abstract class StreamPreprocessor { 15 | 16 | abstract fun regexpForProblem(): Regex? 17 | 18 | /** 19 | * Fixes an iCalendar string. 20 | * 21 | * @param original The complete iCalendar string 22 | * @return The complete iCalendar string, but fixed 23 | */ 24 | abstract fun fixString(original: String): String 25 | 26 | fun preprocess(reader: Reader): Reader { 27 | var result: String? = null 28 | 29 | val resetSupported = try { 30 | reader.reset() 31 | true 32 | } catch(_: IOException) { 33 | false 34 | } 35 | 36 | if (resetSupported) { 37 | val regex = regexpForProblem() 38 | // reset is supported, no need to copy the whole stream to another String (unless we have to fix the TZOFFSET) 39 | if (regex == null || Scanner(reader).findWithinHorizon(regex.toPattern(), 0) != null) { 40 | reader.reset() 41 | result = fixString(reader.readText()) 42 | } 43 | } else 44 | // reset not supported, always generate a new String that will be returned 45 | result = fixString(reader.readText()) 46 | 47 | if (result != null) 48 | // modified or reset not supported, return new stream 49 | return StringReader(result) 50 | 51 | // not modified, return original iCalendar 52 | reader.reset() 53 | return reader 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /lib/src/main/resources/ical4j.properties: -------------------------------------------------------------------------------- 1 | net.fortuna.ical4j.timezone.cache.impl=net.fortuna.ical4j.util.MapTimeZoneCache 2 | net.fortuna.ical4j.timezone.offset.negative_dst_supported=true 3 | net.fortuna.ical4j.timezone.registry=at.bitfire.ical4android.AndroidCompatTimeZoneRegistry$Factory 4 | net.fortuna.ical4j.timezone.update.enabled=false 5 | ical4j.unfolding.relaxed=true 6 | ical4j.parsing.relaxed=true 7 | ical4j.validation.relaxed=true 8 | ical4j.compatibility.outlook=true 9 | -------------------------------------------------------------------------------- /lib/src/main/resources/tz.alias: -------------------------------------------------------------------------------- 1 | # Central Australia Time (ACT) 2 | ACT=Australia/Darwin 3 | # Eastern Australia Time (AET) 4 | AET=Australia/Hobart 5 | # Argentina Time (AGT) 6 | AGT=America/Argentina/Buenos_Aires 7 | # Eastern European Time (ART) 8 | ART=Europe/Athens 9 | # Alaska Time (AST) 10 | AST=America/Juneau 11 | # Brasilia Time (BET) 12 | BET=America/Sao_Paulo 13 | # Bangladesh Time (BST) 14 | BST=Asia/Dhaka 15 | # CAT (CAT) 16 | CAT=Africa/Maputo 17 | # CET (CET) 18 | CET=Europe/Brussels 19 | # China Time (CTT) 20 | CTT=Asia/Shanghai 21 | # Central Time (CST) 22 | CST=America/Winnipeg 23 | # Newfoundland Time (CNT) 24 | CNT=America/St_Johns 25 | # EAT (EAT) 26 | EAT=Africa/Nairobi 27 | # Central European Time (ECT) 28 | ECT=Europe/Paris 29 | # EET (EET) 30 | EET=Europe/Athens 31 | # Factory (Factory) 32 | Factory=Etc/GMT 33 | # Eastern Time (IET) 34 | IET=America/Indianapolis 35 | # IST (IST) 36 | IST=Asia/Calcutta 37 | # Japan Time (JST) 38 | JST=Asia/Tokyo 39 | # MET (MET) 40 | MET=Europe/Brussels 41 | # Apia Time (MIT) 42 | MIT=Pacific/Apia 43 | # Armenia Time (NET) 44 | NET=Asia/Yerevan 45 | # New Zealand Time (NST) 46 | NST=Pacific/Auckland 47 | # Pakistan Time (PLT) 48 | PLT=Asia/Karachi 49 | # Mountain Time (PNT) 50 | PNT=America/Phoenix 51 | # Atlantic Time (PRT) 52 | PRT=America/Puerto_Rico 53 | # Pacific Time (PST) 54 | PST=America/Los_Angeles 55 | # SST (SST) 56 | SST=Pacific/Guadalcanal 57 | # VST (VST) 58 | VST=Asia/Saigon 59 | # WET (WET) 60 | WET=Europe/London 61 | SystemV/AST4=Etc/GMT+4 62 | SystemV/AST4ADT=America/Glace_Bay 63 | SystemV/CST6=Etc/GMT+6 64 | SystemV/CST6CDT=Pacific/Easter 65 | SystemV/EST5=Etc/GMT+5 66 | SystemV/EST5EDT=America/Indianapolis 67 | SystemV/HST10=Pacific/Honolulu 68 | SystemV/MST7=Etc/GMT+7 69 | SystemV/MST7MDT=America/Chihuahua 70 | SystemV/PST8=Etc/GMT+8 71 | SystemV/PST8PDT=America/Los_Angeles 72 | SystemV/YST9=Etc/GMT+9 73 | SystemV/YST9YDT=America/Anchorage -------------------------------------------------------------------------------- /lib/src/test/README.txt: -------------------------------------------------------------------------------- 1 | 2 | ATTENTION! 3 | 4 | Do not use JVM unit tests for methods the use the Time API or other APIs that behave 5 | differently in normal JVMs and the Android JVM. Especially do not write JVM unit tests 6 | that use APIs that are desugared in Android. Use Android unit tests (androidTest) instead. 7 | -------------------------------------------------------------------------------- /lib/src/test/kotlin/at/bitfire/ical4android/Ical4jServiceLoaderTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android 8 | 9 | import net.fortuna.ical4j.data.CalendarBuilder 10 | import net.fortuna.ical4j.model.Component 11 | import net.fortuna.ical4j.model.component.VEvent 12 | import org.junit.Assert.assertEquals 13 | import org.junit.Test 14 | import java.io.StringReader 15 | 16 | class Ical4jServiceLoaderTest { 17 | 18 | @Test 19 | fun Ical4j_ServiceLoader_DoesntNeedContextClassLoader() { 20 | Thread.currentThread().contextClassLoader = null 21 | 22 | val iCal = "BEGIN:VCALENDAR\n" + 23 | "PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN\n" + 24 | "VERSION:2.0\n" + 25 | "BEGIN:VEVENT\n" + 26 | "UID:uid1@example.com\n" + 27 | "DTSTART:19960918T143000Z\n" + 28 | "DTEND:19960920T220000Z\n" + 29 | "SUMMARY:Networld+Interop Conference\n" + 30 | "END:VEVENT\n" + 31 | "END:VCALENDAR\n" 32 | val result = CalendarBuilder().build(StringReader(iCal)) 33 | val vEvent = result.getComponent(Component.VEVENT) 34 | assertEquals("Networld+Interop Conference", vEvent.summary.value) 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidDayOffsetPreprocessorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.validation 8 | 9 | import org.junit.Assert.assertEquals 10 | import org.junit.Assert.assertFalse 11 | import org.junit.Assert.assertTrue 12 | import org.junit.Test 13 | import java.time.Duration 14 | 15 | class FixInvalidDayOffsetPreprocessorTest { 16 | 17 | /** 18 | * Calls [FixInvalidDayOffsetPreprocessor.fixString] and asserts the result is equal to [expected]. 19 | * 20 | * @param expected The expected result 21 | * @param testValue The value to test 22 | * @param parseDuration If `true`, [Duration.parse] is called on the fixed value to make sure it's a valid duration 23 | */ 24 | private fun assertFixedEquals(expected: String, testValue: String, parseDuration: Boolean = true) { 25 | // Fix the duration string 26 | val fixed = FixInvalidDayOffsetPreprocessor.fixString(testValue) 27 | 28 | // Test the duration can now be parsed 29 | if (parseDuration) 30 | for (line in fixed.split('\n')) { 31 | val duration = line.substring(line.indexOf(':') + 1) 32 | Duration.parse(duration) 33 | } 34 | 35 | // Assert 36 | assertEquals(expected, fixed) 37 | } 38 | 39 | @Test 40 | fun test_FixString_NoOccurrence() { 41 | assertEquals( 42 | "Some String", 43 | FixInvalidDayOffsetPreprocessor.fixString("Some String"), 44 | ) 45 | } 46 | 47 | @Test 48 | fun test_FixString_SucceedsAsValueOnCorrectProperties() { 49 | // By RFC 5545 the only properties allowed to hold DURATION as a VALUE are: 50 | // DURATION, REFRESH, RELATED, TRIGGER 51 | assertFixedEquals("DURATION;VALUE=DURATION:P1D", "DURATION;VALUE=DURATION:PT1D") 52 | assertFixedEquals("REFRESH-INTERVAL;VALUE=DURATION:P1D", "REFRESH-INTERVAL;VALUE=DURATION:PT1D") 53 | assertFixedEquals("RELATED-TO;VALUE=DURATION:P1D", "RELATED-TO;VALUE=DURATION:PT1D") 54 | assertFixedEquals("TRIGGER;VALUE=DURATION:P1D", "TRIGGER;VALUE=DURATION:PT1D") 55 | } 56 | 57 | @Test 58 | fun test_FixString_FailsAsValueOnWrongProperty() { 59 | // The update from RFC 2445 to RFC 5545 disallows using DURATION as a VALUE in FREEBUSY 60 | assertFixedEquals("FREEBUSY;VALUE=DURATION:PT1D", "FREEBUSY;VALUE=DURATION:PT1D", parseDuration = false) 61 | } 62 | 63 | @Test 64 | fun test_FixString_FailsIfNotAtStartOfLine() { 65 | assertFixedEquals("xxDURATION;VALUE=DURATION:PT1D", "xxDURATION;VALUE=DURATION:PT1D", parseDuration = false) 66 | } 67 | 68 | @Test 69 | fun test_FixString_DayOffsetFrom_Invalid() { 70 | assertFixedEquals("DURATION:-P1D", "DURATION:-PT1D") 71 | assertFixedEquals("TRIGGER:-P2D", "TRIGGER:-PT2D") 72 | 73 | assertFixedEquals("DURATION:-P1D", "DURATION:-P1DT") 74 | assertFixedEquals("TRIGGER:-P2D", "TRIGGER:-P2DT") 75 | } 76 | 77 | @Test 78 | fun test_FixString_DayOffsetFrom_Valid() { 79 | assertFixedEquals("DURATION:-PT12H", "DURATION:-PT12H") 80 | assertFixedEquals("TRIGGER:-PT12H", "TRIGGER:-PT12H") 81 | } 82 | 83 | @Test 84 | fun test_FixString_DayOffsetFromMultiple_Invalid() { 85 | assertFixedEquals("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-PT1D\nTRIGGER:-PT2D") 86 | assertFixedEquals("DURATION:-P1D\nTRIGGER:-P2D", "DURATION:-P1DT\nTRIGGER:-P2DT") 87 | } 88 | 89 | @Test 90 | fun test_FixString_DayOffsetFromMultiple_Valid() { 91 | assertFixedEquals("DURATION:-PT12H\nTRIGGER:-PT12H", "DURATION:-PT12H\nTRIGGER:-PT12H") 92 | } 93 | 94 | @Test 95 | fun test_FixString_DayOffsetFromMultiple_Mixed() { 96 | assertFixedEquals("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-PT1D\nDURATION:-PT12H\nTRIGGER:-PT2D") 97 | assertFixedEquals("DURATION:-P1D\nDURATION:-PT12H\nTRIGGER:-P2D", "DURATION:-P1DT\nDURATION:-PT12H\nTRIGGER:-P2DT") 98 | } 99 | 100 | @Test 101 | fun test_RegexpForProblem_DayOffsetTo_Invalid() { 102 | val regex = FixInvalidDayOffsetPreprocessor.regexpForProblem() 103 | assertTrue(regex.matches("DURATION:PT2D")) 104 | assertTrue(regex.matches("TRIGGER:PT1D")) 105 | } 106 | 107 | @Test 108 | fun test_RegexpForProblem_DayOffsetTo_Valid() { 109 | val regex = FixInvalidDayOffsetPreprocessor.regexpForProblem() 110 | assertFalse(regex.matches("DURATION:-PT12H")) 111 | assertFalse(regex.matches("TRIGGER:-PT15M")) 112 | } 113 | 114 | } -------------------------------------------------------------------------------- /lib/src/test/kotlin/at/bitfire/ical4android/validation/FixInvalidUtcOffsetPreprocessorTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of ical4android which is released under GPLv3. 3 | * Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details. 4 | * SPDX-License-Identifier: GPL-3.0-or-later 5 | */ 6 | 7 | package at.bitfire.ical4android.validation 8 | 9 | import org.junit.Assert.assertEquals 10 | import org.junit.Assert.assertFalse 11 | import org.junit.Assert.assertTrue 12 | import org.junit.Test 13 | 14 | class FixInvalidUtcOffsetPreprocessorTest { 15 | 16 | @Test 17 | fun test_FixString_NoOccurrence() { 18 | assertEquals( 19 | "Some String", 20 | FixInvalidUtcOffsetPreprocessor.fixString("Some String")) 21 | } 22 | 23 | @Test 24 | fun test_FixString_TzOffsetFrom_Invalid() { 25 | assertEquals("TZOFFSETFROM:+005730", 26 | FixInvalidUtcOffsetPreprocessor.fixString("TZOFFSETFROM:+5730")) 27 | } 28 | 29 | @Test 30 | fun test_FixString_TzOffsetFrom_Valid() { 31 | assertEquals("TZOFFSETFROM:+005730", 32 | FixInvalidUtcOffsetPreprocessor.fixString("TZOFFSETFROM:+005730")) 33 | } 34 | 35 | @Test 36 | fun test_FixString_TzOffsetTo_Invalid() { 37 | assertEquals("TZOFFSETTO:+005730", 38 | FixInvalidUtcOffsetPreprocessor.fixString("TZOFFSETTO:+5730")) 39 | } 40 | 41 | @Test 42 | fun test_FixString_TzOffsetTo_Valid() { 43 | assertEquals("TZOFFSETTO:+005730", 44 | FixInvalidUtcOffsetPreprocessor.fixString("TZOFFSETTO:+005730")) 45 | } 46 | 47 | 48 | @Test 49 | fun test_RegexpForProblem_TzOffsetTo_Invalid() { 50 | val regex = FixInvalidUtcOffsetPreprocessor.regexpForProblem() 51 | assertTrue(regex.matches("TZOFFSETTO:+5730")) 52 | } 53 | 54 | @Test 55 | fun test_RegexpForProblem_TzOffsetTo_Valid() { 56 | val regex = FixInvalidUtcOffsetPreprocessor.regexpForProblem() 57 | assertFalse(regex.matches("TZOFFSETTO:+005730")) 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /opentasks-contract/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /opentasks-contract/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /opentasks-contract/src/main/java/org/dmfs/tasks/contract/UriFactory.java: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. 3 | **************************************************************************************************/ 4 | 5 | package org.dmfs.tasks.contract; 6 | 7 | import android.net.Uri; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | 13 | /** 14 | * TODO 15 | */ 16 | @SuppressWarnings("ALL") 17 | public final class UriFactory 18 | { 19 | private final String mAuthority; 20 | private final Map mUriMap = new HashMap(16); 21 | 22 | 23 | UriFactory(String authority) 24 | { 25 | mAuthority = authority; 26 | mUriMap.put(null, Uri.parse("content://" + authority)); 27 | } 28 | 29 | 30 | void addUri(String path) 31 | { 32 | mUriMap.put(path, Uri.parse("content://" + mAuthority + "/" + path)); 33 | } 34 | 35 | 36 | Uri getUri() 37 | { 38 | return mUriMap.get(null); 39 | } 40 | 41 | 42 | Uri getUri(String path) 43 | { 44 | return mUriMap.get(path); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. 3 | **************************************************************************************************/ 4 | 5 | pluginManagement { 6 | repositories { 7 | google() 8 | mavenCentral() 9 | gradlePluginPortal() 10 | } 11 | } 12 | dependencyResolutionManagement { 13 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | rootProject.name = "root" 21 | 22 | include ':lib' 23 | project(':lib').name = 'ical4android' 24 | --------------------------------------------------------------------------------