├── .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 |
4 |
5 |
6 |
7 |
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 | [](https://github.com/bitfireAT/ical4android/actions/workflows/test-dev.yml)
8 | [](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 |
--------------------------------------------------------------------------------