Compare commits

..

No commits in common. 'main' and '11.11' have entirely different histories.
main ... 11.11

@ -1,47 +0,0 @@
name: Assemble bundle
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
check:
uses: ./.github/workflows/check.yml
bundle:
runs-on: ubuntu-latest
needs: [ check ]
steps:
- name: Decode Keystore
run: |
echo ${{ secrets.KEY_STORE }} | base64 -di > "${RUNNER_TEMP}"/keystore.jks
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Bundle
env:
KEY_PATH: ${{ runner.temp }}/keystore.jks
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
MAPBOX_KEY: ${{ secrets.MAPBOX_KEY }}
GOOGLE_KEY: ${{ secrets.GOOGLE_KEY }}
run: bundle exec fastlane bundle
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: release
path: app/build/outputs/**

@ -1,91 +0,0 @@
name: Run automated checks
on:
pull_request:
workflow_call:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Lint checks
run: bundle exec fastlane lint
- name: Archive lint reports
uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: lint-reports
path: app/build/reports/*.html
test:
runs-on: ubuntu-latest
strategy:
matrix:
flavor: [Googleplay, Generic]
api-level: [29]
steps:
- name: checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
# - name: AVD cache
# uses: actions/cache@v4
# id: avd-cache
# with:
# path: |
# ~/.android/avd/*
# ~/.android/adb*
# key: avd-${{ matrix.api-level }}
#
# - name: create AVD and generate snapshot for caching
# if: steps.avd-cache.outputs.cache-hit != 'true'
# uses: reactivecircus/android-emulator-runner@v2
# with:
# api-level: ${{ matrix.api-level }}
# force-avd-creation: false
# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# disable-animations: false
# script: echo "Generated AVD snapshot for caching."
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew -Pcoverage app:test${{ matrix.flavor }}DebugUnitTest app:connected${{ matrix.flavor }}DebugAndroidTest
- name: Upload test reports
uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: test-reports-${{ matrix.flavor }}
path: app/build/reports/**

@ -0,0 +1,36 @@
# This workflow will build a Java project with Gradle
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: Check licenses
on:
push:
paths-ignore:
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- README.md
- 'fastlane/**'
- .github/FUNDING.yml
pull_request:
paths-ignore:
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- README.md
- 'fastlane/**'
- .github/FUNDING.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: '11.0.8'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew checkLicense

@ -0,0 +1,42 @@
name: Lint
on:
push:
paths-ignore:
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- README.md
- 'fastlane/**'
- .github/FUNDING.yml
pull_request:
paths-ignore:
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- README.md
- 'fastlane/**'
- .github/FUNDING.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: '11.0.8'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Lint checks
run: bundle exec fastlane lint
- name: Archive lint reports
uses: actions/upload-artifact@v2
with:
name: lint-reports
if: always()
path: app/build/reports/*.html

@ -0,0 +1,37 @@
name: Run tests
on:
push:
paths-ignore:
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- README.md
- 'fastlane/**'
- .github/FUNDING.yml
pull_request:
paths-ignore:
- CODE_OF_CONDUCT.md
- CONTRIBUTING.md
- README.md
- 'fastlane/**'
- .github/FUNDING.yml
jobs:
test:
runs-on: macos-latest
steps:
- name: checkout
uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: '11.0.8'
- name: run tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: ./gradlew -Pcoverage :app:jacocoTestReportGoogleplayDebug :app:createGoogleplayDebugAndroidTestCoverageReport
- name: upload coverage
uses: codecov/codecov-action@v1
with:
directory: ./app/build

2
.gitignore vendored

@ -1,4 +1,3 @@
.kotlin
.idea
*.iml
.gradle
@ -10,4 +9,3 @@ local.properties
Thumbs.db
/captures/
/fastlane/report.xml
/compose-metrics/

@ -1 +1 @@
3.3.3
3.0.2

@ -1,401 +1,486 @@
### 13.11 (2024-06-28)
### 11.11 (2021-09-21)
* New icon picker with over 2,100 icons! (pro feature)
### 13.10 (2024-06-27)
* Add search bar to drawer
* Add search bar to list picker
* Move 'Manage drawer' to ⚙️ > Navigation drawer
* Android 13+ users must grant additional reminder permissions
* Fix completing task multiple times from notification
* Fix deleting new subtasks from edit screen
* Enable Managed DAVx5
* Update translations
* Arabic - @islam2hamy
* Brazilian Portuguese - Jose Delvani
* Chinese (Simplified) - 大王叫我来巡山
* Chinese (Traditional) - hugoalh
* Croatian - @milotype
* Finnish - Rami Lehtinen, @CSharpest
* German - min7-i
* Spanish - gallegonovato
* Turkish - @oersen
### 13.9.9 (2024-05-30)
* Fix import backup crashes
* Fix showing completed subtasks in edit screen
### 13.9.7 (2024-05-23)
* Add default reminders when adding start/due dates to existing tasks [#1846](https://github.com/tasks/tasks/issues/1846)
* Fix import backup crash
### 13.9.6 (2024-05-18)
* Fix widget crash [#2873](https://github.com/tasks/tasks/issues/2873)
* Fix recurrence unable to finish [#2874](https://github.com/tasks/tasks/issues/2874)
* Fix edit screen being cleared when reopening app [#2857](https://github.com/tasks/tasks/issues/2857)
* Fix performance regressions
* Simplified internal alarm scheduling logic
* Update translations
* Arabic - @islam2hamy
* Bulgarian - @StoyanDimitrov
### 13.9 (2024-05-01)
* @elmuffo: Add swipe-to-snooze [#2839](https://github.com/tasks/tasks/pull/2839)
* @IlyaBizyaev: Add option to use quick tile without unlocking device [#2847](https://github.com/tasks/tasks/pull/2847)
* @liz-desartiges: Add support for Z Flip 5 cover screen [#2843](https://github.com/tasks/tasks/pull/2843)
* @purushyb: Fix drawer not updating after editing items [#2855](https://github.com/tasks/tasks/pull/2855)
* @hady-exc: Migrate tag picker screen to Compose [#2849](https://github.com/tasks/tasks/pull/2849)
* @yurtpage: Add Russian app store description [#2848](https://github.com/tasks/tasks/pull/2848)
* Fix duplicate notifications [#2835](https://github.com/tasks/tasks/issues/2835)
* Fix adding '(Completed)' to calendar entries [#2832](https://github.com/tasks/tasks/issues/2832)
* Fix hiding empty items from drawer [#2831](https://github.com/tasks/tasks/issues/2831)
* Exclude old snoozed tasks from snoozed task filter
* Update translations
* Brazilian Portuguese - @mayhmemo, @gorgonun
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Esperanto - Don Zouras
* French - Lionel HANNEQUIN
* German - sorifukobexomajepasiricupuva33, min7-i
* Portuguese - @fparri, @laralem
* Spanish - gallegonovato
* Swedish - @JonatanWick
* Turkish - @emintufan, @oersen
### 13.8.1 (2024-03-24)
* Fix copy causing duplicate Google Tasks
* Fix navigation drawer crash
* Fix backup import dropping tasks
### 13.8 (2024-03-22)
* Dynamic widget theme (name-your-price subscription required)
* Replace 'until' with 'ends on' for repeating tasks [#2797](https://github.com/tasks/tasks/pull/2797) - @akwala
* Fix loading selected list on startup [#2777](https://github.com/tasks/tasks/issues/2777)
* Fix repeating tasks ending one day early
* Fix repeating task crash
* Fix backup import crash
* Fix Astrid manual ordering crash in widget
* Add 'Due now' filter criteria - Thanks @tkterris!
* Fix crash on Android 12 - Thanks @tkterris!
* Fix preference display issue - Thanks @Groctel!
* Target Android 12
* Ignore link clicks during multi-select
* Update translations
* Brazilian Portuguese - @mayhmemo
* Arabic - @mhmdanas, @machiav3lli
* Basque - @Thadah
* Brazilian Portuguese - @laralem
* Bulgarian - @StoyanDimitrov
* Catalan - @ferranpujolcamins
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Czech - Odweta
* German - @macpac59
* Italian - @ppasserini
* Spanish - gallegonovato
* Czech - @vitSkalicky
* Danish - @Tntdruid
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @machiav3lli, J. Lavoie
* Greek - @giorgio93p
* Indonesian - @erigmac
* Italian - J. Lavoie, @Fs00
* Japanese - さとうまこと
* Lithuanian - @70h
* Norwegian Bokmål - @comradekingu
* Portuguese - @laralem
* Romanian - Simona Iacob
* Russian - @tolstovka, @zhelemysh, @ToxesFoxes
* Simplified Chinese - @sr093906, @Geeyun-JY3
* Sinhala - @Dilshan-H
* Spanish - @FlorianLeChat, @Groctel, @berman00
* Swedish - @bittin
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk
* Vietnamese - @ngocanhtve
### 13.7 (2024-02-07)
* Fix returning to previous filter after search [#2700](https://github.com/tasks/tasks/pull/2700)
* Fix wearable notifications on Android 14+
* Fix issue causing repeating tasks to not repeat
* Fix dragging a task into a subtask in another list
* Rewrote navigation drawer in Jetpack Compose
* Internal changes to navigation
* Enable multi-select when adding attachments
* Show count of tasks to be deleted when clearing completed
* Include hidden subtasks when clearing completed [#2724](https://github.com/tasks/tasks/issues/2724)
* Don't show hidden or completed tasks in snoozed filter
* Remove markdown from repeating task snackbar
* Vietnamese - bruh
### 11.10.2 (2021-07-15)
* Fix location-based reminders
* Fix preference backup
* Update translations
* Azerbaijani - Shaban Mamedov
* Bulgarian - @StoyanDimitrov
* Catalan - raulmagdalena
* Chinese (Simplified) - 大王叫我来巡山
* Chinese (Traditional) - @abc0922001
* Arabic - git ty, @mhmdanas
* Basque - Sergio Varela
* Croatian - @milotype
* Dutch - @mm4c
* Esperanto - Don Zouras
* Finnish - @millerii
* French - J. Lavoie
* German - @CennoxX
* Hebrew - @elig0n
* Czech - @vitSkalicky, @p-bo
* Dutch - Beardhatcode, @fvbommel
* French - @FlorianLeChat
* German - K. Herbert, @franconian, @ecxod, @bluedeepimpact
* Indonesian - when we were sober
* Interlingua - @softinterlingua
* Odia - @SubhamJena
* Persian - @Monirzadeh
* Spanish - gallegonovato
* Swedish - @bittin
* Turkish - @oersen
* Ukrainian - Сергій
* Vietnamese - @ngocanhtve
### 13.6.3 (2023-11-25)
* Revert "Preserve modification times on initial sync" [#2460](https://github.com/tasks/tasks/issues/2640)
* Fix unnecessary DecSync work
### 13.6.2 (2023-10-30)
* Fix updating modification timestamp on edits
* Italian - J. Lavoie
* Lithuanian - @70h
* Norwegian Bokmål - @Jerome2103
* Portuguese - @laralem
* Russian - @KovalevArtem, @Blueberryy
* Simplified Chinese - @sr093906, @Geeyun-JY3
* Sinhala - HelaBasa
* Spanish - @FlorianLeChat, @fitojb
* Turkish - Oğuz Ersen, @emintufan
* Ukrainian - @IhorHordiichuk
* Urdu - Maaz
* Vietnamese - bruh
### 13.6.1 (2023-10-27)
### 11.10.1 (2021-05-26)
* Push pending changes when app is backgrounded
* Don't require internet connection for DAVx5/EteSync/DecSync sync
* Don't perform background sync for DAVx5/EteSync/DecSync
* Background sync is performed by the sync app
* Preserve modification times on initial sync [#2496](https://github.com/tasks/tasks/issues/2496)
* Replace deprecated method call [#2547](https://github.com/tasks/tasks/pull/2547) - @kmj-99
* Improve task list scrolling performance
* Fix hourly recurrence bug
* Improve Android 12 compatibility
* Update status bar styles
* Update translations
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Czech - @ceskyDJ
* Finnish - @millerii
* French - Lionel HANNEQUIN, Bruno Duyé
* Japanese - Kazushi Hayama
* Portuguese - @loucurapt
* Romanian - @ygorigor
* Swedish - @bittin
* Arabic - @mhmdanas
* Basque - Sergio Varela
* Catalan - @toram
* Chinese (Traditional) - @kisaragi-hiu
* Croatian - @ggdorman
* Czech - @vitSkalicky
* Esperanto - @J053Fabi0, @jakubfabijan
* French - K. Herbert, J. Lavoie
* German - K. Herbert
* Greek - Eugenia Russell
* Hungarian - @gthrepwood
* Indonesian - @andhikapangestu29
* Korean - Sunjae Choi
* Portuguese (Brazil) - @laralem
* Portuguese - @SantosSi, @laralem
* Russian - Nikita Epifanov
* Sinhala - @Dilshan-H
* Spanish - @fitojb
* Ukrainian - @IhorHordiichuk
* Urdu - Maaz
* Vietnamese - bruh
### 13.6 (2023-10-07)
* Change priority with multi-select [#2257](https://github.com/tasks/tasks/pull/2452) - @vulewuxe86
* Automatically select newly copied tasks [#2246](https://github.com/tasks/tasks/pull/2446) - @vulewuxe86
* Reduce minimum size for widgets [#2436](https://github.com/tasks/tasks/pull/2436) - @histefanhere
* Replace deprecated method call [#2526](https://github.com/tasks/tasks/pull/2526) - @kmj-99
* Improve handling text shared to Tasks [#2485](https://github.com/tasks/tasks/issues/2485)
* Use notification audio stream for completion sound
* Notification preference 'More settings' opens channel settings directly
* Respect 'New tasks on top' preference when creating subtasks
* Automatically add due dates for recurring tasks
* Fix crash on startup
### 11.10 (2021-04-19)
* Markdown support ([Documentation](https://tasks.org/docs/markdown))
* Samsung DeX support - Thanks @mhmdanas!
* Update to Google Play Billing v3
* Remove background sync for legacy EteSync v1 accounts
* Update translations
* Brazilian Portuguese - @gorgonun
* Bulgarian - @StoyanDimitrov, @salif
* Catalan - Joan Montané
* Chinese (Simplified) - Poesty Li
* Chinese (Traditional) - @abc0922001
* Arabic - @mhmdanas
* Brazilian Portuguese - @daylightdev
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @qwerty287, deep map, @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Japanese - Kazushi Hayama, Naga
* French - @FlorianLeChat, J. Lavoie
* German - J. Lavoie
* Greek - Michalis, Eugenia Russell
* Indonesian - @liimee
* Italian - J. Lavoie, @Fs00
* Japanese - @kisaragi-hiu
* Kannada - @shashank-p
* Russian - @zhelemysh, Nikita Epifanov
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Swedish - @Anaemix, @bittin
* Turkish - @emintufan, @oersen
* Turkish - Oğuz Ersen
* Ukrainian - @IhorHordiichuk
* Urdu - Maaz
### 13.5.1 (2023-08-02)
### 11.9.2 (2021-03-29)
* Fix crash when importing Google Tasks from a backup file
* Added Burmese translations - @htetoh
* Fix date translation issue - Thanks @mhmdanas!
* Fix misc translation strings - Thanks J. Lavoie!
* Update translations
* Chinese (Simplified) - Poesty Li
* Croatian - @milotype
* Japanese - Kazushi Hayama
* Polish - @alex-ter
* Russian - @alex-ter
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @franconian, Achim Schumacher, J. Lavoie
* Hungarian - kaciokos
* Indonesian - when we were sober
* Italian - @Fs00
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
* Vietnamese - @unbiaseduser
### 13.5 (2023-07-28)
### 11.9.1 (2021-03-25)
* New custom recurrence picker
* Open documentation links in custom tabs
* Fix crash in Mapbox reverse geocoder
* Increase 'Add subtask' touch target
* Update translations
* Bulgarian - @StoyanDimitrov
* Czech - @ceskyDJ
* Arabic - @mhmdanas
* German - Achim Schumacher
* Hungarian - kaciokos
* Italian - @Fs00
* Turkish - @emintufan
### 11.9 (2021-03-20)
* New calendar and clock pickers
* New preference to default to text input for date and time
* Fix issue causing Tasks to use wrong search provider
* Fix crash when Nextcloud/ownCloud don't send list owner
* Update translations
* Basque - Sergio Varela
* Croatian - @milotype
* Dutch - @fvbommel
* French - @FlorianLeChat
* Italian - @ppasserini
* German - Achim Schumacher
* Hungarian - kaciokos
* Indonesian - when we were sober
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Ukrainian - @IhorHordiichuk
### 13.4 - (2023-07-16)
### 11.8 (2021-03-15)
* Sorting improvements
* Add subtask sort configuration
* Update sort menu button design
* Don't show subtasks of hidden tasks in 'My Tasks'
* Fix Google Tasks sync issue
* CalDAV: Send shared list invites
* Compatible with Tasks.org, Nextcloud, ownCloud, and sabre/dav
* Show shared list invite status in list settings
* Fix drawer count when list is shared with 2+ users
* Removed legacy EteSync v1 list management features
* Dropped support for Android 6.0
* Update translations
* Bulgarian - @StoyanDimitrov
* Catalan - @and4po, Eudald Puy Polls
* Croatian - @milotype
* Arabic - @mhmdanas
* Dutch - @fvbommel
* German - @schneidr
* Hungarian - Kaci
* Japanese - Naga
* Korean - Sunjae Choi
* Portuguese - @laralem
* Swedish - @bittin
* Esperanto - @jakubfabijan
* French - @FlorianLeChat
* German - @Jerome2103
* Hungarian - kaciokos
* Indonesian - when we were sober, @andhikapangestu29
* Norwegian Bokmål - @comradekingu
* Polish - @doegedomita
* Portuguese - @Jerome2103
* Spanish - @FlorianLeChat
* Turkish - Oğuz Ersen
* Ukrainian - @IhorHordiichuk
### 13.3.2 - (2023-06-02)
### 11.7 (2021-03-08)
* Sorting improvements
* Configure sort grouping
* Configure sorting within sort group
* Configure completed task sorting
* Fix Google Task list chips showing on widget
* CalDAV: Display shared list members in list settings
* Compatible with Tasks.org, Nextcloud, ownCloud, OpenXchange, and sabre/dav
* CalDAV: List owners can remove shared list members from list
* Compatible with Tasks.org, Nextcloud, ownCloud, and sabre/dav
* Fix time zone issue in recurrence picker
* Update translations
* Bulgarian - @StoyanDimitrov
* Catalan - @and4po
* Chinese (Simplified) - Poesty Li
* Croatian - @milotype
* Arabic - @mhmdanas
* Basque - Sergio Varela
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @qwerty287, @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Hungarian - kaciokos
* Indonesian - @putulopi
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Turkish - @emintufan, Oğuz Ersen
* Ukrainian - @IhorHordiichuk
### 13.2.4 - (2023-05-24)
* Add 'By list' sort mode [#1265](https://github.com/tasks/tasks/issues/1265)
* Save task when pressing done [#2125](https://github.com/tasks/tasks/pull/2125)
* Use ISO 8601 date formatting for backup filenames [#1550](https://github.com/tasks/tasks/pull/1550)
* Fix filter sorting bug [#1561](https://github.com/tasks/tasks/issues/1561)
* Fix manual sorting crash [#2141](https://github.com/tasks/tasks/issues/2141)
* Fix manual sorting bug [#2101](https://github.com/tasks/tasks/issues/2101)
* Fix multiple accounts on same server [#2301](https://github.com/tasks/tasks/issues/2301)
* Don't set `COUNT=0` on recurrence rules [#2158](https://github.com/tasks/tasks/issues/2158)
* Improve task list performance [#2062](https://github.com/tasks/tasks/issues/2062)
* Attempt to hide inactive widgets in settings [#2145](https://github.com/tasks/tasks/issues/2145)
* Disable persistent reminders on Android 14+
* Android 14+ no longer supports persistent reminders 😢
* Fix notifications on Android 14
* Fix crash when missing exact alarm permissions
* Update logic for adding default reminders during sync
* Don't add reminders on initial sync
* Don't add reminders if other client supports reminder sync
* Internal database changes
* You will need to reconfigure any widgets that were set to display a Google
Task list or filter. Sorry for the interruption!
* Add Odia translations - @SubhamJena
### 11.6.1 (2021-03-11)
* F-Droid: Fix OpenStreetMap crash
### 11.6 (2021-03-04)
* CalDAV: Display indicator in drawer when a list is shared with other users
* Compatible with Tasks.org, Nextcloud, ownCloud, OpenXchange, and sabre/dav
* CalDAV: Don't upload changes to read-only lists
([#931](https://github.com/tasks/tasks/issues/931))
* Remove unnecessary icon-mirroring for RTL users
([#1385](https://github.com/tasks/tasks/issues/1385) and
[#1391](https://github.com/tasks/tasks/pull/1391)) - Thanks to @mhmdanas
* Update translations
* Brazilian Portuguese - @lnux-usr
* Arabic - @mhmdanas
* Basque - Sergio Varela
* Bulgarian - @StoyanDimitrov
* Catalan - @and4po
* Chinese (Simplified) - Poesty Li
* Chinese (Traditional) - Chih-Hsuan Yen
* Croatian - @milotype
* Czech - @vitSkalicky
* Dutch - @fvbommel
* Esperanto - Don Zouras
* Finnish - @millerii
* French - @FlorianLeChat
* Italian - @ppasserini
* Japanese - @kisaragi-hiu, Naga
* Korean - Sunjae Choi, @o20n3
* Romanian - @simonaiacob
* Russian - @AHOHNMYC
* Hungarian - kaciokos
* Indonesian - @putulopi
* Russian - Nikita Epifanov
* Simplified Chinese - @sr093906
* Sinhala - HelaBasa
* Spanish - @FlorianLeChat
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk
### 13.1.2 (2023-02-02)
### 11.5.2 (2021-02-25)
* Add default reminders to incoming iCalendar tasks [#1984](https://github.com/tasks/tasks/issues/1984)
* Sync when brought to the foreground [#2096](https://github.com/tasks/tasks/issues/2096)
* Update translations
* Arabic - haidarah esmander
* Czech - @SlavekB
* Danish - Tntdruid
* Esperanto - Don Zouras, @J053Fabi0
* Finnish - @millerii
* German - @franconian
* Italian - @ppasserini
* Japanese - Kazushi Hayama
* Korean - @o20n3
* Polish - @gnu-ewm
* Vietnamese - @unbiaseduser
### 13.1.1 (2022-12-06)
* Fix crash when opening notification settings
* Fix IAP errors in some locales
* Update translations
* Italian - @ppasserini
* Japanese - Kazushi Hayama
* Fix CalDAV sync error
* Report errors when generating recurrence dates
### 13.1.0 (2022-11-30)
### 11.5.1 (2021-02-24)
* Support for DAVx5 and CalDAV read-only lists [#931](https://github.com/tasks/tasks/issues/931)
* Use default Android network security configuration
* Fix 'repeat until' date
* Fix repeat dates for UTC+13
([#1374](https://github.com/tasks/tasks/issues/1374))
* F-Droid: Handle null name in Nominatim reverse geocoder
([#1380](https://github.com/tasks/tasks/issues/1380))
* Update translations
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Basque - Sergio Varela
* Croatian - @ggdorman
* Dutch - @fvbommel
* Finnish - @millerii
* French - @FlorianLeChat
* German - @helloworldtest123
* Hungarian - Kaci
* Italian - @ppasserini
* Lithuanian - @70h
* Hungarian - kaciokos
* Norwegian Bokmål - @comradekingu
* Polish - @alex-ter
* Russian - Nikita Epifanov
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Turkish - @ersen0
* Turkish - Oğuz Ersen
* Ukrainian - @IhorHordiichuk
### 13.0.2 (2022-11-22)
* Fix persistent notifications on Android 13
* Fix Samsung crash on too many reminders (DAVx5, EteSync, DecSync CC)
* Fix crash on too many tasks for Astrid Manual Sorting
* Fix RTL text in task edit customization screen
* Fix priority button order
### 13.0.1 (2022-10-20)
* 🚨 Major internal changes to task edit screen. Please report any bugs! 🚨
* Show thumbnails for attachments
* Tap on existing alarms to replace them
* Add task info row to edit screen [#1839](https://github.com/tasks/tasks/pull/1839)
* Add option to disable reminders for all-day tasks [#2003](https://github.com/tasks/tasks/pull/2003)
* Updated chip style
* Show geofence circle in place settings
* Fix removing preferences [#1981](https://github.com/tasks/tasks/pull/1981)
* Set user-agent on HTTP requests [#1978](https://github.com/tasks/tasks/issues/1978)
* Preserve HTTP session cookies [#1978](https://github.com/tasks/tasks/issues/1978)
* Sort selected tags at top of tag picker
* Android 13 support
* Runtime notification permissions
* Language preference
* Improvements to copying tasks
* Don't forget parent when copying tasks [#1964](https://github.com/tasks/tasks/pull/1964)
* Copy attachments when duplicating tasks [#812](https://github.com/tasks/tasks/issues/812)
* Fix duplicating subtasks
* Fix some missing reminders
* Incoming Google Tasks
* Tasker tasks [#1937](https://github.com/tasks/tasks/issues/1937)
* New subtasks [#1914](https://github.com/tasks/tasks/issues/1914)
* Fix Google Task creation time
* Fix EteSync stops synchronizing [#1893](https://github.com/tasks/tasks/issues/1893)
* Don't overwrite coordinates when synchronizing locations [#1667](https://github.com/tasks/tasks/issues/1667)
* Urdu - Maaz
### 11.5 (2021-02-17)
* Sync snooze time with Tasks.org, DAVx⁵, CalDAV, EteSync, and DecSync
* Compatible with Thunderbird
* New map theme preference
* 10 new icons
* F-Droid: Use Nominatim for reverse geocoding
* Google Play: Use OpenStreetMap tiles when Play Services not available
* Google Play: Use Android location services when Play Services not available
* Tasks.org accounts: Use Google Places for map search
* Update translations
* Asturian - @enolp
* Basque - Sergio Varela
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Czech - Shimon
* Dutch - @fvbommel
* French - @FlorianLeChat, J. Lavoie
* German - @qwerty287
* Italian - @ppasserini
* French - @FlorianLeChat
* Hungarian - kaciokos
* Indonesian - when we were sober
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Ukrainian - @IhorHordiichuk
### 11.4 (2021-02-09)
* Sync collapsed subtask state with Tasks.org, DAVx⁵, CalDAV, EteSync, and
DecSync ([#1339](https://github.com/tasks/tasks/issues/1339))
* Compatible with Nextcloud and ownCloud
* F-Droid: Add location based reminders ([#770](https://github.com/tasks/tasks/issues/770))
* F-Droid: Replace Mapbox tiles with OpenStreetMap tiles ([#922](https://github.com/tasks/tasks/issues/922))
* Fix default start date ([#1350](https://github.com/tasks/tasks/issues/1350))
### 11.3.4 (2021-02-03)
* Adjust start times by one second during sync
([#1326](https://github.com/tasks/tasks/issues/1326))
* Can now sync start time = due time with DAVx⁵, EteSync app, and DecSync CC
* All day start date must come before all day due date with DAVx⁵, EteSync
app, and DecSync CC
* 'Show unstarted' toggled on by default
### 11.3.3 (2021-01-30)
* Fix all-day due date synchronization
([#1325](https://github.com/tasks/tasks/issues/1325))
### 11.3.2 (2021-01-28)
* Fix recurrence sync issue
([#1323](https://github.com/tasks/tasks/issues/1323))
### 11.3.1 (2021-01-27)
* Improve support for recurring tasks with subtasks
* Subtasks will be unchecked after completing a recurring task
* Clear completed will not delete subtasks of recurring tasks
* Improve widget sort header when space is limited
* Add option to hide widget title
* Fix timezone conversions during synchronization
* Add Esperanto translations - @jakubfabijan
### 11.3 (2021-01-20)
* 'Hide until' is now 'Start date'
* Synchronize start dates with Tasks.org, DAVx⁵, CalDAV, EteSync, and DecSync
* New start date picker
* New start date custom filter criteria
* Add sort 'By start date'
* Display start dates as chips
* Don't perform background sync when data saver enabled
* Preference changes
* Add app and widget preferences to disable start date chips
* Synchronization accounts displayed on main preference screen
* Removed background sync and metered connection options (now respecting data
saver mode)
* Removed Google Tasks 'Custom order synchronization fix' (automatically
performing full sync if 'My order' enabled)
* Remove support for legacy XML backup format ([more info](https://github.com/tasks/tasks/issues/1565))
* Bug fixes
### 11.2.2 (2021-01-07)
* Rename 'Lists' to 'Local lists' to clarify that they are not synchronized
* Tasks.org sign in improvements
* Miscellaneous improvements - Thanks @mhmdanas!
### 11.2.1 (2021-01-05)
* Fix Portuguese translation issue
* Report OpenTask sync errors
* Report Tasks.org sign in errors
* Don't crash on widget configuration error
* Purchase dialog changes
### 11.2 (2020-12-30)
* [Synchronize your Tasks.org account with third-party task and calendar apps, like Outlook,
Thunderbird, or Apple Reminders](https://tasks.org/passwords)
* Miscellaneous improvements - Thanks @mhmdanas!
### 11.1.1 (2020-12-24)
* Fix compatibility issues with third-party clients
* Completed tasks without completion dates
([222a34f](https://github.com/tasks/tasks/commit/222a34fc263816bb23f633bc9c79de78aeb3968d))
* Tasks with start date but no due date
([7a1d566](https://github.com/tasks/tasks/commit/7a1d566bfb613b95d3fe1df46d8fa67200c91021))
* Miscellaneous improvements - Thanks @mhmdanas!
### 11.1 (2020-12-21)
* Add [DecSync CC synchronization](https://tasks.org/decsync)
* Fix rescheduling remotely completed recurring task
([5eb9370](https://github.com/tasks/tasks/commit/5eb9370294ef707b3e667c4a42851030419920d8))
* Miscellaneous code improvements - Thanks @mhmdanas!
### 11.0.1 (2020-12-17)
* Fix EteSync client issue with v2 accounts
([b761309](https://github.com/tasks/tasks/commit/b76130902ae0be6e1d580d588798a9ed0d7ff385))
* Fix multi-select 'Pick time' crash
* Fix default hide until due time
([#842](https://github.com/tasks/tasks/issues/842#issuecomment-746358382))
* Add Croatian translations - Garden Hose
* Add Urdu translations - Maaz
### 11.0 (2020-12-10)
* New Tasks.org synchronization service
* Multi-select rescheduling
* New task default settings
* Default tags
* Default recurrence
* Default location
* Hide until due time
* New custom filter criteria
* Hidden tasks
* Completed tasks
* Subtasks
* Parent tasks
* Recurring tasks
* Added EteSync v2 support
* Deprecated EteSync v1 support
* v1 accounts cannot be added to Tasks.org
* v1 accounts can be added to the EteSync Android client
* Add ability to delete comments (Thanks to @romedius!)
* Add option to always display date (Thanks to @T0M0F!)
* Copy subtasks when copying tasks (Thanks to @supermzn!)
* Fix ring five times cutoff (Thanks to @przemhb!)
* Bug fixes
* Translation updates
* Arabic - @mhmdanas
* Basque - @osoitz, @ppasserini
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @franconian, J. Lavoie, @myabc
* Hebrew - @yarons
* Hungarian - kaciokos
* Indonesian - @andikatuluspangestu
* Italian - @ppasserini, @Fs00, @pjammo
* Korean - Sunjae Choi, @Hwaro-K
* Norwegian Bokmål - @comradekingu
* Persian - @latelateprogrammer
* Polish - @ebogucka
* Portuguese - @laralem
* Romanian - @simonaiacob
* Russian - @Allineer, Nikita Epifanov
* Sinhala - @Dilshan-H
* Polish - @alex-ter
* Russian - Nikita Epifanov
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk, @artemmolotov
* Vietnamese - @unbiaseduser
* Traditional Chinese - @realpineapplemilk
* Turkish - @emintufan, Oğuz Ersen
### 10.4.1 (2020-11-09)
* Fix Mapbox Maps crash on Android 11 (F-Droid only)
### 10.4 (2020-10-09)
* New widget configuration options
* Sort
* Show hidden
* Show completed
* Header spacing
* Bug fixes
### 10.3 (2020-10-02)
* Collapsible sort groups in widget
* Add 'System default' widget theme
* Bug fixes
### 10.2 (2020-09-25)
* Display list, tag, and place chips on widgets
* Add option to disable list, tag, and place chips on widgets
### 10.1 (2020-09-23)
* Android 11 support
* Backup improvements
* Swipe-to-refresh initiates DAVx5/EteSync sync
* Show indicator when DAVx5/EteSync are synchronizing
* Bug fixes
### 10.0.3 (2020-09-16)
* Fix crash from calendar event snackbar
* Fix crash when setting Google Maps markers
* Fix invalid calendar entry creation
### 10.0.2 (2020-09-14)
* Fix crash from corrupted custom filter
* Fix crash in 'Astrid manual sorting' mode
* Fix missing 'Calendar event created' snackbar
### 10.0.1 (2020-09-05)
* Bug fixes
* Translation updates
* Czech - @vitSkalicky
* Danish - @ChMunk
### 10.0 (2020-08-31)
* PRO: DAVx⁵ support (requires [DAVx⁵ beta](https://tasks.org/davx5))
* PRO: EteSync client support
* [ToDo Agenda](https://play.google.com/store/apps/details?id=org.andstatus.todoagenda) integration
* Changed backstack behavior to follow Android conventions
* Major internal changes! Please report any bugs!
* Remove Mapbox tiles (Google Play only)
* Added 'Astrid manual sort' information to backup file
* Bug fixes
* Performance improvements
* Security improvements
[Older releases](https://github.com/tasks/tasks/blob/main/V10_12_CHANGELOG.md)
[Older releases](https://github.com/tasks/tasks/blob/main/V06_09_CHANGELOG.md)

@ -1,55 +1,51 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.17)
CFPropertyList (3.0.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.944.0)
aws-sdk-core (3.197.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.85.0)
aws-sdk-core (~> 3, >= 3.197.0)
aws-eventstream (1.2.0)
aws-partitions (1.503.0)
aws-sdk-core (3.121.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.152.3)
aws-sdk-core (~> 3, >= 3.197.0)
jmespath (~> 1.0)
aws-sdk-kms (1.48.0)
aws-sdk-core (~> 3, >= 3.120.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.103.0)
aws-sdk-core (~> 3, >= 3.120.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sigv4 (1.8.0)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.5)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.110.0)
faraday (1.10.3)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.2)
excon (0.85.0)
faraday (1.8.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-httpclient (~> 1.0.1)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
@ -58,24 +54,21 @@ GEM
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday_middleware (1.1.0)
faraday (~> 1.0)
fastimage (2.3.1)
fastlane (2.221.1)
fastimage (2.2.5)
fastlane (2.194.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
colored
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
@ -87,32 +80,30 @@ GEM
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
google-apis-androidpublisher_v3 (0.11.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@ -120,84 +111,87 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a)
webrick
google-apis-iamcredentials_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.5.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.6.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.1.0)
google-cloud-storage (1.34.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
googleauth (0.17.1)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
signet (~> 0.15)
highline (2.0.3)
http-cookie (1.0.6)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.2)
base64
mini_magick (4.13.1)
mini_mime (1.1.5)
jmespath (1.4.0)
json (2.5.1)
jwt (2.2.3)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.1)
multi_json (1.15.0)
multipart-post (2.4.1)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
nkf (0.2.0)
optparse (0.5.0)
os (1.1.4)
plist (3.7.1)
public_suffix (5.1.1)
rake (13.2.1)
representable (3.2.0)
optparse (0.1.1)
os (1.1.1)
plist (3.6.0)
public_suffix (4.0.6)
rake (13.0.6)
representable (3.1.1)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.9)
strscan
rexml (3.2.5)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.5)
signet (0.19.0)
security (0.1.3)
signet (0.16.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
simctl (1.6.8)
CFPropertyList
naturally
strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.1)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.5.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.24.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
@ -216,4 +210,4 @@ DEPENDENCIES
fastlane
BUNDLED WITH
2.2.32
2.2.22

@ -11,11 +11,11 @@ Please visit [tasks.org](https://tasks.org) for end user documentation and suppo
---
[![Donate with Bitcoin](https://img.shields.io/badge/bitcoin-donate-yellow.svg?logo=bitcoin)](https://tasks.org/docs/donate)
[![Donate with Bitcoin](https://img.shields.io/badge/bitcoin-donate-yellow.svg?logo=bitcoin)](https://en.cryptobadges.io/donate/136mW34jW3cmZKhxuTDn3tHXMRwbbaRU8s)
[![PayPal donate button](https://img.shields.io/badge/paypal-donate-yellow.svg?logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=alex@tasks.org)
[![Liberapay donate button](https://img.shields.io/liberapay/receives/tasks.svg?logo=liberapay)](https://liberapay.com/tasks/donate)
[![build](https://github.com/tasks/tasks/actions/workflows/bundle.yml/badge.svg)](https://github.com/tasks/tasks/actions/workflows/bundle.yml) [![weblate](https://hosted.weblate.org/widgets/tasks/-/android/svg-badge.svg)](https://hosted.weblate.org/engage/tasks/?utm_source=widget) [![codebeat badge](https://codebeat.co/badges/07924fca-2f18-4eff-99a3-120ec5ac2d5f)](https://codebeat.co/projects/github-com-tasks-tasks-main)
![tests](https://github.com/tasks/tasks/workflows/Run%20tests/badge.svg) [![weblate](https://hosted.weblate.org/widgets/tasks/-/android/svg-badge.svg)](https://hosted.weblate.org/engage/tasks/?utm_source=widget) [![codecov](https://codecov.io/gh/tasks/tasks/branch/main/graph/badge.svg)](https://codecov.io/gh/tasks/tasks) [![codebeat badge](https://codebeat.co/badges/07924fca-2f18-4eff-99a3-120ec5ac2d5f)](https://codebeat.co/projects/github-com-tasks-tasks-main)
### Contributing

@ -1,767 +0,0 @@
### 12.7 (2022-06-18)
* Android 13 themed icon - Thanks @hanthor!
* Fix self-signed SSL certificates on Android 12+
* Don't hide empty tags and places in pickers
* Update translations
* Basque - @Txopi, Sergio Varela, @osoitz
* Belarusian - @Prominence, Андрей
* Bulgarian - @StoyanDimitrov
* Czech - Shimon
* Danish - Tntdruid
* Dutch - @mm4c
* German - @3ole
* Hungarian - kaciokos
* Indonesian - Cyua Pyua
* Italian - @ppasserini
* Polish - @wiktor-k
* Portuguese (Brazilian) - @LevyMarCiS, @sunflowerskater
* Portuguese - @laralem, @alvar0liveira
* Swedish - @reportxx
* Turkish - @emintufan
* Vietnamese - @unbiaseduser
### 12.6.1 (2022-03-27)
* Move task list and edit screen options to top level settings
* Prompt users to customize edit screen
* Fix cancel button for recurring reminder dialog
* Update translations
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - Eric, @Geeyun-JY3
* Croatian - @milotype
* Dutch - @mm4c, @fvbommel
* Finnish - J. Lavoie
* French - @FlorianLeChat
* Galician - @mglbranco, J. Lavoie
* German - @qwerty287
* Hungarian - kaciokos
* Italian - @Fs00
* Norwegian Bokmål - @comradekingu
* Polish - @wiktor-k
* Portuguese (Brazilian) - @tsunamistonefly
* Romanian - @simonaiacob
* Russian - Nikita Epifanov
* Spanish - @FlorianLeChat
* Swedish - @reportxx
* Turkish - @ersen0, @emintufan
* Ukrainian - @IhorHordiichuk
* Vietnamese - @unbiaseduser, J. Lavoie
### 12.6 (2022-03-12)
* Configure notifications to repeat at custom intervals
([#3](https://github.com/tasks/tasks/issues/3))
* Notifications can repeat by minute, hour, day, or weekly intervals
* Add 'Snoozed' filter ([#1633](https://github.com/tasks/tasks/issues/1633))
* Add 'Notifications' filter
* CalDAV/DAVx5 server selection setting
* This replaces 'Let server schedule recurring tasks'
* Synology Calendar users must set this to fix sync
([#1802](https://github.com/tasks/tasks/issues/1802))
* Mailbox.org and Open-Xchange users must set this to prevent duplicate
repeating tasks
* Set geofence radius in place settings
* Remove DAVx5/EteSync app accounts when native CalDAV/EteSync enabled
* Clear reminders when they are dismissed in Thunderbird
* Fix reminder synchronization
* Fix crash in task edit screen
* Fix prompt to discard changes
* Fix crash during 12.4 upgrade
* Update translations
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - @Crystal-RainSlide, @Geeyun-JY3, Eric
* Croatian - @milotype
* Dutch - @mm4c, @fvbommel
* French - J. Lavoie, @FlorianLeChat
* German - @eldiep, J. Lavoie, @qwerty287
* Hungarian - kaciokos
* Italian - @ppasserini, J. Lavoie
* Portuguese (Brazilian) - @hugomg
* Romanian - @simonaiacob
* Russian - @Allineer
* Spanish - @toni-em, @FlorianLeChat, @Romerolweb
* Swedish - @reportxx
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk
* Urdue - @Crystal-RainSlide
* Vietnamese - @unbaseduser
### 12.5 (2022-02-27)
* Choose custom random reminder period
* Add multiple random reminders
* Fix sync crash for Tasks.org, CalDAV, and native EteSync
* Add Kurdish (Central) translations - @roj1512
* Update translations
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Dutch - @mm4c
* French - @FlorianLeChat
* Portuguese - @laralem
* Spanish - @Romerolweb, Jeffree Romero
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk
### 12.4 (2022-02-19)
* Relative reminder support
* Quickly add reminders minutes, hours, days, or weeks before due
* Sync reminders with Tasks.org, DAVx5, CalDAV, EteSync, and DecSync CC
* Synchronize relative and absolute reminders
* Tasks.org, CalDAV, and native EteSync sync improvements
* Merge remote changes before pushing local changes
* Not applicable to DAVx5, EteSync app, or DecSync CC
* View and cancel snoozed reminders in task edit screen
* Add 'Has reminder' custom filter criteria
* Fix updating calendar entries after editing task
* Fix search when using top app bar
* Fix task deletion when adding from two devices simultaneously
* Update translations
* Arabic - @mhmdanas
* Basque - Sergio Varela
* Brazilian Portuguese - @Luiz-bro
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Dutch - @mm4c
* French - @FlorianLeChat, J. Lavoie
* German - J. Lavoie, @qwerty287
* Hungarian - kaciokos
* Italian - @ppasserini, J. Lavoie, @andrearosso
* Portuguese - @laralem
* Romanian - @simonaiacob
* Russian - @NikGreens
* Spanish - @FlorianLeChat, Sergio Varela
* Turkish - @ersen0, @emintufan
* Ukrainian - @IhorHordiichuk
* Vietnamese - bruh, @unbaseduser
### 12.3 (2022-02-04)
* Add option to disable moving completed tasks to bottom
* Add option to disable sorting completed by completion date
* Add undo snackbar for task completion
* Fix crash when location lookup fails
* Fix voice reminders on Android 12
* Fix widget due dates in overdue sort group
* Add Karelian translations - Olexii Ondrei
* Update translations
* Basque - Sergio Varela
* Catalan - @ivangjxyz
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Dutch - @mm4c
* French - @FlorianLeChat
* German - @qwerty287
* Hungarian - kaciokos
* Romanian - @simonaiacob
* Russian - @NikGreens
* Spanish - @FlorianLeChat
* Swedish - @reportxx
* Turkish - @emintufan, @ersen0
* Vietnamese - @unbaseduser
### 12.2 (2022-01-16)
* Move completed tasks to bottom
* Add option to disable collapsing app bars
* Uncheck parent tasks when subtask is unchecked
* Fix crash on completion sound
* Update translations
* Chinese (Simplified) - Eric
* Danish - @Tntdruid
* Dutch - @fvbommel, @mm4c
* French - @FlorianLeChat
* German - @qwerty287
* Russian - @NikGreens
* Spanish - @FlorianLeChat
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk
* Vietnamese - @unbaseduser
### 12.1 (2022-01-09)
* Group overdue tasks when sorting by due date
* Update translations
* Basque - Sergio Varela
* Chinese (Simplified) - Eric
* French - @FlorianLeChat
* Norwegian Bokmål - @comradekingu
* Spanish - @FlorianLeChat
* Vietnamese - @unbaseduser
### 12.0 (2022-01-08)
* New bottom app bar
* Choose top or bottom app bar in settings
* Miscellaneous design updates
* Improve privacy and security by removing RECORD_AUDIO and
WRITE_EXTERNAL_STORAGE permissions
* Attaching an audio note will launch your device's audio recorder
* Translation updates
* Catalan - @Solatec
* Dutch - @mm4c
* German - @qwerty287
* Italian - @ppasserini, @Fs00
* Portuguese - @SantosSi
* Romanian - @simonaiacob
* Russian - Nikita Epifanov
* Ukrainian - @IhorHordiichuk
### 11.13 (2021-12-31)
* Add option to play a sound when a task is completed
* Accept audio attachments shared from other apps
* Removed native EteSync v1 support
* EteSync v1 accounts can still be synchronized with the EteSync app
* Bug fixes
* Translation updates
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - @sr093906
* Chinese (Traditional) - @dixon777
* Finnish - @CSharpest, Rami Lehtinen
* French - @FlorianLeChat
* Hungarian - kaciokos
* Italian - J. Lavoie, @Fs00
* Norwegian Bokmål - @comradekingu
* Persian - @Ahmadhosseinbor
* Spanish - @aplopez, @FlorianLeChat
* Ukrainian - @IhorHordiichuk
### 11.12.3 (2021-11-22)
* Fix reminders
* Update translations
* Indonesian - when we were sober
* Kurdish (Northern) - Pêşeroja paşerojê
* Romanian - @Steinhagen
### 11.12.2 (2021-11-13)
* Fix reminders
* Fix reminder preference backup
* Update translations
* Interlingua - @softinterlingua
* Tamil - @balogic
### 11.12.1 (2021-11-05)
* Fix reminders
* Update translations
* Bulgarian - @StoyanDimitrov
* Croatian - @milotype
* Norwegian Bokmål - @HumanNr4584093104
* Romanian - Simona Iacob
* Russian - @NikGreens
* Tamil - @balogic
* Turkish - @ersen0
### 11.12 (2021-10-26)
* Add option to notify at start date
* Widget tweaks for Android 12
* Fix crash when deleting tasks (Thanks @fschrempf!)
* Fix truncated calendar picker
* Update translations
* Basque - Sergio Varela
* Brazilian Portuguese - @laralem
* Bulgarian - @StoyanDimitrov
* Catalan - @Solatec
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @qwerty287
* Hungarian - kaciokos
* Lithuanian - @70h
* Polish - @dominik-korsa
* Simplified Chinese - @sr093906, @Geeyun-JY3
* Ukrainian - @IhorHordiichuk
* Vietnamese - bruh
### 11.11 (2021-09-21)
* Add 'Due now' filter criteria - Thanks @tkterris!
* Fix crash on Android 12 - Thanks @tkterris!
* Fix preference display issue - Thanks @Groctel!
* Target Android 12
* Ignore link clicks during multi-select
* Update translations
* Arabic - @mhmdanas, @machiav3lli
* Basque - @Thadah
* Brazilian Portuguese - @laralem
* Bulgarian - @StoyanDimitrov
* Croatian - @milotype
* Czech - @vitSkalicky
* Danish - @Tntdruid
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @machiav3lli, J. Lavoie
* Greek - @giorgio93p
* Indonesian - @erigmac
* Italian - J. Lavoie, @Fs00
* Japanese - さとうまこと
* Lithuanian - @70h
* Norwegian Bokmål - @comradekingu
* Portuguese - @laralem
* Romanian - Simona Iacob
* Russian - @tolstovka, @zhelemysh, @ToxesFoxes
* Simplified Chinese - @sr093906, @Geeyun-JY3
* Sinhala - @Dilshan-H
* Spanish - @FlorianLeChat, @Groctel, @berman00
* Swedish - @bittin
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk
* Vietnamese - bruh
### 11.10.2 (2021-07-15)
* Fix location-based reminders
* Fix preference backup
* Update translations
* Arabic - git ty, @mhmdanas
* Basque - Sergio Varela
* Croatian - @milotype
* Czech - @vitSkalicky, @p-bo
* Dutch - Beardhatcode, @fvbommel
* French - @FlorianLeChat
* German - K. Herbert, @franconian, @ecxod, @bluedeepimpact
* Indonesian - when we were sober
* Interlingua - @softinterlingua
* Italian - J. Lavoie
* Lithuanian - @70h
* Norwegian Bokmål - @Jerome2103
* Portuguese - @laralem
* Russian - @KovalevArtem, @Blueberryy
* Simplified Chinese - @sr093906, @Geeyun-JY3
* Sinhala - HelaBasa
* Spanish - @FlorianLeChat, @fitojb
* Turkish - Oğuz Ersen, @emintufan
* Ukrainian - @IhorHordiichuk
* Urdu - Maaz
* Vietnamese - bruh
### 11.10.1 (2021-05-26)
* Improve Android 12 compatibility
* Update status bar styles
* Update translations
* Arabic - @mhmdanas
* Basque - Sergio Varela
* Catalan - @toram
* Chinese (Traditional) - @kisaragi-hiu
* Croatian - @ggdorman
* Czech - @vitSkalicky
* Esperanto - @J053Fabi0, @jakubfabijan
* French - K. Herbert, J. Lavoie
* German - K. Herbert
* Greek - Eugenia Russell
* Hungarian - @gthrepwood
* Indonesian - @andhikapangestu29
* Korean - Sunjae Choi
* Portuguese (Brazil) - @laralem
* Portuguese - @SantosSi, @laralem
* Russian - Nikita Epifanov
* Sinhala - @Dilshan-H
* Spanish - @fitojb
* Ukrainian - @IhorHordiichuk
* Urdu - Maaz
* Vietnamese - bruh
### 11.10 (2021-04-19)
* Markdown support ([Documentation](https://tasks.org/docs/markdown))
* Samsung DeX support - Thanks @mhmdanas!
* Update to Google Play Billing v3
* Remove background sync for legacy EteSync v1 accounts
* Update translations
* Arabic - @mhmdanas
* Brazilian Portuguese - @daylightdev
* Dutch - @fvbommel
* French - @FlorianLeChat, J. Lavoie
* German - J. Lavoie
* Greek - Michalis, Eugenia Russell
* Indonesian - @liimee
* Italian - J. Lavoie, @Fs00
* Japanese - @kisaragi-hiu
* Kannada - @shashank-p
* Russian - @zhelemysh, Nikita Epifanov
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Turkish - Oğuz Ersen
* Ukrainian - @IhorHordiichuk
* Urdu - Maaz
### 11.9.2 (2021-03-29)
* Fix date translation issue - Thanks @mhmdanas!
* Fix misc translation strings - Thanks J. Lavoie!
* Update translations
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @franconian, Achim Schumacher, J. Lavoie
* Hungarian - kaciokos
* Indonesian - when we were sober
* Italian - @Fs00
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 11.9.1 (2021-03-25)
* Open documentation links in custom tabs
* Fix crash in Mapbox reverse geocoder
* Increase 'Add subtask' touch target
* Update translations
* Arabic - @mhmdanas
* German - Achim Schumacher
* Hungarian - kaciokos
* Italian - @Fs00
* Turkish - @emintufan
### 11.9 (2021-03-20)
* New calendar and clock pickers
* New preference to default to text input for date and time
* Fix issue causing Tasks to use wrong search provider
* Fix crash when Nextcloud/ownCloud don't send list owner
* Update translations
* Basque - Sergio Varela
* Croatian - @milotype
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - Achim Schumacher
* Hungarian - kaciokos
* Indonesian - when we were sober
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Ukrainian - @IhorHordiichuk
### 11.8 (2021-03-15)
* CalDAV: Send shared list invites
* Compatible with Tasks.org, Nextcloud, ownCloud, and sabre/dav
* Show shared list invite status in list settings
* Fix drawer count when list is shared with 2+ users
* Removed legacy EteSync v1 list management features
* Dropped support for Android 6.0
* Update translations
* Arabic - @mhmdanas
* Dutch - @fvbommel
* Esperanto - @jakubfabijan
* French - @FlorianLeChat
* German - @Jerome2103
* Hungarian - kaciokos
* Indonesian - when we were sober, @andhikapangestu29
* Norwegian Bokmål - @comradekingu
* Polish - @doegedomita
* Portuguese - @Jerome2103
* Spanish - @FlorianLeChat
* Turkish - Oğuz Ersen
* Ukrainian - @IhorHordiichuk
### 11.7 (2021-03-08)
* CalDAV: Display shared list members in list settings
* Compatible with Tasks.org, Nextcloud, ownCloud, OpenXchange, and sabre/dav
* CalDAV: List owners can remove shared list members from list
* Compatible with Tasks.org, Nextcloud, ownCloud, and sabre/dav
* Fix time zone issue in recurrence picker
* Update translations
* Arabic - @mhmdanas
* Basque - Sergio Varela
* Dutch - @fvbommel
* French - @FlorianLeChat
* Hungarian - kaciokos
* Indonesian - @putulopi
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Turkish - @emintufan, Oğuz Ersen
* Ukrainian - @IhorHordiichuk
### 11.6.1 (2021-03-11)
* F-Droid: Fix OpenStreetMap crash
### 11.6 (2021-03-04)
* CalDAV: Display indicator in drawer when a list is shared with other users
* Compatible with Tasks.org, Nextcloud, ownCloud, OpenXchange, and sabre/dav
* CalDAV: Don't upload changes to read-only lists
([#931](https://github.com/tasks/tasks/issues/931))
* Remove unnecessary icon-mirroring for RTL users
([#1385](https://github.com/tasks/tasks/issues/1385) and
[#1391](https://github.com/tasks/tasks/pull/1391)) - Thanks to @mhmdanas
* Update translations
* Arabic - @mhmdanas
* Basque - Sergio Varela
* Bulgarian - @StoyanDimitrov
* Czech - @vitSkalicky
* Dutch - @fvbommel
* French - @FlorianLeChat
* Hungarian - kaciokos
* Indonesian - @putulopi
* Russian - Nikita Epifanov
* Simplified Chinese - @sr093906
* Sinhala - HelaBasa
* Spanish - @FlorianLeChat
* Ukrainian - @IhorHordiichuk
### 11.5.2 (2021-02-25)
* Fix CalDAV sync error
* Report errors when generating recurrence dates
### 11.5.1 (2021-02-24)
* Fix 'repeat until' date
* Fix repeat dates for UTC+13
([#1374](https://github.com/tasks/tasks/issues/1374))
* F-Droid: Handle null name in Nominatim reverse geocoder
([#1380](https://github.com/tasks/tasks/issues/1380))
* Update translations
* Basque - Sergio Varela
* Croatian - @ggdorman
* Dutch - @fvbommel
* French - @FlorianLeChat
* Hungarian - kaciokos
* Norwegian Bokmål - @comradekingu
* Polish - @alex-ter
* Russian - Nikita Epifanov
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Turkish - Oğuz Ersen
* Ukrainian - @IhorHordiichuk
* Urdu - Maaz
### 11.5 (2021-02-17)
* Sync snooze time with Tasks.org, DAVx⁵, CalDAV, EteSync, and DecSync
* Compatible with Thunderbird
* New map theme preference
* 10 new icons
* F-Droid: Use Nominatim for reverse geocoding
* Google Play: Use OpenStreetMap tiles when Play Services not available
* Google Play: Use Android location services when Play Services not available
* Tasks.org accounts: Use Google Places for map search
* Update translations
* Dutch - @fvbommel
* French - @FlorianLeChat
* Hungarian - kaciokos
* Indonesian - when we were sober
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Ukrainian - @IhorHordiichuk
### 11.4 (2021-02-09)
* Sync collapsed subtask state with Tasks.org, DAVx⁵, CalDAV, EteSync, and
DecSync ([#1339](https://github.com/tasks/tasks/issues/1339))
* Compatible with Nextcloud and ownCloud
* F-Droid: Add location based reminders ([#770](https://github.com/tasks/tasks/issues/770))
* F-Droid: Replace Mapbox tiles with OpenStreetMap tiles ([#922](https://github.com/tasks/tasks/issues/922))
* Fix default start date ([#1350](https://github.com/tasks/tasks/issues/1350))
### 11.3.4 (2021-02-03)
* Adjust start times by one second during sync
([#1326](https://github.com/tasks/tasks/issues/1326))
* Can now sync start time = due time with DAVx⁵, EteSync app, and DecSync CC
* All day start date must come before all day due date with DAVx⁵, EteSync
app, and DecSync CC
* 'Show unstarted' toggled on by default
### 11.3.3 (2021-01-30)
* Fix all-day due date synchronization
([#1325](https://github.com/tasks/tasks/issues/1325))
### 11.3.2 (2021-01-28)
* Fix recurrence sync issue
([#1323](https://github.com/tasks/tasks/issues/1323))
### 11.3.1 (2021-01-27)
* Improve support for recurring tasks with subtasks
* Subtasks will be unchecked after completing a recurring task
* Clear completed will not delete subtasks of recurring tasks
* Improve widget sort header when space is limited
* Add option to hide widget title
* Fix timezone conversions during synchronization
* Add Esperanto translations - @jakubfabijan
### 11.3 (2021-01-20)
* 'Hide until' is now 'Start date'
* Synchronize start dates with Tasks.org, DAVx⁵, CalDAV, EteSync, and DecSync
* New start date picker
* New start date custom filter criteria
* Add sort 'By start date'
* Display start dates as chips
* Don't perform background sync when data saver enabled
* Preference changes
* Add app and widget preferences to disable start date chips
* Synchronization accounts displayed on main preference screen
* Removed background sync and metered connection options (now respecting data
saver mode)
* Removed Google Tasks 'Custom order synchronization fix' (automatically
performing full sync if 'My order' enabled)
* Remove support for legacy XML backup format ([more info](https://github.com/tasks/tasks/issues/1565))
* Bug fixes
### 11.2.2 (2021-01-07)
* Rename 'Lists' to 'Local lists' to clarify that they are not synchronized
* Tasks.org sign in improvements
* Miscellaneous improvements - Thanks @mhmdanas!
### 11.2.1 (2021-01-05)
* Fix Portuguese translation issue
* Report OpenTask sync errors
* Report Tasks.org sign in errors
* Don't crash on widget configuration error
* Purchase dialog changes
### 11.2 (2020-12-30)
* [Synchronize your Tasks.org account with third-party task and calendar apps, like Outlook,
Thunderbird, or Apple Reminders](https://tasks.org/passwords)
* Miscellaneous improvements - Thanks @mhmdanas!
### 11.1.1 (2020-12-24)
* Fix compatibility issues with third-party clients
* Completed tasks without completion dates
([222a34f](https://github.com/tasks/tasks/commit/222a34fc263816bb23f633bc9c79de78aeb3968d))
* Tasks with start date but no due date
([7a1d566](https://github.com/tasks/tasks/commit/7a1d566bfb613b95d3fe1df46d8fa67200c91021))
* Miscellaneous improvements - Thanks @mhmdanas!
### 11.1 (2020-12-21)
* Add [DecSync CC synchronization](https://tasks.org/decsync)
* Fix rescheduling remotely completed recurring task
([5eb9370](https://github.com/tasks/tasks/commit/5eb9370294ef707b3e667c4a42851030419920d8))
* Miscellaneous code improvements - Thanks @mhmdanas!
### 11.0.1 (2020-12-17)
* Fix EteSync client issue with v2 accounts
([b761309](https://github.com/tasks/tasks/commit/b76130902ae0be6e1d580d588798a9ed0d7ff385))
* Fix multi-select 'Pick time' crash
* Fix default hide until due time
([#842](https://github.com/tasks/tasks/issues/842#issuecomment-746358382))
* Add Croatian translations - Garden Hose
* Add Urdu translations - Maaz
### 11.0 (2020-12-10)
* New Tasks.org synchronization service
* Multi-select rescheduling
* New task default settings
* Default tags
* Default recurrence
* Default location
* Hide until due time
* New custom filter criteria
* Hidden tasks
* Completed tasks
* Subtasks
* Parent tasks
* Recurring tasks
* Added EteSync v2 support
* Deprecated EteSync v1 support
* v1 accounts cannot be added to Tasks.org
* v1 accounts can be added to the EteSync Android client
* Add ability to delete comments (Thanks to @romedius!)
* Add option to always display date (Thanks to @T0M0F!)
* Copy subtasks when copying tasks (Thanks to @supermzn!)
* Fix ring five times cutoff (Thanks to @przemhb!)
* Bug fixes
* Translation updates
* Arabic - @mhmdanas
* Basque - @osoitz, @ppasserini
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @franconian, J. Lavoie, @myabc
* Hebrew - @yarons
* Hungarian - kaciokos
* Indonesian - @andikatuluspangestu
* Italian - @ppasserini, @Fs00, @pjammo
* Korean - Sunjae Choi, @Hwaro-K
* Norwegian Bokmål - @comradekingu
* Polish - @alex-ter
* Russian - Nikita Epifanov
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Traditional Chinese - @realpineapplemilk
* Turkish - @emintufan, Oğuz Ersen
### 10.4.1 (2020-11-09)
* Fix Mapbox Maps crash on Android 11 (F-Droid only)
### 10.4 (2020-10-09)
* New widget configuration options
* Sort
* Show hidden
* Show completed
* Header spacing
* Bug fixes
### 10.3 (2020-10-02)
* Collapsible sort groups in widget
* Add 'System default' widget theme
* Bug fixes
### 10.2 (2020-09-25)
* Display list, tag, and place chips on widgets
* Add option to disable list, tag, and place chips on widgets
### 10.1 (2020-09-23)
* Android 11 support
* Backup improvements
* Swipe-to-refresh initiates DAVx5/EteSync sync
* Show indicator when DAVx5/EteSync are synchronizing
* Bug fixes
### 10.0.3 (2020-09-16)
* Fix crash from calendar event snackbar
* Fix crash when setting Google Maps markers
* Fix invalid calendar entry creation
### 10.0.2 (2020-09-14)
* Fix crash from corrupted custom filter
* Fix crash in 'Astrid manual sorting' mode
* Fix missing 'Calendar event created' snackbar
### 10.0.1 (2020-09-05)
* Bug fixes
* Translation updates
* Czech - @vitSkalicky
* Danish - @ChMunk
### 10.0 (2020-08-31)
* PRO: DAVx⁵ support (requires [DAVx⁵ beta](https://tasks.org/davx5))
* PRO: EteSync client support
* [ToDo Agenda](https://play.google.com/store/apps/details?id=org.andstatus.todoagenda) integration
* Changed backstack behavior to follow Android conventions
* Major internal changes! Please report any bugs!
* Remove Mapbox tiles (Google Play only)
* Added 'Astrid manual sort' information to backup file
* Bug fixes
* Performance improvements
* Security improvements
[Older releases](https://github.com/tasks/tasks/blob/main/V06_09_CHANGELOG.md)

@ -1,32 +1,42 @@
@file:Suppress("UnstableApiUsage")
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
id("com.android.application")
id("checkstyle")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
kotlin("android")
kotlin("kapt")
id("com.cookpad.android.plugin.license-tools") version "1.2.8"
id("com.github.ben-manes.versions") version "0.39.0"
id("com.vanniktech.android.junit.jacoco") version "0.16.0"
id("dagger.hilt.android.plugin")
id("com.google.android.gms.oss-licenses-plugin")
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose.compiler)
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
repositories {
mavenCentral()
google()
maven {
url = uri("https://jitpack.io")
content {
includeGroup("com.gitlab.abaker")
includeModule("com.gitlab.bitfireAT", "cert4android")
includeModule("com.github.tasks.opentasks", "opentasks-provider")
includeModule("com.github.QuadFlask", "colorpicker")
}
}
jcenter {
content {
includeModule("com.twofortyfouram", "android-plugin-api-for-locale")
}
}
}
composeCompiler {
enableStrongSkippingMode = true
}
android {
val commonTest = "src/commonTest/java"
sourceSets["test"].java.srcDir(commonTest)
sourceSets["androidTest"].java.srcDirs("src/androidTest/java", commonTest)
bundle {
language {
enableSplit = false
@ -37,25 +47,32 @@ android {
viewBinding = true
dataBinding = true
compose = true
buildConfig = true
}
lint {
disable("InvalidPeriodicWorkRequestInterval")
lintConfig = file("lint.xml")
textOutput = File("stdout")
textOutput("stdout")
textReport = true
}
compileSdk = libs.versions.android.compileSdk.get().toInt()
compileSdk = Versions.compileSdk
defaultConfig {
testApplicationId = "org.tasks.test"
applicationId = "org.tasks"
versionCode = 131100
versionName = "13.11"
targetSdk = libs.versions.android.targetSdk.get().toInt()
minSdk = libs.versions.android.minSdk.get().toInt()
versionCode = 111101
versionName = "11.11"
targetSdk = Versions.targetSdk
minSdk = Versions.minSdk
testInstrumentationRunner = "org.tasks.TestRunner"
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
}
}
}
signingConfigs {
@ -74,15 +91,21 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.compose
}
flavorDimensions += listOf("store")
kotlinOptions {
jvmTarget = "1.8"
}
@Suppress("LocalVariableName")
buildTypes {
debug {
getByName("debug") {
configure<CrashlyticsExtension> {
mappingFileUploadEnabled = false
}
@ -94,9 +117,9 @@ android {
resValue("string", "tasks_caldav_url", tasks_caldav_url ?: "https://caldav.tasks.org")
resValue("string", "tasks_nominatim_url", tasks_caldav_url ?: "https://nominatim.tasks.org")
resValue("string", "tasks_places_url", tasks_caldav_url ?: "https://places.tasks.org")
enableUnitTestCoverage = project.hasProperty("coverage")
isTestCoverageEnabled = project.hasProperty("coverage")
}
release {
getByName("release") {
val tasks_mapbox_key: String? by project
val tasks_google_key: String? by project
resValue("string", "mapbox_key", tasks_mapbox_key ?: "")
@ -107,34 +130,25 @@ android {
}
}
flavorDimensions("store")
productFlavors {
create("generic") {
dimension = "store"
}
create("googleplay") {
isDefault = true
dimension = "store"
}
}
packaging {
resources {
excludes += setOf("META-INF/*.kotlin_module", "META-INF/INDEX.LIST")
}
}
testOptions {
managedDevices {
localDevices {
create("pixel2api30") {
device = "Pixel 2"
apiLevel = 30
systemImageSource = "aosp-atd"
}
}
}
packagingOptions {
exclude("META-INF/*.kotlin_module")
}
}
namespace = "org.tasks"
configure<CheckstyleExtension> {
configFile = project.file("google_checks.xml")
toolVersion = "8.16"
}
configurations.all {
@ -151,130 +165,101 @@ val genericImplementation by configurations
val googleplayImplementation by configurations
dependencies {
implementation(projects.data)
implementation(projects.kmp)
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bitfire.dav4jvm) {
exclude(group = "junit")
exclude(group = "org.ogce", module = "xpp3")
}
implementation(libs.bitfire.ical4android) {
exclude(group = "commons-logging")
exclude(group = "org.json", module = "json")
exclude(group = "org.codehaus.groovy", module = "groovy")
exclude(group = "org.codehaus.groovy", module = "groovy-dateutil")
}
implementation(libs.bitfire.cert4android)
implementation(libs.dmfs.opentasks.provider) {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation("com.gitlab.abaker:dav4jvm:deb2c9aef8")
implementation("com.gitlab.abaker:ical4android:0e928b567c")
implementation("com.gitlab.bitfireAT:cert4android:26a91a729f")
implementation("com.github.tasks.opentasks:opentasks-provider:a1faa1b") {
exclude("com.github.tasks.opentasks", "opentasks-contract")
}
implementation(libs.dmfs.rfc5545.datetime)
implementation(libs.dmfs.recur)
implementation(libs.dmfs.jems)
implementation(libs.dagger.hilt)
ksp(libs.dagger.hilt.compiler)
ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.work)
implementation("com.google.dagger:hilt-android:${Versions.hilt}")
kapt("com.google.dagger:hilt-compiler:${Versions.hilt}")
kapt("androidx.hilt:hilt-compiler:${Versions.hilt_androidx}")
implementation("androidx.hilt:hilt-work:${Versions.hilt_androidx}")
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.room)
implementation(libs.androidx.appcompat)
implementation(libs.iconics)
implementation(libs.iconics.material.design)
implementation(libs.markwon)
implementation(libs.markwon.editor)
implementation(libs.markwon.linkify)
implementation(libs.markwon.strikethrough)
implementation(libs.markwon.tables)
implementation(libs.markwon.tasklist)
implementation("androidx.fragment:fragment-ktx:1.3.6")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}")
implementation("androidx.room:room-ktx:${Versions.room}")
kapt("org.xerial:sqlite-jdbc:3.34.0")
kapt("androidx.room:room-compiler:${Versions.room}")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
implementation("androidx.paging:paging-runtime:2.1.2")
implementation("io.noties.markwon:core:${Versions.markwon}")
implementation("io.noties.markwon:editor:${Versions.markwon}")
implementation("io.noties.markwon:ext-tasklist:${Versions.markwon}")
implementation("io.noties.markwon:ext-strikethrough:${Versions.markwon}")
implementation("io.noties.markwon:ext-tables:${Versions.markwon}")
implementation("io.noties.markwon:linkify:${Versions.markwon}")
debugImplementation(libs.facebook.flipper)
debugImplementation(libs.facebook.flipper.network)
debugImplementation(libs.facebook.soloader)
debugImplementation(libs.leakcanary)
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation(libs.kotlin.reflect)
debugImplementation("com.facebook.flipper:flipper:${Versions.flipper}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${Versions.flipper}")
debugImplementation("com.facebook.soloader:soloader:0.10.1")
debugImplementation("com.squareup.leakcanary:leakcanary-android:${Versions.leakcanary}")
debugImplementation("androidx.compose.ui:ui-tooling:${Versions.compose}")
debugImplementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}")
implementation(libs.kotlin.jdk8)
implementation(libs.kotlinx.immutable)
implementation(libs.kotlinx.serialization)
implementation(libs.okhttp)
implementation(libs.persistent.cookiejar)
implementation(libs.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.preference)
implementation(libs.timber)
implementation(libs.dashclock.api)
implementation(libs.locale) {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.4")
implementation("com.squareup.okhttp3:okhttp:${Versions.okhttp}")
implementation("com.google.code.gson:gson:2.8.8")
implementation("com.google.android.material:material:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.preference:preference:1.1.1")
implementation("com.jakewharton.timber:timber:5.0.1")
implementation("com.google.android.apps.dashclock:dashclock-api:2.0.0")
implementation("com.twofortyfouram:android-plugin-api-for-locale:1.0.2") {
isTransitive = false
}
implementation(libs.jchronic) {
implementation("com.rubiconproject.oss:jchronic:0.2.6") {
isTransitive = false
}
implementation(libs.shortcut.badger)
implementation(libs.google.api.tasks)
implementation(libs.google.api.drive)
implementation(libs.google.oauth2)
implementation(libs.androidx.work)
implementation(libs.etebase)
implementation(libs.colorpicker)
implementation(libs.appauth)
implementation(libs.osmdroid)
implementation(libs.retrofit)
implementation(libs.retrofit.moshi)
implementation(libs.androidx.recyclerview)
implementation(platform(libs.androidx.compose))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material")
implementation("androidx.compose.runtime:runtime-livedata")
implementation(libs.androidx.activity.compose)
implementation("androidx.compose.material:material-icons-extended")
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation("androidx.compose.ui:ui-viewbinding")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation(libs.coil.compose)
implementation(libs.coil.video)
implementation(libs.coil.svg)
implementation(libs.coil.gif)
implementation("me.leolin:ShortcutBadger:1.1.22@aar")
implementation("com.google.apis:google-api-services-tasks:v1-rev20210709-1.32.1")
implementation("com.google.apis:google-api-services-drive:v3-rev20210725-1.32.1")
implementation("com.google.auth:google-auth-library-oauth2-http:0.26.0")
implementation("androidx.work:work-runtime:${Versions.work}")
implementation("androidx.work:work-runtime-ktx:${Versions.work}")
implementation("com.etesync:journalmanager:1.1.1")
implementation("com.etebase:client:2.3.2")
implementation("com.github.QuadFlask:colorpicker:0.0.15")
implementation("net.openid:appauth:0.8.1")
implementation("org.osmdroid:osmdroid-android:6.1.11@aar")
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.systemuicontroller)
implementation("androidx.compose.ui:ui:${Versions.compose}")
implementation("androidx.compose.foundation:foundation:${Versions.compose}")
implementation("androidx.compose.material:material:${Versions.compose}")
implementation("androidx.compose.runtime:runtime-livedata:${Versions.compose}")
implementation("com.google.android.material:compose-theme-adapter:${Versions.compose}")
implementation("androidx.activity:activity-compose:1.3.1")
releaseCompileOnly("androidx.compose.ui:ui-tooling:${Versions.compose}")
googleplayImplementation(platform(libs.firebase))
googleplayImplementation("com.google.firebase:firebase-crashlytics")
googleplayImplementation("com.google.firebase:firebase-analytics") {
googleplayImplementation("com.google.firebase:firebase-crashlytics:${Versions.crashlytics}")
googleplayImplementation("com.google.firebase:firebase-analytics:${Versions.analytics}") {
exclude("com.google.android.gms", "play-services-ads-identifier")
}
googleplayImplementation("com.google.firebase:firebase-config-ktx")
googleplayImplementation(libs.play.services.location)
googleplayImplementation(libs.play.services.maps)
googleplayImplementation(libs.play.billing.ktx)
googleplayImplementation(libs.play.review)
googleplayImplementation(libs.play.services.oss.licenses)
googleplayImplementation("com.google.firebase:firebase-config-ktx:${Versions.remote_config}")
googleplayImplementation("com.google.android.gms:play-services-location:18.0.0")
googleplayImplementation("com.google.android.gms:play-services-maps:17.0.1")
googleplayImplementation("com.android.billingclient:billing-ktx:3.0.3")
androidTestImplementation(libs.dagger.hilt.testing)
kspAndroidTest(libs.dagger.hilt.compiler)
kspAndroidTest(libs.androidx.hilt.compiler)
androidTestImplementation(libs.mockito.android)
androidTestImplementation(libs.make.it.easy)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.okhttp.mockwebserver)
androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.hilt}")
kaptAndroidTest("com.google.dagger:hilt-compiler:${Versions.hilt}")
kaptAndroidTest("androidx.hilt:hilt-compiler:${Versions.hilt_androidx}")
androidTestImplementation("org.mockito:mockito-android:${Versions.mockito}")
androidTestImplementation("com.natpryce:make-it-easy:${Versions.make_it_easy}")
androidTestImplementation("androidx.test:runner:${Versions.androidx_test}")
androidTestImplementation("androidx.test:rules:${Versions.androidx_test}")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("com.squareup.okhttp3:mockwebserver:${Versions.okhttp}")
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.make.it.easy)
testImplementation(libs.androidx.test.core)
testImplementation(libs.mockito.core)
testImplementation(libs.xpp3)
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2")
testImplementation("com.natpryce:make-it-easy:${Versions.make_it_easy}")
testImplementation("androidx.test:core:${Versions.androidx_test}")
testImplementation("org.mockito:mockito-core:${Versions.mockito}")
testImplementation("org.ogce:xpp3:1.1.6")
}

@ -0,0 +1,263 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!-- https://raw.githubusercontent.com/checkstyle/checkstyle/checkstyle-8.16/src/main/resources/google_checks.xml -->
<!--
Checkstyle configuration that checks the Google coding conventions from Google Java Style
that can be found at https://google.github.io/styleguide/javaguide.html.
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.sf.net (or in your downloaded distribution).
To completely disable a check, just comment it out or delete it from the file.
Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
-->
<module name = "Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.sf.net/config_whitespace.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="LineLength">
<property name="max" value="100"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="NeedBraces"/>
<module name="LeftCurly"/>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="tokens" value="VARIABLE_DEF"/>
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="0"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
</module>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
</module>
<module name="MethodParamPad"/>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad"/>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="JavadocTagContinuationIndentation">
<property name="severity" value="ignore" />
</module>
<module name="SummaryJavadoc">
<property name="severity" value="ignore" />
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<module name="JavadocParagraph">
<property name="severity" value="ignore" />
</module>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="severity" value="ignore" />
<property name="scope" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingThrowsTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="minLineCount" value="2"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="allowThrowsTagsForSubclasses" value="true"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SingleLineJavadoc">
<property name="severity" value="ignore"/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation"/>
</module>
</module>

@ -0,0 +1,980 @@
- artifact: com.gitlab.abaker:dav4jvm:+
name: dav4jvm
copyrightHolder: bitfire web engineering (Ricki Hirner, Bernhard Stockmann)
license: Mozilla Public License, Version 2.0
licenseUrl: https://www.mozilla.org/en-US/MPL/2.0/
- artifact: com.gitlab.abaker:ical4android:+
name: ical4android
copyrightHolder: bitfire web engineering (Ricki Hirner, Bernhard Stockmann)
license: GNU General Public License, Version 3.0
licenseUrl: https://www.gnu.org/licenses/gpl.txt
- artifact: com.gitlab.bitfireAT:cert4android:+
name: cert4android
copyrightHolder: bitfire web engineering (Ricki Hirner, Bernhard Stockmann)
licenseUrl: https://www.gnu.org/licenses/gpl.txt
license: GNU General Public License, Version 3.0
- artifact: androidx.coordinatorlayout:coordinatorlayout:+
name: Android Support Library Coordinator Layout
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.constraintlayout:constraintlayout:+
name: Android ConstraintLayout
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://tools.android.com
- artifact: androidx.sqlite:sqlite:+
name: Android DB
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.google.apis:google-api-services-drive:+
name: Drive API v3-rev136-1.25.0
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.fragment:fragment:+
name: Android Support Library fragment
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.vectordrawable:vectordrawable-animated:+
name: Android Support AnimatedVectorDrawable
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.core:core:+
name: Android Support Library compat
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.arch.core:core-common:+
name: Android Arch-Common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.room:room-common:+
name: Android Room-Common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.room:room-runtime:+
name: Android Room-Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.google.code.gson:gson:+
name: Gson
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: me.leolin:ShortcutBadger:+
name: ShortcutBadger
copyrightHolder: Leo Lin
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/leolin310148/ShortcutBadger
- artifact: androidx.lifecycle:lifecycle-runtime:+
name: Android Lifecycle Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.versionedparcelable:versionedparcelable:+
name: VersionedParcelable and friends
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.viewpager:viewpager:+
name: Android Support Library View Pager
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.lifecycle:lifecycle-livedata:+
name: Android Lifecycle LiveData
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: commons-codec:commons-codec:+
name: Apache Commons Codec
copyrightHolder: The Apache Software Foundation
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://commons.apache.org/proper/commons-codec/
- artifact: androidx.annotation:annotation:+
name: Android Support Library Annotations
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.interpolator:interpolator:+
name: Android Support Library Interpolators
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: javax.inject:javax.inject:+
name: javax.inject
copyrightHolder: The JSR-330 Expert Group
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://code.google.com/p/atinject/
- artifact: com.twofortyfouram:android-plugin-api-for-locale:+
name: android-plugin-api-for-locale
copyrightHolder: two forty four a.m. LLC.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.lifecycle:lifecycle-viewmodel:+
name: Android Lifecycle ViewModel
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.google.dagger:dagger:+
name: Dagger
copyrightHolder: The Dagger Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/google/dagger
- artifact: com.google.guava:guava:+
name: Guava Google Core Libraries for Java
copyrightHolder: The Guava Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.jetbrains:annotations:+
name: JetBrains Java Annotations
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/JetBrains/java-annotations
- artifact: org.apache.commons:commons-lang3:+
name: Apache Commons Lang
copyrightHolder: The Apache Software Foundation
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://commons.apache.org/proper/commons-lang/
- artifact: androidx.loader:loader:+
name: Android Support Library loader
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.cursoradapter:cursoradapter:+
name: Android Support Library Cursor Adapter
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.lifecycle:lifecycle-livedata-core:+
name: Android Lifecycle LiveData Core
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.customview:customview:+
name: Android Support Library Custom View
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: io.reactivex.rxjava2:rxandroid:+
name: RxAndroid
copyrightHolder: The RxAndroid authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/ReactiveX/RxAndroid
- artifact: androidx.swiperefreshlayout:swiperefreshlayout:+
name: Android Support Library Custom View
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.lifecycle:lifecycle-extensions:+
name: Android Lifecycle Extensions
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.arch.core:core-runtime:+
name: Android Arch-Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: org.apache.commons:commons-collections4:+
name: Apache Commons Collections
copyrightHolder: The Apache Software Foundation
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://commons.apache.org/proper/commons-collections/
- artifact: org.mnode.ical4j:ical4j:+
name: ical4j
copyrightHolder: Ben Fortuna
license: BSD 3-Clause
licenseUrl: https://opensource.org/licenses/BSD-3-Clause
url: http://ical4j.github.io
forceGenerate: true
- artifact: androidx.recyclerview:recyclerview:+
name: Android Support RecyclerView v7
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.collection:collection:+
name: Android Support Library collections
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.cardview:cardview:+
name: Android Support CardView v7
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.rubiconproject.oss:jchronic:+
name: jchronic
copyrightHolder: The jchronic authors
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: http://github.com/samtingleff/jchronic
- artifact: androidx.sqlite:sqlite-framework:+
name: Android Support SQLite - Framework Implementation
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.google.android.material:material:+
name: Material Components for Android
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.google.android.apps.dashclock:dashclock-api:+
name: DashClock API
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://dashclock.com/api
- artifact: androidx.vectordrawable:vectordrawable:+
name: Android Support VectorDrawable
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: org.reactivestreams:reactive-streams:+
name: reactive-streams
copyrightHolder: Public domain
license: CC0
licenseUrl: http://creativecommons.org/publicdomain/zero/1.0/
url: http://www.reactive-streams.org/
- artifact: androidx.work:work-runtime:+
name: Android WorkManager Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.appcompat:appcompat:+
name: Android AppCompat Library v7
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.lifecycle:lifecycle-common:+
name: Android Lifecycle-Common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.lifecycle:lifecycle-process:+
name: Android Lifecycle Process
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.lifecycle:lifecycle-service:+
name: Android Lifecycle Service
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.transition:transition:+
name: Android Transition Support Library
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.jakewharton.timber:timber:+
name: Timber
copyrightHolder: Jake Wharton
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/JakeWharton/timber
- artifact: com.google.oauth-client:google-oauth-client:+
name: Google OAuth Client Library for Java
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.drawerlayout:drawerlayout:+
name: Android Support Library Drawer Layout
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: io.reactivex.rxjava2:rxjava:+
name: RxJava
copyrightHolder: RxJava Contributors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/ReactiveX/RxJava
- artifact: com.google.apis:google-api-services-tasks:+
name: Tasks API v1-rev55-1.25.0
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: com.google.api-client:google-api-client:+
name: Google APIs Client Library for Java
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.jetbrains.kotlin:kotlin-stdlib:+
name: org.jetbrains.kotlin:kotlin-stdlib
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://kotlinlang.org/
- artifact: com.google.http-client:google-http-client:+
name: Google HTTP Client Library for Java
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.slf4j:slf4j-jdk14:+
name: SLF4J JDK14 Binding
copyrightHolder: QOS.ch
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: http://www.slf4j.org
- artifact: com.squareup.okhttp3:okhttp:+
name: OkHttp
copyrightHolder: Square, Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.slf4j:slf4j-api:+
name: SLF4J API Module
copyrightHolder: QOS.ch
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: http://www.slf4j.org
- artifact: org.jetbrains.kotlin:kotlin-stdlib-common:+
name: org.jetbrains.kotlin:kotlin-stdlib-common
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://kotlinlang.org/
- artifact: com.squareup.okhttp3:logging-interceptor:+
name: OkHttp Logging Interceptor
copyrightHolder: Square, Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.jetbrains.kotlin:kotlin-stdlib-jdk7:+
name: org.jetbrains.kotlin:kotlin-stdlib-jdk7
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://kotlinlang.org/
- artifact: io.grpc:grpc-context:+
name: io.grpc:grpc-context
copyrightHolder: The gRPC Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/grpc/grpc-java
- artifact: com.google.guava:listenablefuture:+
name: Guava ListenableFuture only
copyrightHolder: The Guava Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: io.opencensus:opencensus-api:+
name: OpenCensus API
copyrightHolder: OpenCensus Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/census-instrumentation/opencensus-java
- artifact: com.google.guava:failureaccess:+
name: Guava InternalFutureFailureAccess and InternalFutures
copyrightHolder: The Guava Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: io.opencensus:opencensus-contrib-http-util:+
name: OpenCensus contrib-http-util
copyrightHolder: OpenCensus Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/census-instrumentation/opencensus-java
- artifact: androidx.core:core-ktx:+
name: Core Kotlin Extensions
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.appcompat:appcompat-resources:+
name: Android Resources Library
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.viewpager2:viewpager2:+
name: AndroidX Widget ViewPager2
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.savedstate:savedstate:+
name: Activity
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.activity:activity:+
name: Activity
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.paging:paging-runtime:+
name: Android Paging-Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.paging:paging-common:+
name: Android Paging-Common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: org.conscrypt:conscrypt-android:+
name: org.conscrypt:conscrypt-android
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: https://www.apache.org/licenses/LICENSE-2.0
url: https://conscrypt.org/
- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-android:+
name: kotlinx-coroutines-android
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/Kotlin/kotlinx.coroutines
- artifact: androidx.databinding:databinding-adapters:+
name: databinding-adapters
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.lifecycle:lifecycle-viewmodel-ktx:+
name: Android Lifecycle ViewModel Kotlin Extensions
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.annotation:annotation-experimental:+
name: Experimental annotation
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:+
name: kotlinx-coroutines-core-jvm
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/Kotlin/kotlinx.coroutines
- artifact: androidx.databinding:databinding-common:+
name: Data Binding Base Library
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/studio
- artifact: androidx.databinding:databinding-runtime:+
name: databinding-runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.databinding:viewbinding:+
name: viewbinding
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: com.madgag.spongycastle:core:+
name: Spongy Castle Core
copyrightHolder: The Legion of the Bouncy Castle Inc.
license: Bouncy Castle Licence
licenseUrl: http://www.bouncycastle.org/licence.html
url: http://rtyley.github.io/spongycastle/
- artifact: com.etesync:journalmanager:+
name: EteSync JVM
copyrightHolder: Tom Hacohen
license: LGPL-3.0-only
licenseUrl: https://spdx.org/licenses/LGPL-3.0-only.html
url: https://www.etesync.com
- artifact: com.madgag.spongycastle:prov:+
name: Spongy Castle
copyrightHolder: The Legion of the Bouncy Castle Inc.
license: Bouncy Castle Licence
licenseUrl: http://www.bouncycastle.org/licence.html
url: http://rtyley.github.io/spongycastle/
- artifact: androidx.preference:preference:+
name: AndroidX Preference
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: androidx.lifecycle:lifecycle-viewmodel-savedstate:+
name: Android Lifecycle ViewModel with SavedState
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.github.QuadFlask:colorpicker:+
name: QuadFlask/colorpicker
copyrightHolder: QuadFlask
license: The Apache Software License, Version 2.0
url: https://github.com/QuadFlask/colorpicker
- artifact: com.google.auth:google-auth-library-credentials:+
name: Google Auth Library for Java - Credentials
copyrightHolder: Google Inc.
license: BSD 3-Clause
- artifact: com.google.auth:google-auth-library-oauth2-http:+
name: Google Auth Library for Java - OAuth2 HTTP
copyrightHolder: Google Inc.
license: BSD 3-Clause
- artifact: com.google.auto.value:auto-value-annotations:+
name: AutoValue Annotations
copyrightHolder: Google LLC
license: The Apache Software License, Version 2.0
url: https://github.com/google/auto/tree/master/value
- artifact: com.sun.mail:android-mail:+
name: android-mail
copyrightHolder: Oracle and/or its affiliates
license: Eclipse Public License, Version 2.0
- artifact: commons-io:commons-io:+
name: commons-io
copyrightHolder: The Apache Software Foundation
license: The Apache Software License, Version 2.0
url: http://commons.apache.org/proper/commons-io/
- artifact: com.sun.mail:android-activation:+
name: android-activation
copyrightHolder: Oracle and/or its affiliates
license: Eclipse Public License, Version 2.0
- artifact: androidx.hilt:hilt-work:+
name: hilt-work
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.hilt:hilt-common:+
name: hilt-common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.google.dagger:hilt-android:+
name: hilt-android
copyrightHolder: The Dagger Authors
license: The Apache Software License, Version 2.0
licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/google/dagger
- artifact: com.google.dagger:dagger-lint-aar:+
name: dagger-lint-aar
copyrightHolder: The Dagger Authors
license: The Apache Software License, Version 2.0
licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/google/dagger
- artifact: androidx.room:room-ktx:+
name: room-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.lifecycle:lifecycle-runtime-ktx:+
name: lifecycle-runtime-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: androidx.fragment:fragment-ktx:+
name: fragment-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: androidx.activity:activity-ktx:+
name: activity-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: androidx.collection:collection-ktx:+
name: collection-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.lifecycle:lifecycle-livedata-core-ktx:+
name: lifecycle-livedata-core-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: androidx.work:work-runtime-ktx:+
name: work-runtime-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.github.tasks.opentasks:opentasks-provider:+
name: opentasks-provider
copyrightHolder: dmfs GmbH
license: The Apache Software License, Version 2.0
licenseUrl: https://api.github.com/licenses/apache-2.0
url: https://github.com/dmfs/opentasks
- artifact: com.github.dmfs.opentasks:opentasks-contract:+
name: opentasks-contract
copyrightHolder: dmfs GmbH
license: The Apache Software License, Version 2.0
licenseUrl: https://api.github.com/licenses/apache-2.0
url: https://github.com/dmfs/opentasks
forceGenerate: true
- artifact: org.dmfs:lib-recur:+
name: lib-recur
copyrightHolder: Marten Gajda
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt
url: https://github.com/dmfs/lib-recur
- artifact: org.dmfs:rfc5545-datetime:+
name: rfc5545-datetime
copyrightHolder: Marten Gajda
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/dmfs/rfc5545-datetime
- artifact: org.dmfs:jems:+
name: jems
copyrightHolder: dmfs GmbH
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/license/LICENSE-2.0.txt
url: https://github.com/dmfs/jems
- artifact: org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:+
name: kotlinx-collections-immutable-jvm
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
- artifact: com.squareup.okio:okio:+
name: okio
copyrightHolder: Square, Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/square/okio/
- artifact: com.google.dagger:hilt-core:+
name: hilt-core
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: https://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/google/dagger
- artifact: io.noties.markwon:core:+
name: core
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: com.atlassian.commonmark:commonmark:+
name: commonmark
copyrightHolder: Atlassian and others
license: BSD 2-Clause
- artifact: androidx.browser:browser:+
name: browser
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.etebase:client:+
name: client
copyrightHolder: Tom Hacohen
license: LGPL-3.0-only
licenseUrl: https://spdx.org/licenses/LGPL-3.0-only.html
url: https://www.etebase.com
- artifact: net.openid:appauth:+
name: appauth
copyrightHolder: The AppAuth for Android Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/openid/AppAuth-Android
- artifact: androidx.concurrent:concurrent-futures:+
name: concurrent-futures
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.lifecycle:lifecycle-livedata-ktx:+
name: lifecycle-livedata-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: androidx.dynamicanimation:dynamicanimation:+
name: dynamicanimation
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.legacy:legacy-support-core-utils:+
name: legacy-support-core-utils
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.documentfile:documentfile:+
name: documentfile
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.localbroadcastmanager:localbroadcastmanager:+
name: localbroadcastmanager
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.print:print:+
name: print
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.google.http-client:google-http-client-gson:+
name: google-http-client-gson
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
- artifact: org.osmdroid:osmdroid-android:+
name: osmdroid-android
copyrightHolder: The OsmDroid Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0
url: https://github.com/osmdroid/osmdroid
- artifact: androidx.savedstate:savedstate-ktx:+
name: savedstate-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/savedstate#1.1.0
- artifact: androidx.tracing:tracing:+
name: tracing
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/tracing#1.0.0
- artifact: androidx.databinding:databinding-ktx:+
name: databinding-ktx
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.jetbrains.kotlin:kotlin-stdlib-jdk8:+
name: kotlin-stdlib-jdk8
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://kotlinlang.org/
- artifact: androidx.compose.ui:ui:+
name: ui
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-ui#1.0.0-beta01
- artifact: androidx.compose.animation:animation-core:+
name: animation-core
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-animation#1.0.0-beta01
- artifact: androidx.compose.ui:ui-text:+
name: ui-text
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-ui#1.0.0-beta01
- artifact: androidx.compose.runtime:runtime-saveable:+
name: runtime-saveable
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.0.0-beta01
- artifact: androidx.compose.ui:ui-graphics:+
name: ui-graphics
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-ui#1.0.0-beta01
- artifact: androidx.compose.ui:ui-unit:+
name: ui-unit
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-ui#1.0.0-beta01
- artifact: androidx.compose.ui:ui-geometry:+
name: ui-geometry
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-ui#1.0.0-beta01
- artifact: androidx.compose.ui:ui-util:+
name: ui-util
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-ui#1.0.0-beta01
- artifact: androidx.compose.runtime:runtime:+
name: runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.0.0-beta01
- artifact: androidx.autofill:autofill:+
name: autofill
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: androidx.compose.foundation:foundation:+
name: foundation
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.0.0-beta01
- artifact: androidx.compose.animation:animation:+
name: animation
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-animation#1.0.0-beta01
- artifact: androidx.compose.foundation:foundation-layout:+
name: foundation-layout
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-foundation#1.0.0-beta01
- artifact: androidx.compose.material:material:+
name: material
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-material#1.0.0-beta01
- artifact: androidx.compose.material:material-ripple:+
name: material-ripple
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-material#1.0.0-beta01
- artifact: androidx.compose.material:material-icons-core:+
name: material-icons-core
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-material#1.0.0-beta01
- artifact: androidx.compose.ui:ui-tooling:+
skip: true
- artifact: androidx.compose.ui:ui-tooling-data:+
skip: true
- artifact: androidx.compose.runtime:runtime-livedata:+
name: runtime-livedata
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-runtime#1.0.0-beta01
- artifact: com.google.android.material:compose-theme-adapter:+
name: compose-theme-adapter
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/material-components/material-components-android-compose-theme-adapter/
- artifact: androidx.constraintlayout:constraintlayout-core:+
name: constraintlayout-core
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://tools.android.com
- artifact: androidx.activity:activity-compose:+
name: activity-compose
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/activity#1.3.0-alpha04
- artifact: io.noties.markwon:editor:+
name: editor
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: io.noties.markwon:ext-tasklist:+
name: ext-tasklist
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: io.noties.markwon:ext-strikethrough:+
name: ext-strikethrough
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:+
name: commonmark-ext-gfm-strikethrough
copyrightHolder: Atlassian and others
license: BSD 2-Clause
- artifact: io.noties.markwon:linkify:+
name: linkify
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: io.noties.markwon:ext-tables:+
name: ext-tables
copyrightHolder: Dimitry Ivanov
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/noties/Markwon
- artifact: com.atlassian.commonmark:commonmark-ext-gfm-tables:+
name: commonmark-ext-gfm-tables
copyrightHolder: Atlassian and others
license: BSD 2-Clause
- artifact: androidx.lifecycle:lifecycle-common-java8:+
name: lifecycle-common-java8
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/lifecycle#2.3.0
- artifact: androidx.profileinstaller:profileinstaller:+
name: profileinstaller
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/profileinstaller#1.0.0-beta01
- artifact: androidx.startup:startup-runtime:+
name: startup-runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/startup#1.0.0
- artifact: androidx.compose.ui:ui-tooling-preview:+
name: ui-tooling-preview
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/compose-ui#1.0.0-rc01

15
app/proguard.pro vendored

@ -16,7 +16,9 @@
-dontwarn javax.inject.**
-dontwarn com.google.j2objc.annotations.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn com.google.errorprone.annotations.**
-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
-dontwarn com.google.errorprone.annotations.concurrent.LazyInit
-dontwarn com.google.errorprone.annotations.ForOverride
# https://github.com/square/okhttp/blob/0b74bba08805c28f6aede626cf06f213ef6480f2/README.md
-dontwarn okhttp3.**
@ -47,13 +49,4 @@
-dontnote java.nio.file.Files, java.nio.file.Path
-dontnote **.ILicensingService
-dontnote sun.misc.Unsafe
-dontwarn sun.misc.Unsafe
# errors from upgrading to AGP 8
-dontwarn java.beans.Transient
-dontwarn org.joda.convert.FromString
-dontwarn org.joda.convert.ToString
-dontwarn org.json.JSONString
# material icons
-keep class androidx.compose.material.icons.outlined.**
-dontwarn sun.misc.Unsafe

@ -17,7 +17,7 @@ import org.tasks.Freeze.Companion.freezeAt
import org.tasks.date.DateTimeUtils
import org.tasks.time.DateTime
import java.time.format.FormatStyle
import java.util.Locale
import java.util.*
@RunWith(AndroidJUnit4::class)
class DateUtilitiesTest {
@ -29,21 +29,21 @@ class DateUtilitiesTest {
@Test
fun testGet24HourTime() {
DateUtilities.is24HourOverride = true
assertEquals("09:05", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 9, 5, 36)))
assertEquals("13:00", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 13, 0, 1)))
assertEquals("09:05", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 9, 5, 36)))
assertEquals("13:00", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 13, 0, 1)))
}
@Test
fun testGetTime() {
DateUtilities.is24HourOverride = false
assertEquals("9:05 AM", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 9, 5, 36)))
assertEquals("1:05 PM", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 13, 5, 36)))
assertEquals("9:05 AM", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 9, 5, 36)))
assertEquals("1:05 PM", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 13, 5, 36)))
}
@Test
fun testGetTimeWithNoMinutes() {
DateUtilities.is24HourOverride = false
assertEquals("1 PM", DateUtilities.getTimeString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 13, 0, 59))) // derp?
assertEquals("1 PM", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 13, 0, 59))) // derp?
}
@Test
@ -451,6 +451,5 @@ class DateUtilitiesTest {
}
}
private fun assertMatches(regex: String, actual: String) =
assertTrue("expected=$regex\nactual=$actual", actual.matches(Regex(regex)))
private fun assertMatches(regex: String, actual: String) = assertTrue(actual.matches(Regex(regex)))
}

@ -2,10 +2,9 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import org.tasks.filters.CaldavFilter
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
@ -15,9 +14,9 @@ import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.injection.InjectingTestCase
@ -41,11 +40,10 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: CaldavManualSortTaskAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = CaldavFilter(CaldavCalendar(name = "calendar", uuid = "1234"))
private val filter = CaldavFilter(CaldavCalendar("calendar", "1234"))
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
@ -58,7 +56,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true)
tasks.clear()
adapter = CaldavManualSortTaskAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
adapter = CaldavManualSortTaskAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource)
}
@ -218,7 +216,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
}
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking {
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) })
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter, it) })
val adjustedTo = if (from < to) to + 1 else to // match DragAndDropRecyclerAdapter behavior
adapter.moved(from, adjustedTo, indent)
}
@ -226,7 +224,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
private fun checkOrder(dateTime: DateTime, index: Int) = checkOrder(dateTime.toAppleEpoch(), index)
private fun checkOrder(order: Long?, index: Int) = runBlocking {
val sortOrder = taskDao.fetch(adapter.getTask(index).id)!!.order
val sortOrder = caldavDao.getTask(adapter.getTask(index).id)!!.order
if (order == null) {
assertNull(sortOrder)
} else {

@ -3,7 +3,6 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
@ -12,9 +11,6 @@ import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.*
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskContainerMaker.PARENT
@ -28,7 +24,6 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>()
@ -38,7 +33,7 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
super.setUp()
tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
@ -199,18 +194,22 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
private fun addTask(vararg properties: PropertyValue<in TaskContainer?, *>) = runBlocking {
val t = newTaskContainer(*properties)
tasks.add(t)
val task = t.task
taskDao.createNew(task)
val caldavTask = CaldavTask(task = t.id, calendar = "calendar")
val caldavTask = CaldavTask(t.id, "calendar")
if (task.parent > 0) {
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent)
}
tasks.add(
t.copy(
caldavTask = caldavTask.copy(
id = caldavDao.insert(caldavTask)
)
)
)
}
}
caldavTask.id = caldavDao.insert(caldavTask)
t.caldavTask = caldavTask.toSubset()
}
private fun CaldavTask.toSubset(): SubsetCaldav {
val result = SubsetCaldav()
result.cd_id = id
result.cd_calendar = calendar
result.cd_remote_parent = remoteParent
return result
}
}

@ -2,9 +2,9 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import org.tasks.filters.GtasksFilter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
@ -13,17 +13,18 @@ import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.GoogleTaskListMaker.REMOTE_ID
import org.tasks.makers.GoogleTaskListMaker.newGoogleTaskList
import org.tasks.makers.GoogleTaskMaker
import org.tasks.makers.GoogleTaskMaker.LIST
import org.tasks.makers.GoogleTaskMaker.TASK
import org.tasks.makers.GoogleTaskMaker.newGoogleTask
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
@ -37,11 +38,10 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: GoogleTaskManualSortAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = GtasksFilter(CaldavCalendar(uuid = "1234"))
private val filter = GtasksFilter(newGoogleTaskList(with(REMOTE_ID, "1234")))
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
@ -416,33 +416,33 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true)
tasks.clear()
adapter = GoogleTaskManualSortAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
adapter = GoogleTaskManualSortAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource)
}
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking {
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) })
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter, it) })
val adjustedTo = if (from < to) to + 1 else to
adapter.moved(from, adjustedTo, indent)
}
private fun checkOrder(order: Long, index: Int, parent: Long = 0) = runBlocking {
val googleTask = taskDao.fetch(adapter.getTask(index).id)!!
val googleTask = googleTaskDao.getByTaskId(adapter.getTask(index).id)!!
assertEquals(order, googleTask.order)
assertEquals(parent, googleTask.parent)
}
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking {
val task = newTask(*properties)
val parent = task.parent
task.parent = 0
taskDao.createNew(task)
googleTaskDao.insertAndShift(
task,
newCaldavTask(
with(TASK, task.id),
with(CALENDAR, "1234"),
),
false
)
newGoogleTask(
with(TASK, task.id),
with(LIST, "1234"),
with(GoogleTaskMaker.PARENT, parent)),
false)
task.id
}
}

@ -0,0 +1,78 @@
package com.todoroo.astrid.adapter
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class NonRecursiveQueryTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = BuiltInFilterExposer.getMyTasksFilter(ApplicationProvider.getApplicationContext<Context>().resources)
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
override fun getTaskCount() = tasks.size
}
@Before
override fun setUp() {
super.setUp()
preferences.clear()
preferences.setBoolean(R.string.p_use_paged_queries, true)
tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource)
}
@Test
fun ignoreSubtasks() {
val parent = addTask()
val child = addTask(with(PARENT, parent))
query()
assertEquals(child, tasks[1].id)
assertEquals(parent, tasks[1].parent)
assertEquals(0, tasks[1].indent)
}
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking {
val task = newTask(*properties)
taskDao.createNew(task)
task.id
}
private fun query() = runBlocking {
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter, it) })
}
}

@ -6,8 +6,7 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
@ -15,8 +14,8 @@ import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.injection.InjectingTestCase
@ -34,7 +33,6 @@ class OfflineSubtaskTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>()
@ -50,7 +48,7 @@ class OfflineSubtaskTest : InjectingTestCase() {
super.setUp()
preferences.clear()
tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource)
}
@ -86,6 +84,6 @@ class OfflineSubtaskTest : InjectingTestCase() {
}
private fun query() = runBlocking {
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) })
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter, it) })
}
}

@ -4,7 +4,7 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
@ -76,7 +76,7 @@ class RecursiveLoopTest : InjectingTestCase() {
}
private suspend fun getTasks() = taskDao.fetchTasks {
getQuery(preferences, BuiltInFilterExposer.getTodayFilter(context.resources))
getQuery(preferences, BuiltInFilterExposer.getTodayFilter(context.resources), it)
}
private suspend fun addTask(vararg properties: PropertyValue<in Task?, *>): Long {

@ -1,290 +1,52 @@
package com.todoroo.astrid.alarms
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.createDueDate
import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Notification
import org.tasks.data.entity.Task
import org.tasks.data.Alarm
import org.tasks.data.AlarmDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.jobs.AlarmEntry
import org.tasks.jobs.NotificationQueue
import org.tasks.makers.TaskMaker.REMINDER_LAST
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class AlarmJobServiceTest : InjectingTestCase() {
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var jobs: NotificationQueue
@Inject lateinit var alarmService: AlarmService
@Test
fun testNoAlarms() = runBlocking {
testResults(emptyList(), 0)
}
@Test
fun futureAlarmWithNoPastAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 18).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
testResults(emptyList(), DateTime(2024, 5, 18, 18, 0).millis)
}
}
@Test
fun pastAlarmWithNoFutureAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
0
)
}
}
@Test
fun pastRecurringAlarmWithFutureRecurrence() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(
Alarm(
type = Alarm.TYPE_REL_END,
repeat = 1,
interval = TimeUnit.HOURS.toMillis(6)
)
)
)
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
DateTime(2024, 5, 18, 0, 0).millis
)
}
}
@Test
fun pastAlarmsRemoveSnoozed() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(
Alarm(type = Alarm.TYPE_REL_END),
Alarm(time = DateTimeUtils2.currentTimeMillis(), type = Alarm.TYPE_SNOOZE)
)
)
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
0
)
assertEquals(
listOf(Alarm(id = 1, task = 1, time = 0, type = Alarm.TYPE_REL_END)),
alarmService.getAlarms(1)
)
}
}
@Test
fun alarmsOneMinuteApart() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME,
DateTime(2024, 5, 17, 23, 20).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
taskDao.insert(Task())
alarmService.synchronizeAlarms(
taskId = 2,
alarms = mutableSetOf(
Alarm(
type = Alarm.TYPE_SNOOZE,
time = DateTime(2024, 5, 17, 23, 21).millis)
)
)
fun scheduleAlarm() = runBlocking {
val task = newTask()
taskDao.createNew(task)
val alarmTime = DateTime(2017, 9, 24, 19, 57)
val alarm = Alarm(task.id, alarmTime.millis)
alarm.id = alarmDao.insert(alarm)
alarmService.scheduleAllAlarms()
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
DateTime(2024, 5, 17, 23, 21).millis
)
}
}
@Test
fun futureSnoozeOverrideOverdue() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(
Alarm(type = Alarm.TYPE_REL_END),
Alarm(
time = DateTimeUtils2.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5),
type = Alarm.TYPE_SNOOZE
)
)
)
testResults(
emptyList(),
DateTimeUtils2.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)
)
}
assertEquals(listOf(AlarmEntry(alarm)), jobs.getJobs())
}
@Test
fun ignoreStaleAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
reminderLast = DateTime(2024, 5, 17, 18, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
}
@Test
fun dontScheduleForCompletedTask() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
completionDate = DateTime(2024, 5, 17, 14, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
}
@Test
fun dontScheduleForDeletedTask() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
deletionDate = DateTime(2024, 5, 17, 14, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
}
val alarmTime = DateTime(2017, 9, 24, 19, 57)
val task = newTask(with(REMINDER_LAST, alarmTime.endOfMinute()))
taskDao.createNew(task)
alarmDao.insert(Alarm(task.id, alarmTime.millis))
alarmService.scheduleAllAlarms()
private suspend fun testResults(notifications: List<Notification>, nextAlarm: Long) {
val actualNextAlarm = alarmService.triggerAlarms {
assertEquals(notifications, it)
it.forEach { taskDao.setLastNotified(it.taskId, DateTimeUtils2.currentTimeMillis()) }
}
assertEquals(nextAlarm, actualNextAlarm)
assertTrue(jobs.getJobs().isEmpty())
}
}

@ -5,7 +5,8 @@
*/
package com.todoroo.astrid.dao
import org.tasks.data.entity.Task
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
@ -14,7 +15,6 @@ import org.junit.Assert.*
import org.junit.Test
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@ -75,23 +75,23 @@ class TaskDaoTests : InjectingTestCase() {
// create hidden task
task = Task()
task.title = "hidden"
task.hideUntil = currentTimeMillis() + 10000
task.hideUntil = DateUtilities.now() + 10000
taskDao.createNew(task)
// create task with deadlines
task = Task()
task.title = "deadlineInFuture"
task.dueDate = currentTimeMillis() + 10000
task.dueDate = DateUtilities.now() + 10000
taskDao.createNew(task)
task = Task()
task.title = "deadlineInPast"
task.dueDate = currentTimeMillis() - 10000
task.dueDate = DateUtilities.now() - 10000
taskDao.createNew(task)
// create completed task
task = Task()
task.title = "completed"
task.completionDate = currentTimeMillis() - 10000
task.completionDate = DateUtilities.now() - 10000
taskDao.createNew(task)
// check is active

@ -11,12 +11,14 @@ import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.GoogleTaskAccount
import org.tasks.data.GoogleTaskListDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.GtaskListMaker.ID
import org.tasks.makers.GtaskListMaker.NAME
import org.tasks.makers.GtaskListMaker.REMOTE_ID
import org.tasks.makers.GtaskListMaker.newGtaskList
import org.tasks.makers.RemoteGtaskListMaker
import org.tasks.makers.RemoteGtaskListMaker.newRemoteList
import javax.inject.Inject
@ -27,7 +29,6 @@ class GtasksListServiceTest : InjectingTestCase() {
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var caldavDao: CaldavDao
private lateinit var gtasksListService: GtasksListService
@ -43,14 +44,13 @@ class GtasksListServiceTest : InjectingTestCase() {
newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "Default")))
assertEquals(
CaldavCalendar(id = 1, account = "account", uuid = "1", name = "Default"),
googleTaskListDao.getById(1L)
)
newGtaskList(with(ID, 1L), with(REMOTE_ID, "1"), with(NAME, "Default")),
googleTaskListDao.getById(1L))
}
@Test
fun testGetListByRemoteId() = runBlocking {
val list = CaldavCalendar(uuid = "1")
val list = newGtaskList(with(REMOTE_ID, "1"))
list.id = googleTaskListDao.insertOrReplace(list)
assertEquals(list, googleTaskListDao.getByRemoteId("1"))
}
@ -62,24 +62,22 @@ class GtasksListServiceTest : InjectingTestCase() {
@Test
fun testDeleteMissingList() = runBlocking {
googleTaskListDao.insertOrReplace(CaldavCalendar(id = 1, account = "account", uuid = "1"))
googleTaskListDao.insertOrReplace(newGtaskList(with(ID, 1L), with(REMOTE_ID, "1")))
val taskList = newRemoteList(with(RemoteGtaskListMaker.REMOTE_ID, "2"))
setLists(taskList)
assertEquals(
listOf(CaldavCalendar(id = 2, account = "account", uuid = "2", name = "Default")),
googleTaskListDao.getLists("account")
)
listOf(newGtaskList(with(ID, 2L), with(REMOTE_ID, "2"))),
googleTaskListDao.getLists("account"))
}
@Test
fun testUpdateListName() = runBlocking {
googleTaskListDao.insertOrReplace(
CaldavCalendar(id = 1, uuid = "1", name = "oldName", account = "account")
)
newGtaskList(with(ID, 1L), with(REMOTE_ID, "1"), with(NAME, "oldName")))
setLists(
newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "newName")))
assertEquals("newName", googleTaskListDao.getById(1)!!.name)
assertEquals("newName", googleTaskListDao.getById(1)!!.title)
}
@Test
@ -89,11 +87,8 @@ class GtasksListServiceTest : InjectingTestCase() {
}
private suspend fun setLists(vararg list: TaskList) {
val account = CaldavAccount(
username = "account",
uuid = "account",
)
caldavDao.insert(account)
val account = GoogleTaskAccount("account")
googleTaskListDao.insert(account)
gtasksListService.updateLists(account, listOf(*list))
}
}

@ -0,0 +1,81 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.gtasks
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class GtasksMetadataServiceTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
private var task: Task? = null
private var metadata: GoogleTask? = null
@Test
fun testMetadataFound() = runBlocking {
givenTask(taskWithMetadata(null))
whenSearchForMetadata()
thenExpectMetadataFound()
}
@Test
fun testMetadataDoesntExist() = runBlocking {
givenTask(taskWithoutMetadata())
whenSearchForMetadata()
thenExpectNoMetadataFound()
}
private fun thenExpectNoMetadataFound() {
assertNull(metadata)
}
private fun thenExpectMetadataFound() {
assertNotNull(metadata)
}
// --- helpers
private suspend fun whenSearchForMetadata() {
metadata = googleTaskDao.getByTaskId(task!!.id)
}
private suspend fun taskWithMetadata(id: String?): Task {
val task = Task()
task.title = "cats"
taskDao.createNew(task)
val metadata = GoogleTask(task.id, "")
if (id != null) {
metadata.remoteId = id
}
metadata.task = task.id
googleTaskDao.insert(metadata)
return task
}
private fun givenTask(taskToTest: Task) {
task = taskToTest
}
private suspend fun taskWithoutMetadata(): Task {
val task = Task()
task.title = "dogs"
taskDao.createNew(task)
return task
}
}

@ -1,7 +1,7 @@
package com.todoroo.astrid.model
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
@ -10,7 +10,7 @@ import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeClock
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.DateTimeUtils
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@ -23,7 +23,7 @@ class TaskTest : InjectingTestCase() {
freezeClock {
val task = Task()
taskDao.createNew(task)
assertEquals(currentTimeMillis(), task.creationDate)
assertEquals(DateTimeUtils.currentTimeMillis(), task.creationDate)
}
}

@ -0,0 +1,327 @@
package com.todoroo.astrid.reminders
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.Freeze
import org.tasks.R
import org.tasks.data.TaskDao
import org.tasks.date.DateTimeUtils
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.jobs.NotificationQueue
import org.tasks.jobs.ReminderEntry
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_DATE
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.RANDOM_REMINDER_PERIOD
import org.tasks.makers.TaskMaker.REMINDERS
import org.tasks.makers.TaskMaker.REMINDER_LAST
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import org.tasks.reminders.Random
import org.tasks.time.DateTime
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class ReminderServiceTest : InjectingTestCase() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var jobs: NotificationQueue
private lateinit var service: ReminderService
private lateinit var random: RandomStub
@Before
override fun setUp() {
super.setUp()
random = RandomStub()
preferences.clear()
service = ReminderService(preferences, jobs, random, taskDao)
}
@Test
fun dontScheduleDueDateReminderWhenFlagNotSet() {
service.scheduleAlarm(newTask(with(ID, 1L), with(DUE_TIME, DateTimeUtils.newDateTime())))
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenTimeNotSet() {
service.scheduleAlarm(newTask(with(ID, 1L), with(REMINDERS, Task.NOTIFY_AT_DEADLINE)))
assertTrue(jobs.isEmpty())
}
@Test
fun schedulePastDueDate() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime().minusDays(1)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.dueDate, ReminderService.TYPE_DUE))
}
@Test
fun scheduleFutureDueDate() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime().plusDays(1)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.dueDate, ReminderService.TYPE_DUE))
}
@Test
fun scheduleReminderAtDefaultDueTime() {
val now = DateTimeUtils.newDateTime()
val task = newTask(with(ID, 1L), with(DUE_DATE, now), with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, now.startOfDay().withHourOfDay(18).millis, ReminderService.TYPE_DUE))
}
@Test
fun dontScheduleReminderForCompletedTask() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime().plusDays(1)),
with(COMPLETION_TIME, DateTimeUtils.newDateTime()),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleReminderForDeletedTask() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime().plusDays(1)),
with(DELETION_TIME, DateTimeUtils.newDateTime()),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenAlreadyReminded() {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(DUE_TIME, now),
with(REMINDER_LAST, now.plusSeconds(1)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun ignoreStaleSnoozeTime() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime()),
with(SNOOZE_TIME, DateTimeUtils.newDateTime().minusMinutes(5)),
with(REMINDER_LAST, DateTimeUtils.newDateTime().minusMinutes(4)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.dueDate, ReminderService.TYPE_DUE))
}
@Test
fun dontIgnoreMissedSnoozeTime() {
val dueDate = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(DUE_TIME, dueDate),
with(SNOOZE_TIME, dueDate.minusMinutes(4)),
with(REMINDER_LAST, dueDate.minusMinutes(5)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.reminderSnooze, ReminderService.TYPE_SNOOZE))
}
@Test
fun scheduleInitialRandomReminder() {
random.seed = 0.3865f
Freeze.freezeClock {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(REMINDER_LAST, null as DateTime?),
with(CREATION_TIME, now.minusDays(1)),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_WEEK))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, now.minusDays(1).millis + 584206592, ReminderService.TYPE_RANDOM))
}
}
@Test
fun scheduleNextRandomReminder() {
random.seed = 0.3865f
Freeze.freezeClock {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(REMINDER_LAST, now.minusDays(1)),
with(CREATION_TIME, now.minusDays(30)),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_WEEK))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, now.minusDays(1).millis + 584206592, ReminderService.TYPE_RANDOM))
}
}
@Test
fun scheduleOverdueRandomReminder() {
random.seed = 0.3865f
Freeze.freezeClock {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(REMINDER_LAST, now.minusDays(14)),
with(CREATION_TIME, now.minusDays(30)),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_WEEK))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, now.millis + 10148400, ReminderService.TYPE_RANDOM))
}
}
@Test
fun scheduleOverdueNoLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, null as DateTime?),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 23, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverduePastLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 24, 12, 0)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 24, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverdueBeforeLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 12, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 24, 15, 0)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 25, 12, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverdueWithNoDueTime() {
preferences.setInt(R.string.p_rmd_time, TimeUnit.HOURS.toMillis(15).toInt())
val task = newTask(
with(ID, 1L),
with(DUE_DATE, DateTime(2017, 9, 22)),
with(REMINDER_LAST, DateTime(2017, 9, 23, 12, 17, 59, 999)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 23, 15, 0, 0, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleSubsequentOverdueReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 23, 15, 30, 59, 999)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 24, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverdueAfterLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 23, 12, 17, 59, 999)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 23, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun snoozeOverridesAll() {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(DUE_TIME, now),
with(SNOOZE_TIME, now.plusMonths(12)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE or Task.NOTIFY_AFTER_DEADLINE),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_HOUR))
service.scheduleAlarm(task)
verify(ReminderEntry(1, now.plusMonths(12).millis, ReminderService.TYPE_SNOOZE))
}
private fun verify(vararg reminders: ReminderEntry) = assertEquals(reminders.toList(), jobs.getJobs())
internal class RandomStub : Random() {
var seed = 1.0f
override fun nextFloat() = seed
}
}

@ -1,68 +1,50 @@
package com.todoroo.astrid.repeats
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskCompleter
import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.TaskDao
import org.tasks.data.TaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.RECUR
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class RepeatWithSubtasksTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var repeat: RepeatTaskHelper
@Test
fun uncompleteGrandchildren() = runBlocking {
val grandparent = taskDao.createNew(
Task(
recurrence = "RRULE:FREQ=DAILY"
)
)
val parent = taskDao.createNew(
Task(
parent = grandparent
)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
val grandparent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY")))
val parent = taskDao.createNew(newTask(with(PARENT, grandparent)))
val child = taskDao.createNew(newTask(
with(PARENT, parent),
with(COMPLETION_TIME, DateTime())
))
assertTrue(taskDao.fetch(child)!!.isCompleted)
taskCompleter.setComplete(grandparent)
repeat.handleRepeat(taskDao.fetch(grandparent)!!)
assertFalse(taskDao.fetch(child)!!.isCompleted)
}
@Test
fun uncompleteGoogleTaskChildren() = runBlocking {
val parent = taskDao.createNew(
Task(
recurrence = "RRULE:FREQ=DAILY"
)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
assertTrue(taskDao.fetch(child)!!.isCompleted)
val parent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY")))
val child = taskDao.createNew(newTask(
with(PARENT, parent),
with(COMPLETION_TIME, DateTime())
))
taskCompleter.setComplete(parent)
repeat.handleRepeat(taskDao.fetch(parent)!!)
assertFalse(taskDao.fetch(child)!!.isCompleted)
}

@ -5,14 +5,14 @@
*/
package com.todoroo.astrid.service
import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.data.dao.TagDataDao
import org.tasks.data.TagDataDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import java.util.*

@ -1,12 +1,10 @@
package com.todoroo.astrid.service
import com.todoroo.astrid.api.PermaSql.VALUE_EOD
import com.todoroo.astrid.api.PermaSql.VALUE_EOD_NEXT_WEEK
import com.todoroo.astrid.api.PermaSql.VALUE_EOD_TOMORROW
import org.tasks.data.entity.Task
import org.tasks.data.entity.Task.Companion.DUE_DATE
import org.tasks.data.entity.Task.Companion.HIDE_UNTIL
import org.tasks.data.entity.Task.Companion.URGENCY_SPECIFIC_DAY
import com.todoroo.astrid.api.PermaSql.*
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.DUE_DATE
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
@ -14,7 +12,6 @@ import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.R
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.createDueDate
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences
@ -38,7 +35,7 @@ class TaskCreatorTest : InjectingTestCase() {
assertEquals(DateTime(2021, 2, 4).millis, task.hideUntil)
assertEquals(
createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis),
Task.createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis),
task.dueDate
)
}
@ -66,7 +63,7 @@ class TaskCreatorTest : InjectingTestCase() {
assertEquals(DateTime(2021, 2, 4).millis, task.hideUntil)
assertEquals(
createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 4).millis),
Task.createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 4).millis),
task.dueDate
)
}
@ -96,7 +93,7 @@ class TaskCreatorTest : InjectingTestCase() {
}
assertEquals(
createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis),
Task.createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis),
task.dueDate
)
}

@ -1,15 +1,25 @@
package com.todoroo.astrid.service
import org.tasks.data.entity.Task
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.core.BuiltInFilterExposer.Companion.getMyTasksFilter
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.TaskDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.GoogleTaskMaker
import org.tasks.makers.GoogleTaskMaker.TASK
import org.tasks.makers.GoogleTaskMaker.newGoogleTask
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.RECUR
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@ -17,26 +27,111 @@ import javax.inject.Inject
class TaskDeleterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Test
fun markTaskAsDeleted() = runBlocking {
val task = Task()
taskDao.createNew(task)
fun clearCompletedTask() = runBlocking {
val task = taskDao.createNew(newTask(with(COMPLETION_TIME, DateTime())))
taskDeleter.markDeleted(task)
clearCompleted()
assertTrue(taskDao.fetch(task.id)!!.isDeleted)
assertTrue(taskDao.fetch(task)!!.isDeleted)
}
@Test
fun dontDeleteReadOnlyTasks() = runBlocking {
val task = Task(
readOnly = true
)
taskDao.createNew(task)
fun dontDeleteTaskWithRecurringParent() = runBlocking {
val parent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1")))
val child = taskDao.createNew(newTask(
with(PARENT, parent),
with(COMPLETION_TIME, DateTime())
))
taskDeleter.markDeleted(task)
clearCompleted()
assertFalse(taskDao.fetch(task.id)!!.isDeleted)
assertFalse(taskDao.fetch(child)!!.isDeleted)
}
}
@Test
fun dontDeleteTaskWithRecurringGrandparent() = runBlocking {
val grandparent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1")))
val parent = taskDao.createNew(newTask(with(PARENT, grandparent)))
val child = taskDao.createNew(newTask(
with(PARENT, parent),
with(COMPLETION_TIME, DateTime())
))
clearCompleted()
assertFalse(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearGrandchildWithNoRecurringAncestors() = runBlocking {
val grandparent = taskDao.createNew(newTask())
val parent = taskDao.createNew(newTask(with(PARENT, grandparent)))
val child = taskDao.createNew(newTask(
with(PARENT, parent),
with(COMPLETION_TIME, DateTime())
))
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearGrandchildWithCompletedRecurringAncestor() = runBlocking {
val grandparent = taskDao.createNew(newTask(
with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1"),
with(COMPLETION_TIME, DateTime())
))
val parent = taskDao.createNew(newTask(with(PARENT, grandparent)))
val child = taskDao.createNew(newTask(
with(PARENT, parent),
with(COMPLETION_TIME, DateTime())
))
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun dontClearCompletedGoogleTaskWithRecurringParent() = runBlocking {
val parent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1")))
val child = taskDao.createNew(newTask(with(COMPLETION_TIME, DateTime())))
googleTaskDao.insert(newGoogleTask(with(TASK, child), with(GoogleTaskMaker.PARENT, parent)))
clearCompleted()
assertFalse(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearCompletedGoogleTaskWithNonRecurringParent() = runBlocking {
val parent = taskDao.createNew(newTask())
val child = taskDao.createNew(newTask(with(COMPLETION_TIME, DateTime())))
googleTaskDao.insert(newGoogleTask(with(TASK, child), with(GoogleTaskMaker.PARENT, parent)))
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearCompletedGoogleTaskWithCompletedRecurringParent() = runBlocking {
val parent = taskDao.createNew(newTask(
with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1"),
with(COMPLETION_TIME, DateTime())
))
val child = taskDao.createNew(newTask(with(COMPLETION_TIME, DateTime())))
googleTaskDao.insert(newGoogleTask(with(TASK, child), with(GoogleTaskMaker.PARENT, parent)))
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
private suspend fun clearCompleted() =
taskDeleter.clearCompleted(getMyTasksFilter(context.resources))
}

@ -1,32 +1,34 @@
package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.filters.CaldavFilter
import org.tasks.filters.GtasksFilter
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.jobs.WorkManager
import org.tasks.makers.CaldavTaskMaker
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.GoogleTaskMaker.LIST
import org.tasks.makers.GoogleTaskMaker.PARENT
import org.tasks.makers.GoogleTaskMaker.TASK
import org.tasks.makers.GoogleTaskMaker.newGoogleTask
import org.tasks.makers.GtaskListMaker
import org.tasks.makers.GtaskListMaker.newGtaskList
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@ -40,28 +42,18 @@ class TaskMoverTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskMover: TaskMover
@Before
fun setup() {
runBlocking {
caldavDao.insert(CaldavCalendar(uuid = "1", account = "account1"))
caldavDao.insert(CaldavCalendar(uuid = "2", account = "account2"))
}
}
@Test
fun moveBetweenGoogleTaskLists() = runBlocking {
setAccountType("account1", TYPE_GOOGLE_TASKS)
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
moveToGoogleTasks("2", 1)
assertEquals("2", googleTaskDao.getByTaskId(1)?.calendar)
assertEquals("2", googleTaskDao.getByTaskId(1)!!.listId)
}
@Test
fun deleteGoogleTaskAfterMove() = runBlocking {
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(1)
assertEquals(1, deleted.size.toLong())
@ -71,25 +63,23 @@ class TaskMoverTest : InjectingTestCase() {
@Test
fun moveChildrenBetweenGoogleTaskLists() = runBlocking {
setAccountType("account1", TYPE_GOOGLE_TASKS)
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1)
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
createTasks(1, 2)
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(PARENT, 1L)))
moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(2)
assertEquals(1, deleted.size.toLong())
assertEquals(2, deleted[0].task)
assertTrue(deleted[0].deleted > 0)
assertEquals(1L, taskDao.fetch(2)?.parent)
assertEquals("2", googleTaskDao.getByTaskId(2)?.calendar)
val task = googleTaskDao.getByTaskId(2)!!
assertEquals(1, task.parent)
assertEquals("2", task.listId)
}
@Test
fun moveBetweenCaldavList() = runBlocking {
createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1")))
caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("2", 1)
assertEquals("2", caldavDao.getTask(1)!!.calendar)
}
@ -97,7 +87,7 @@ class TaskMoverTest : InjectingTestCase() {
@Test
fun deleteCaldavTaskAfterMove() = runBlocking {
createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1")))
caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("2", 1)
val deleted = caldavDao.getMoved("1")
assertEquals(1, deleted.size.toLong())
@ -113,14 +103,14 @@ class TaskMoverTest : InjectingTestCase() {
caldavDao.insert(
listOf(
newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask(
with(TASK, 2L),
with(CaldavTaskMaker.TASK, 2L),
with(CALENDAR, "1"),
with(REMOTE_ID, "b"),
with(REMOTE_PARENT, "a")),
newCaldavTask(
with(TASK, 3L),
with(CaldavTaskMaker.TASK, 3L),
with(CALENDAR, "1"),
with(REMOTE_PARENT, "b"))))
moveToCaldavList("2", 1)
@ -133,16 +123,13 @@ class TaskMoverTest : InjectingTestCase() {
@Test
fun moveGoogleTaskChildrenToCaldav() = runBlocking {
setAccountType("account1", TYPE_GOOGLE_TASKS)
setAccountType("account2", TYPE_CALDAV)
createTasks(1)
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
createTasks(1, 2)
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(PARENT, 1L)))
moveToCaldavList("1", 1)
val task = caldavDao.getTask(2)
assertEquals("1", task!!.calendar)
assertEquals(1L, taskDao.fetch(2)?.parent)
assertEquals(1, taskDao.fetch(2)!!.parent)
}
@Test
@ -151,7 +138,8 @@ class TaskMoverTest : InjectingTestCase() {
createSubtask(2, 1)
createSubtask(3, 2)
moveToGoogleTasks("1", 1)
assertEquals(1L, taskDao.fetch(3)?.parent)
assertEquals(1, googleTaskDao.getByTaskId(3)!!.parent)
assertEquals(0, taskDao.fetch(3)!!.parent)
}
@Test
@ -159,7 +147,7 @@ class TaskMoverTest : InjectingTestCase() {
createTasks(1)
createSubtask(2, 1)
moveToGoogleTasks("1", 2)
assertEquals(0L, taskDao.fetch(2)?.parent)
assertEquals(0, taskDao.fetch(2)!!.parent)
}
@Test
@ -178,31 +166,30 @@ class TaskMoverTest : InjectingTestCase() {
caldavDao.insert(
listOf(
newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask(
with(TASK, 2L),
with(CaldavTaskMaker.TASK, 2L),
with(CALENDAR, "1"),
with(REMOTE_ID, "b"),
with(REMOTE_PARENT, "a")),
newCaldavTask(
with(TASK, 3L),
with(CaldavTaskMaker.TASK, 3L),
with(CALENDAR, "1"),
with(REMOTE_PARENT, "b"))))
moveToGoogleTasks("1", 1)
val task = taskDao.fetch(3L)
assertEquals(1L, task?.parent)
val task = googleTaskDao.getByTaskId(3L)!!
assertEquals(1, task.parent)
}
@Test
fun moveGoogleTaskChildWithoutParent() = runBlocking {
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1)
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
createTasks(1, 2)
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(PARENT, 1L)))
moveToGoogleTasks("2", 2)
assertEquals(0L, taskDao.fetch(2)?.parent)
assertEquals("2", googleTaskDao.getByTaskId(2)?.calendar)
val task = googleTaskDao.getByTaskId(2)!!
assertEquals(0L, task.parent)
assertEquals("2", task.listId)
}
@Test
@ -212,9 +199,9 @@ class TaskMoverTest : InjectingTestCase() {
caldavDao.insert(
listOf(
newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask(
with(TASK, 2L),
with(CaldavTaskMaker.TASK, 2L),
with(CALENDAR, "1"),
with(REMOTE_PARENT, "a"))))
moveToCaldavList("2", 2)
@ -225,19 +212,17 @@ class TaskMoverTest : InjectingTestCase() {
@Test
fun moveGoogleTaskToCaldav() = runBlocking {
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
moveToCaldavList("2", 1)
assertEquals("2", caldavDao.getTask(1)!!.calendar)
}
@Test
fun moveCaldavToGoogleTask() = runBlocking {
setAccountType("account1", TYPE_CALDAV)
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1")))
caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1)
assertEquals("2", googleTaskDao.getByTaskId(1L)?.calendar)
assertEquals("2", googleTaskDao.getByTaskId(1L)!!.listId)
}
@Test
@ -246,15 +231,14 @@ class TaskMoverTest : InjectingTestCase() {
createSubtask(2, 1)
createSubtask(3, 2)
moveToCaldavList("1", 1)
assertEquals("1", caldavDao.getTask(3)?.calendar)
assertEquals(2L, taskDao.fetch(3)?.parent)
assertEquals("1", caldavDao.getTask(3)!!.calendar)
assertEquals(2, taskDao.fetch(3)!!.parent)
}
@Test
fun moveToSameGoogleTaskListIsNoop() = runBlocking {
setAccountType("account1", TYPE_GOOGLE_TASKS)
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
moveToGoogleTasks("1", 1)
assertTrue(googleTaskDao.getDeletedByTaskId(1).isEmpty())
assertEquals(1, googleTaskDao.getAllByTaskId(1).size.toLong())
@ -263,7 +247,7 @@ class TaskMoverTest : InjectingTestCase() {
@Test
fun moveToSameCaldavListIsNoop() = runBlocking {
createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1")))
caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("1", 1)
assertTrue(caldavDao.getMoved("1").isEmpty())
assertEquals(1, caldavDao.getTasks(1).size.toLong())
@ -271,10 +255,9 @@ class TaskMoverTest : InjectingTestCase() {
@Test
fun dontDuplicateWhenParentAndChildGoogleTaskMoved() = runBlocking {
createTasks(1)
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
createTasks(1, 2)
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(PARENT, 1L)))
moveToGoogleTasks("2", 1, 2)
assertEquals(1, googleTaskDao.getAllByTaskId(2).filter { it.deleted == 0L }.size)
}
@ -286,9 +269,9 @@ class TaskMoverTest : InjectingTestCase() {
caldavDao.insert(
listOf(
newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask(
with(TASK, 2L),
with(CaldavTaskMaker.TASK, 2L),
with(CALENDAR, "1"),
with(REMOTE_PARENT, "a"))))
moveToCaldavList("2", 1, 2)
@ -302,23 +285,14 @@ class TaskMoverTest : InjectingTestCase() {
}
private suspend fun createSubtask(id: Long, parent: Long) {
taskDao.createNew(newTask(with(ID, id), with(PARENT, parent)))
taskDao.createNew(newTask(with(ID, id), with(TaskMaker.PARENT, parent)))
}
private suspend fun moveToGoogleTasks(list: String, vararg tasks: Long) {
taskMover.move(tasks.toList(), GtasksFilter(CaldavCalendar(uuid = list)))
taskMover.move(tasks.toList(), GtasksFilter(newGtaskList(with(GtaskListMaker.REMOTE_ID, list))))
}
private suspend fun moveToCaldavList(calendar: String, vararg tasks: Long) {
taskMover.move(tasks.toList(), CaldavFilter(CaldavCalendar(name = "", uuid = calendar)))
}
private suspend fun setAccountType(account: String, type: Int) {
caldavDao.insert(
CaldavAccount(
uuid = account,
accountType = type,
)
)
taskMover.move(tasks.toList(), CaldavFilter(CaldavCalendar("", calendar)))
}
}

@ -5,7 +5,7 @@
*/
package com.todoroo.astrid.service
import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
@ -16,7 +16,7 @@ import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.tasks.R
import org.tasks.data.dao.TagDataDao
import org.tasks.data.TagDataDao
import org.tasks.date.DateTimeUtils
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule

@ -1,62 +0,0 @@
@file:Suppress("ClassName")
package com.todoroo.astrid.service
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.todoroo.astrid.service.Upgrade_11_12_3.Companion.LEGACY_PREFERENCE
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.TestUtilities.newPreferences
import org.tasks.preferences.Preferences
@RunWith(AndroidJUnit4::class)
class Upgrade_11_12_3_Test {
private lateinit var preferences: Preferences
private lateinit var upgrader: Upgrade_11_12_3
@Test
fun migrateNoDefaultReminders() {
preferences.setString(LEGACY_PREFERENCE, "0")
upgrader.migrateDefaultReminderPreference()
assertEquals(emptySet<String>(), preferences.defaultRemindersSet)
assertEquals(0, preferences.defaultReminders)
}
@Test
fun migrateWhenDue() {
preferences.setString(LEGACY_PREFERENCE, "2")
upgrader.migrateDefaultReminderPreference()
assertEquals(setOf("2"), preferences.defaultRemindersSet)
assertEquals(2, preferences.defaultReminders)
}
@Test
fun migrateWhenOverdue() {
preferences.setString(LEGACY_PREFERENCE, "4")
upgrader.migrateDefaultReminderPreference()
assertEquals(setOf("4"), preferences.defaultRemindersSet)
assertEquals(4, preferences.defaultReminders)
}
@Test
fun migrateWhenDueAndOverdue() {
preferences.setString(LEGACY_PREFERENCE, "6")
upgrader.migrateDefaultReminderPreference()
assertEquals(setOf("2", "4"), preferences.defaultRemindersSet)
assertEquals(6, preferences.defaultReminders)
}
@Before
fun setUp() {
preferences = newPreferences(ApplicationProvider.getApplicationContext())
preferences.clear()
upgrader = Upgrade_11_12_3(preferences)
}
}

@ -3,23 +3,21 @@
package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.TestUtilities.assertEquals
import org.tasks.caldav.VtodoCache
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task
import org.tasks.data.CaldavDao
import org.tasks.data.TaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.VTODO
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.DUE_DATE
import org.tasks.makers.TaskMaker.HIDE_TYPE
@ -36,26 +34,11 @@ class Upgrade_11_3_Test : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var openTaskDao: TestOpenTaskDao
@Inject lateinit var upgrader: Upgrade_11_3
@Inject lateinit var vtodoCache: VtodoCache
private lateinit var calendar: CaldavCalendar
@Before
override fun setUp() {
super.setUp()
calendar = CaldavCalendar()
runBlocking {
caldavDao.insert(calendar)
}
}
@Test
fun applyRemoteiCalendarStartDate() = runBlocking {
val taskId = taskDao.insert(newTask())
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE)
caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_WITH_START_DATE)))
upgrader.applyiCalendarStartDates()
assertEquals(DateTime(2021, 1, 21), taskDao.fetch(taskId)?.hideUntil)
@ -67,10 +50,7 @@ class Upgrade_11_3_Test : InjectingTestCase() {
with(DUE_DATE, DateTime(2021, 1, 20)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE)
))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE)
caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_WITH_START_DATE)))
upgrader.applyiCalendarStartDates()
assertEquals(DateTime(2021, 1, 20), taskDao.fetch(taskId)?.hideUntil)
@ -84,9 +64,7 @@ class Upgrade_11_3_Test : InjectingTestCase() {
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE),
with(MODIFICATION_TIME, DateTime(2021, 1, 21, 9, 50, 4, 348))
))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE)
caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_WITH_START_DATE)))
freezeAt(upgradeTime) {
upgrader.applyiCalendarStartDates()
@ -99,9 +77,7 @@ class Upgrade_11_3_Test : InjectingTestCase() {
fun dontTouchWhenNoiCalendarStartDate() = runBlocking {
val modificationTime = DateTime(2021, 1, 21, 9, 50, 4, 348)
val taskId = taskDao.insert(newTask(with(MODIFICATION_TIME, modificationTime)))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_NO_START_DATE)
caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_NO_START_DATE)))
upgrader.applyiCalendarStartDates()

@ -1,13 +1,13 @@
package com.todoroo.astrid.subtasks
import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.data.entity.TaskListMetadata
import org.tasks.data.TaskListMetadata
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)

@ -1,12 +1,12 @@
package com.todoroo.astrid.subtasks
import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.tasks.data.entity.TaskListMetadata
import org.tasks.data.TaskListMetadata
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)

@ -1,20 +1,20 @@
package com.todoroo.astrid.subtasks
import androidx.test.InstrumentationRegistry
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.tasks.data.dao.TaskListMetadataDao
import org.tasks.data.entity.Task
import org.tasks.filters.AstridOrderingFilter
import org.tasks.data.TaskListMetadataDao
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences
import javax.inject.Inject
abstract class SubtasksTestCase : InjectingTestCase() {
lateinit var updater: SubtasksFilterUpdater
lateinit var filter: AstridOrderingFilter
lateinit var filter: Filter
@Inject lateinit var taskListMetadataDao: TaskListMetadataDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences

@ -1,9 +1,9 @@
package com.todoroo.astrid.sync
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.TagData
import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import org.tasks.data.TagData
import org.tasks.data.TagDataDao
import org.tasks.injection.InjectingTestCase
import javax.inject.Inject
@ -12,17 +12,17 @@ open class NewSyncTestCase : InjectingTestCase() {
@Inject lateinit var tagDataDao: TagDataDao
suspend fun createTask(): Task {
val task = Task(
title = SYNC_TASK_TITLE,
priority = SYNC_TASK_IMPORTANCE,
)
val task = Task()
task.title = SYNC_TASK_TITLE
task.priority = SYNC_TASK_IMPORTANCE
taskDao.createNew(task)
return task
}
suspend fun createTagData(): TagData {
val tag = TagData(name = "new tag")
tagDataDao.insert(tag)
val tag = TagData()
tag.name = "new tag"
tagDataDao.createNew(tag)
return tag
}

@ -1,6 +1,6 @@
package com.todoroo.astrid.sync
import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking

@ -1 +0,0 @@
../../../../test/java/org/tasks/Freeze.kt

@ -1 +0,0 @@
../../../../test/java/org/tasks/SuspendFreeze.kt

@ -1 +0,0 @@
../../../../test/java/org/tasks/TestUtilities.kt

@ -1,22 +1,20 @@
package org.tasks.caldav
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.UUIDHelper
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.ETAG
import org.tasks.makers.CaldavTaskMaker.OBJECT
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
@ -25,13 +23,12 @@ class CaldavSynchronizerTest : CaldavTest() {
@Before
override fun setUp() = runBlocking {
super.setUp()
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = "username",
password = encryption.encrypt("password"),
url = server.url("/remote.php/dav/calendars/user1/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
username = "username"
password = encryption.encrypt("password")
url = server.url("/remote.php/dav/calendars/user1/").toString()
id = caldavDao.insert(this)
}
}
@ -46,13 +43,11 @@ class CaldavSynchronizerTest : CaldavTest() {
@Test
fun dontFetchCalendarIfCtagMatches() = runBlocking {
caldavDao.insert(
CaldavCalendar(
account = this@CaldavSynchronizerTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@CaldavSynchronizerTest.account.url}test-shared/",
)
)
caldavDao.insert(CaldavCalendar().apply {
account = this@CaldavSynchronizerTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@CaldavSynchronizerTest.account.url}test-shared/"
})
enqueue(OC_SHARE_PROPFIND)
sync()
@ -60,17 +55,16 @@ class CaldavSynchronizerTest : CaldavTest() {
@Test
fun dontFetchTaskIfEtagMatches() = runBlocking {
val calendar = CaldavCalendar(
account = this@CaldavSynchronizerTest.account.uuid,
uuid = UUIDHelper.newUUID(),
url = "${this@CaldavSynchronizerTest.account.url}test-shared/",
)
caldavDao.insert(calendar)
val calendar = CaldavCalendar().apply {
account = this@CaldavSynchronizerTest.account.uuid
uuid = UUIDHelper.newUUID()
url = "${this@CaldavSynchronizerTest.account.url}test-shared/"
caldavDao.insert(this)
}
caldavDao.insert(newCaldavTask(
with(TASK, taskDao.insert(newTask())),
with(OBJECT, "3164728546640386952.ics"),
with(ETAG, "43b3ffaac5131880e4dd07a79adba82a"),
with(CALENDAR, calendar.uuid)
with(OBJECT, "3164728546640386952.ics"),
with(ETAG, "43b3ffaac5131880e4dd07a79adba82a"),
with(CALENDAR, calendar.uuid)
))
enqueue(OC_SHARE_PROPFIND, OC_SHARE_REPORT)

@ -9,8 +9,8 @@ import org.junit.Before
import org.junit.Rule
import org.junit.rules.Timeout
import org.tasks.R
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.dao.CaldavDao
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavDao
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption

@ -1,19 +1,19 @@
package org.tasks.caldav
import org.tasks.data.UUIDHelper
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@ -79,14 +79,13 @@ class ServerDetectionTest : CaldavTest() {
vararg headers: Pair<String, String>,
accountType: Int = TYPE_CALDAV
) {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = "username",
password = encryption.encrypt("password"),
url = server.url("/remote.php/dav/calendars/user1/").toString(),
accountType = accountType,
).let {
it.copy(id = caldavDao.insert(it))
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
username = "username"
password = encryption.encrypt("password")
url = server.url("/remote.php/dav/calendars/user1/").toString()
id = caldavDao.insert(this)
this.accountType = accountType
}
this.headers.putAll(headers)
enqueue(NO_CALENDARS)

@ -1,16 +1,16 @@
package org.tasks.caldav
import org.tasks.data.UUIDHelper
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.dao.PrincipalDao
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@ -22,20 +22,19 @@ class SharingMailboxDotOrgTest : CaldavTest() {
@Test
fun ownerAccess() = runBlocking {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = "3",
password = encryption.encrypt("password"),
url = server.url("/caldav/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
username = "3"
password = encryption.encrypt("password")
url = server.url("/caldav/").toString()
id = caldavDao.insert(this)
}
val calendar = CaldavCalendar().apply {
account = this@SharingMailboxDotOrgTest.account.uuid
ctag = "1614876450015"
url = "${this@SharingMailboxDotOrgTest.account.url}MzM/"
caldavDao.insert(this)
}
val calendar = CaldavCalendar(
account = this@SharingMailboxDotOrgTest.account.uuid,
ctag = "1614876450015",
url = "${this@SharingMailboxDotOrgTest.account.url}MzM/",
)
caldavDao.insert(calendar)
enqueue(SHARE_OWNER)
sync()
@ -46,20 +45,19 @@ class SharingMailboxDotOrgTest : CaldavTest() {
@Test
fun principalForSharee() = runBlocking {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = "3",
password = encryption.encrypt("password"),
url = server.url("/caldav/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
username = "3"
password = encryption.encrypt("password")
url = server.url("/caldav/").toString()
id = caldavDao.insert(this)
}
val calendar = CaldavCalendar().apply {
account = this@SharingMailboxDotOrgTest.account.uuid
ctag = "1614876450015"
url = "${this@SharingMailboxDotOrgTest.account.url}MzM/"
caldavDao.insert(this)
}
val calendar = CaldavCalendar(
account = this@SharingMailboxDotOrgTest.account.uuid,
ctag = "1614876450015",
url = "${this@SharingMailboxDotOrgTest.account.url}MzM/",
)
caldavDao.insert(calendar)
enqueue(SHARE_OWNER)
sync()

@ -1,16 +1,16 @@
package org.tasks.caldav
import org.tasks.data.UUIDHelper
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.dao.PrincipalDao
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@ -21,25 +21,24 @@ class SharingOwncloudTest : CaldavTest() {
@Inject lateinit var principalDao: PrincipalDao
private suspend fun setupAccount(user: String) {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = user,
password = encryption.encrypt("password"),
url = server.url("/remote.php/dav/calendars/$user/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
username = user
password = encryption.encrypt("password")
url = server.url("/remote.php/dav/calendars/$user/").toString()
id = caldavDao.insert(this)
}
}
@Test
fun calendarOwner() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar(
account = this@SharingOwncloudTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingOwncloudTest.account.url}test-shared/",
)
caldavDao.insert(calendar)
val calendar = CaldavCalendar().apply {
account = this@SharingOwncloudTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingOwncloudTest.account.url}test-shared/"
caldavDao.insert(this)
}
enqueue(OC_OWNER)
sync()
@ -50,12 +49,12 @@ class SharingOwncloudTest : CaldavTest() {
@Test
fun readOnly() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar(
account = this@SharingOwncloudTest.account.uuid,
ctag = "http://sabre.io/ns/sync/2",
url = "${this@SharingOwncloudTest.account.url}test-shared_shared_by_user1/",
)
caldavDao.insert(calendar)
val calendar = CaldavCalendar().apply {
account = this@SharingOwncloudTest.account.uuid
ctag = "http://sabre.io/ns/sync/2"
url = "${this@SharingOwncloudTest.account.url}test-shared_shared_by_user1/"
caldavDao.insert(this)
}
enqueue(OC_READ_ONLY)
sync()
@ -66,12 +65,12 @@ class SharingOwncloudTest : CaldavTest() {
@Test
fun principalForSharee() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar(
account = this@SharingOwncloudTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingOwncloudTest.account.url}test-shared/",
)
caldavDao.insert(calendar)
val calendar = CaldavCalendar().apply {
account = this@SharingOwncloudTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingOwncloudTest.account.url}test-shared/"
caldavDao.insert(this)
}
enqueue(OC_OWNER)
sync()
@ -90,12 +89,12 @@ class SharingOwncloudTest : CaldavTest() {
@Test
fun principalForOwner() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar(
account = this@SharingOwncloudTest.account.uuid,
ctag = "http://sabre.io/ns/sync/2",
url = "${this@SharingOwncloudTest.account.url}test-shared_shared_by_user1/",
)
caldavDao.insert(calendar)
val calendar = CaldavCalendar().apply {
account = this@SharingOwncloudTest.account.uuid
ctag = "http://sabre.io/ns/sync/2"
url = "${this@SharingOwncloudTest.account.url}test-shared_shared_by_user1/"
caldavDao.insert(this)
}
enqueue(OC_READ_ONLY)
sync()

@ -1,18 +1,18 @@
package org.tasks.caldav
import org.tasks.data.UUIDHelper
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.entity.CaldavCalendar.Companion.INVITE_ACCEPTED
import org.tasks.data.dao.PrincipalDao
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.CaldavCalendar.Companion.INVITE_ACCEPTED
import org.tasks.data.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@ -23,25 +23,24 @@ class SharingSabredavTest : CaldavTest() {
@Inject lateinit var principalDao: PrincipalDao
private suspend fun setupAccount(user: String) {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = user,
password = encryption.encrypt("password"),
url = server.url("/calendars/$user/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
account = CaldavAccount().apply {
uuid = UUIDHelper.newUUID()
username = user
password = encryption.encrypt("password")
url = server.url("/calendars/$user/").toString()
id = caldavDao.insert(this)
}
}
@Test
fun calendarOwner() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar(
account = this@SharingSabredavTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingSabredavTest.account.url}940468858232147861/",
)
caldavDao.insert(calendar)
val calendar = CaldavCalendar().apply {
account = this@SharingSabredavTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingSabredavTest.account.url}940468858232147861/"
caldavDao.insert(this)
}
enqueue(SD_OWNER)
sync()
@ -55,12 +54,12 @@ class SharingSabredavTest : CaldavTest() {
@Test
fun calendarSharee() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar(
account = this@SharingSabredavTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingSabredavTest.account.url}c3853d69-cb7a-476c-a23b-30ffd70f110b/",
)
caldavDao.insert(calendar)
val calendar = CaldavCalendar().apply {
account = this@SharingSabredavTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingSabredavTest.account.url}c3853d69-cb7a-476c-a23b-30ffd70f110b/"
caldavDao.insert(this)
}
enqueue(SD_SHAREE)
sync()
@ -74,13 +73,12 @@ class SharingSabredavTest : CaldavTest() {
@Test
fun excludeCurrentUserPrincipalFromSharees() = runBlocking {
setupAccount("user1")
caldavDao.insert(
CaldavCalendar(
account = account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${account.url}940468858232147861/",
)
)
CaldavCalendar().apply {
account = this@SharingSabredavTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingSabredavTest.account.url}940468858232147861/"
caldavDao.insert(this)
}
enqueue(SD_OWNER)
sync()
@ -91,12 +89,12 @@ class SharingSabredavTest : CaldavTest() {
@Test
fun principalForSharee() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar(
account = this@SharingSabredavTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingSabredavTest.account.url}940468858232147861/",
)
caldavDao.insert(calendar)
val calendar = CaldavCalendar().apply {
account = this@SharingSabredavTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingSabredavTest.account.url}940468858232147861/"
caldavDao.insert(this)
}
enqueue(SD_OWNER)
sync()
@ -113,12 +111,12 @@ class SharingSabredavTest : CaldavTest() {
@Test
fun principalForOwner() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar(
account = this@SharingSabredavTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingSabredavTest.account.url}c3853d69-cb7a-476c-a23b-30ffd70f110b/",
)
caldavDao.insert(calendar)
val calendar = CaldavCalendar().apply {
account = this@SharingSabredavTest.account.uuid
ctag = "http://sabre.io/ns/sync/1"
url = "${this@SharingSabredavTest.account.url}c3853d69-cb7a-476c-a23b-30ffd70f110b/"
caldavDao.insert(this)
}
enqueue(SD_SHAREE)
sync()

@ -2,6 +2,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.andlib.utility.DateUtilities.now
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
@ -9,15 +10,11 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskContainerMaker
import org.tasks.makers.TaskContainerMaker.CREATED
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@ -104,24 +101,22 @@ class CaldavDaoShiftTests : InjectingTestCase() {
fun ignoreMovedTasksWhenShiftingDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created))
caldavDao.update(caldavDao.getTask(tasks[0].id).apply { this?.deleted =
currentTimeMillis()
}!!)
caldavDao.update(caldavDao.getTask(tasks[0].id).apply { this?.deleted = now() }!!)
caldavDao.shiftDown("calendar", 0, created.toAppleEpoch())
assertNull(taskDao.fetch(tasks[0].id)!!.order)
assertNull(caldavDao.getTasks(tasks[0].id)[0].order)
}
@Test
fun ignoreDeletedTasksWhenShiftingDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created))
taskDao.update(taskDao.fetch(tasks[0].id).apply { this?.deletionDate = currentTimeMillis() }!!)
taskDao.update(taskDao.fetch(tasks[0].id).apply { this?.deletionDate = now() }!!)
caldavDao.shiftDown("calendar", 0, created.toAppleEpoch())
assertNull(taskDao.fetch(tasks[0].id)!!.order)
assertNull(caldavDao.getTasks(tasks[0].id)[0].order)
}
@Test
@ -139,11 +134,10 @@ class CaldavDaoShiftTests : InjectingTestCase() {
}
private suspend fun checkOrder(dateTime: DateTime?, task: TaskContainer) {
val order = taskDao.fetch(task.id)!!.order
if (dateTime == null) {
assertNull(order)
assertNull(caldavDao.getTask(task.id)!!.order)
} else {
assertEquals(dateTime.toAppleEpoch(), order)
assertEquals(dateTime.toAppleEpoch(), caldavDao.getTask(task.id)!!.order)
}
}
@ -151,18 +145,22 @@ class CaldavDaoShiftTests : InjectingTestCase() {
private suspend fun addTask(calendar: String, vararg properties: PropertyValue<in TaskContainer?, *>) {
val t = TaskContainerMaker.newTaskContainer(*properties)
tasks.add(t)
val task = t.task
taskDao.createNew(task)
val caldavTask = CaldavTask(task = t.id, calendar = calendar)
val caldavTask = CaldavTask(t.id, calendar)
if (task.parent > 0) {
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent)
}
tasks.add(
t.copy(
caldavTask = caldavTask.copy(
id = caldavDao.insert(caldavTask)
)
)
)
caldavTask.id = caldavDao.insert(caldavTask)
t.caldavTask = caldavTask.toSubset()
}
}
private fun CaldavTask.toSubset(): SubsetCaldav {
val result = SubsetCaldav()
result.cd_id = id
result.cd_calendar = calendar
result.cd_remote_parent = remoteParent
return result
}
}

@ -2,18 +2,12 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.*
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TagDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.CREATION_TIME
@ -33,7 +27,8 @@ class CaldavDaoTests : InjectingTestCase() {
fun insertNewTaskAtTopOfEmptyList() = runBlocking {
val task = newTask()
taskDao.createNew(task)
caldavDao.insert(task, CaldavTask(task = task.id, calendar = "calendar"), true)
val caldavTask = CaldavTask(task.id, "calendar")
caldavDao.insert(task, caldavTask, true)
checkOrder(null, task.id)
}
@ -45,8 +40,9 @@ class CaldavDaoTests : InjectingTestCase() {
val second = newTask(with(CREATION_TIME, created.plusSeconds(1)))
taskDao.createNew(first)
taskDao.createNew(second)
caldavDao.insert(first, CaldavTask(task = first.id, calendar = "calendar"), true)
caldavDao.insert(second, CaldavTask(task = second.id, calendar = "calendar"), true)
caldavDao.insert(first, CaldavTask(first.id, "calendar"), true)
caldavDao.insert(second, CaldavTask(second.id, "calendar"), true)
checkOrder(null, first.id)
checkOrder(created.minusSeconds(1), second.id)
@ -59,8 +55,9 @@ class CaldavDaoTests : InjectingTestCase() {
val second = newTask(with(CREATION_TIME, created.plusSeconds(1)))
taskDao.createNew(first)
taskDao.createNew(second)
caldavDao.insert(first, CaldavTask(task = first.id, calendar = "calendar"), false)
caldavDao.insert(second, CaldavTask(task = second.id, calendar = "calendar"), false)
caldavDao.insert(first, CaldavTask(first.id, "calendar"), false)
caldavDao.insert(second, CaldavTask(second.id, "calendar"), false)
checkOrder(null, first.id)
checkOrder(null, second.id)
@ -73,8 +70,9 @@ class CaldavDaoTests : InjectingTestCase() {
val second = newTask(with(CREATION_TIME, created))
taskDao.createNew(first)
taskDao.createNew(second)
caldavDao.insert(first, CaldavTask(task = first.id, calendar = "calendar"), false)
caldavDao.insert(second, CaldavTask(task = second.id, calendar = "calendar"), false)
caldavDao.insert(first, CaldavTask(first.id, "calendar"), false)
caldavDao.insert(second, CaldavTask(second.id, "calendar"), false)
checkOrder(null, first.id)
checkOrder(created.plusSeconds(1), second.id)
@ -84,14 +82,16 @@ class CaldavDaoTests : InjectingTestCase() {
fun insertNewTaskAtBottomOfEmptyList() = runBlocking {
val task = newTask()
taskDao.createNew(task)
caldavDao.insert(task, CaldavTask(task = task.id, calendar = "calendar"), false)
val caldavTask = CaldavTask(task.id, "calendar")
caldavDao.insert(task, caldavTask, false)
checkOrder(null, task.id)
}
@Test
fun noResultsForEmptyAccounts() = runBlocking {
val caldavAccount = CaldavAccount(uuid = UUIDHelper.newUUID())
val caldavAccount = CaldavAccount()
caldavAccount.uuid = UUIDHelper.newUUID()
caldavDao.insert(caldavAccount)
assertTrue(caldavDao.getCaldavFilters(caldavAccount.uuid!!).isEmpty())
}
@ -99,11 +99,11 @@ class CaldavDaoTests : InjectingTestCase() {
private suspend fun checkOrder(dateTime: DateTime, task: Long) = checkOrder(dateTime.toAppleEpoch(), task)
private suspend fun checkOrder(order: Long?, task: Long) {
val sortOrder = taskDao.fetch(task)!!.order
val sortOrder = caldavDao.getTask(task)!!.order
if (order == null) {
assertNull(sortOrder)
} else {
assertEquals(order, sortOrder)
}
}
}
}

@ -2,18 +2,13 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.*
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.CaldavDao.Companion.LOCAL
import org.tasks.data.dao.DeletionDao
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavTask
import org.tasks.data.CaldavDao.Companion.LOCAL
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
@ -21,7 +16,7 @@ import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.DateTimeUtils
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@ -48,7 +43,7 @@ class DeletionDaoTests : InjectingTestCase() {
deletionDao.markDeleted(listOf(task.id))
task = taskDao.fetch(task.id)!!
assertTrue(task.modificationDate > task.creationDate)
assertTrue(task.modificationDate < currentTimeMillis())
assertTrue(task.modificationDate < DateTimeUtils.currentTimeMillis())
}
@Test
@ -58,15 +53,15 @@ class DeletionDaoTests : InjectingTestCase() {
deletionDao.markDeleted(listOf(task.id))
task = taskDao.fetch(task.id)!!
assertTrue(task.deletionDate > task.creationDate)
assertTrue(task.deletionDate < currentTimeMillis())
assertTrue(task.deletionDate < DateTimeUtils.currentTimeMillis())
}
@Test
fun purgeDeletedLocalTask() = runBlocking {
val task = newTask(with(DELETION_TIME, newDateTime()))
taskDao.createNew(task)
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = LOCAL))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
caldavDao.insert(CaldavCalendar("", "1234").apply { account = LOCAL })
caldavDao.insert(CaldavTask(task.id, "1234"))
deletionDao.purgeDeleted()
@ -77,8 +72,8 @@ class DeletionDaoTests : InjectingTestCase() {
fun dontPurgeActiveTasks() = runBlocking {
val task = newTask()
taskDao.createNew(task)
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = LOCAL))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
caldavDao.insert(CaldavCalendar("", "1234").apply { account = LOCAL })
caldavDao.insert(CaldavTask(task.id, "1234"))
deletionDao.purgeDeleted()
@ -89,11 +84,11 @@ class DeletionDaoTests : InjectingTestCase() {
fun dontPurgeDeletedCaldavTask() = runBlocking {
val task = newTask(with(DELETION_TIME, newDateTime()))
taskDao.createNew(task)
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = UUIDHelper.newUUID()))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
caldavDao.insert(CaldavCalendar("", "1234").apply { account = UUIDHelper.newUUID() })
caldavDao.insert(CaldavTask(task.id, "1234"))
deletionDao.purgeDeleted()
assertNotNull(taskDao.fetch(task.id))
}
}
}

@ -9,20 +9,15 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.GoogleTaskMaker.LIST
import org.tasks.makers.GoogleTaskMaker.PARENT
import org.tasks.makers.GoogleTaskMaker.REMOTE_ID
import org.tasks.makers.GoogleTaskMaker.REMOTE_PARENT
import org.tasks.makers.GoogleTaskMaker.TASK
import org.tasks.makers.GoogleTaskMaker.newGoogleTask
import org.tasks.makers.GtaskListMaker.newGtaskList
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@ -32,135 +27,140 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
@Before
override fun setUp() {
super.setUp()
runBlocking {
caldavDao.insert(CaldavAccount(uuid = "account", accountType = TYPE_GOOGLE_TASKS))
caldavDao.insert(CaldavCalendar(account = "account", uuid = "calendar"))
googleTaskListDao.insert(newGtaskList())
}
}
@Test
fun insertAtTopOfEmptyList() = runBlocking {
insertTop(newCaldavTask(with(REMOTE_ID, "1234")))
val tasks = googleTaskDao.getByLocalOrder("calendar")
insertTop(newGoogleTask(with(REMOTE_ID, "1234")))
val tasks = googleTaskDao.getByLocalOrder("1")
assertEquals(1, tasks.size.toLong())
val task = tasks[0]
assertEquals("1234", googleTaskDao.getByTaskId(task.id)?.remoteId)
assertEquals(0L, task.order)
assertEquals("1234", task.remoteId)
assertEquals(0, task.order)
}
@Test
fun insertAtBottomOfEmptyList() = runBlocking {
insertBottom(newCaldavTask(with(REMOTE_ID, "1234")))
val tasks = googleTaskDao.getByLocalOrder("calendar")
insertBottom(newGoogleTask(with(REMOTE_ID, "1234")))
val tasks = googleTaskDao.getByLocalOrder("1")
assertEquals(1, tasks.size.toLong())
val task = tasks[0]
assertEquals("1234", googleTaskDao.getByTaskId(task.id)?.remoteId)
assertEquals(0L, task.order)
assertEquals("1234", task.remoteId)
assertEquals(0, task.order)
}
@Test
fun getPreviousIsNullForTopTask() = runBlocking {
insert(newCaldavTask())
googleTaskDao.insertAndShift(newGoogleTask(), true)
assertNull(googleTaskDao.getPrevious("1", 0, 0))
}
@Test
fun getPrevious() = runBlocking {
insertTop(newCaldavTask())
insertTop(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals("1234", googleTaskDao.getPrevious("calendar", 0, 1))
insertTop(newGoogleTask())
insertTop(newGoogleTask(with(REMOTE_ID, "1234")))
assertEquals("1234", googleTaskDao.getPrevious("1", 0, 1))
}
@Test
fun insertAtTopOfList() = runBlocking {
insertTop(newCaldavTask(with(REMOTE_ID, "1234")))
insertTop(newCaldavTask(with(REMOTE_ID, "5678")))
val tasks = googleTaskDao.getByLocalOrder("calendar")
insertTop(newGoogleTask(with(REMOTE_ID, "1234")))
insertTop(newGoogleTask(with(REMOTE_ID, "5678")))
val tasks = googleTaskDao.getByLocalOrder("1")
assertEquals(2, tasks.size.toLong())
val top = tasks[0]
assertEquals("5678", googleTaskDao.getByTaskId(top.id)?.remoteId)
assertEquals(0L, top.order)
assertEquals("5678", top.remoteId)
assertEquals(0, top.order)
}
@Test
fun insertAtTopOfListShiftsExisting() = runBlocking {
insertTop(newCaldavTask(with(REMOTE_ID, "1234")))
insertTop(newCaldavTask(with(REMOTE_ID, "5678")))
val tasks = googleTaskDao.getByLocalOrder("calendar")
insertTop(newGoogleTask(with(REMOTE_ID, "1234")))
insertTop(newGoogleTask(with(REMOTE_ID, "5678")))
val tasks = googleTaskDao.getByLocalOrder("1")
assertEquals(2, tasks.size.toLong())
val bottom = tasks[1]
assertEquals("1234", googleTaskDao.getByTaskId(bottom.id)?.remoteId)
assertEquals(1L, bottom.order)
assertEquals("1234", bottom.remoteId)
assertEquals(1, bottom.order)
}
@Test
fun getTaskFromRemoteId() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals(1L, googleTaskDao.getTask("1234"))
googleTaskDao.insert(newGoogleTask(with(REMOTE_ID, "1234"), with(TASK, 4)))
assertEquals(4L, googleTaskDao.getTask("1234"))
}
@Test
fun getRemoteIdForTask() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals("1234", googleTaskDao.getRemoteId(1L))
googleTaskDao.insert(newGoogleTask(with(REMOTE_ID, "1234"), with(TASK, 4)))
assertEquals("1234", googleTaskDao.getRemoteId(4L))
}
@Test
fun moveDownInList() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1")))
insert(newCaldavTask(with(REMOTE_ID, "2")))
insert(newCaldavTask(with(REMOTE_ID, "3")))
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false)
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false)
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false)
val two = getByRemoteId("2")
googleTaskDao.move(taskDao.fetch(two.task)!!, "calendar", 0, 0)
assertEquals(0L, getOrder("2"))
assertEquals(1L, getOrder("1"))
assertEquals(2L, getOrder("3"))
googleTaskDao.move(two, 0, 0)
assertEquals(0, googleTaskDao.getByRemoteId("2")!!.order)
assertEquals(1, googleTaskDao.getByRemoteId("1")!!.order)
assertEquals(2, googleTaskDao.getByRemoteId("3")!!.order)
}
@Test
fun moveUpInList() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1")))
insert(newCaldavTask(with(REMOTE_ID, "2")))
insert(newCaldavTask(with(REMOTE_ID, "3")))
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false)
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false)
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false)
val one = getByRemoteId("1")
googleTaskDao.move(taskDao.fetch(one.task)!!, "calendar", 0, 1)
assertEquals(0L, getOrder("2"))
assertEquals(1L, getOrder("1"))
assertEquals(2L, getOrder("3"))
googleTaskDao.move(one, 0, 1)
assertEquals(0, googleTaskDao.getByRemoteId("2")!!.order)
assertEquals(1, googleTaskDao.getByRemoteId("1")!!.order)
assertEquals(2, googleTaskDao.getByRemoteId("3")!!.order)
}
@Test
fun moveToTop() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1")))
insert(newCaldavTask(with(REMOTE_ID, "2")))
insert(newCaldavTask(with(REMOTE_ID, "3")))
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false)
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false)
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false)
val three = getByRemoteId("3")
googleTaskDao.move(taskDao.fetch(three.task)!!, "calendar", 0, 0)
assertEquals(0L, getOrder("3"))
assertEquals(1L, getOrder("1"))
assertEquals(2L, getOrder("2"))
googleTaskDao.move(three, 0, 0)
assertEquals(0, googleTaskDao.getByRemoteId("3")!!.order)
assertEquals(1, googleTaskDao.getByRemoteId("1")!!.order)
assertEquals(2, googleTaskDao.getByRemoteId("2")!!.order)
}
@Test
fun moveToBottom() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1")))
insert(newCaldavTask(with(REMOTE_ID, "2")))
insert(newCaldavTask(with(REMOTE_ID, "3")))
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false)
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false)
googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false)
val one = getByRemoteId("1")
googleTaskDao.move(taskDao.fetch(one.task)!!, "calendar", 0, 2)
assertEquals(0L, getOrder("2"))
assertEquals(1L, getOrder("3"))
assertEquals(2L, getOrder("1"))
googleTaskDao.move(one, 0, 2)
assertEquals(0, googleTaskDao.getByRemoteId("2")!!.order)
assertEquals(1, googleTaskDao.getByRemoteId("3")!!.order)
assertEquals(2, googleTaskDao.getByRemoteId("1")!!.order)
}
@Test
fun findChildrenInList() = runBlocking {
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(PARENT, 1L)))
assertEquals(listOf(2L), googleTaskDao.getChildren(listOf(1L, 2L)))
}
@Test
fun dontAllowEmptyParent() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "1234")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "1234")))
googleTaskDao.updatePosition("1234", "", "0")
@ -169,7 +169,7 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Test
fun updatePositionWithNullParent() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "1234")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "1234")))
googleTaskDao.updatePosition("1234", null, "0")
@ -178,7 +178,7 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Test
fun updatePosition() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "1234")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "1234")))
googleTaskDao.updatePosition("1234", "abcd", "0")
@ -187,87 +187,86 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Test
fun updateParents() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_PARENT, "123")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "123")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(REMOTE_PARENT, "123")))
caldavDao.updateParents()
googleTaskDao.updateParents()
assertEquals(1, taskDao.fetch(2)!!.parent)
assertEquals(1, googleTaskDao.getByTaskId(2)!!.parent)
}
@Test
fun updateParentsByList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_PARENT, "123")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "123")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(REMOTE_PARENT, "123")))
caldavDao.updateParents("calendar")
googleTaskDao.updateParents("1")
assertEquals(1, taskDao.fetch(2)!!.parent)
assertEquals(1, googleTaskDao.getByTaskId(2)!!.parent)
}
@Test
fun updateParentsMustMatchList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "2"), with(REMOTE_PARENT, "123")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "123")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "2"), with(REMOTE_PARENT, "123")))
caldavDao.updateParents()
googleTaskDao.updateParents()
assertEquals(0, taskDao.fetch(2)!!.parent)
assertEquals(0, googleTaskDao.getByTaskId(2)!!.parent)
}
@Test
fun updateParentsByListMustMatchList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "2"), with(REMOTE_PARENT, "123")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "123")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "2"), with(REMOTE_PARENT, "123")))
caldavDao.updateParents("2")
googleTaskDao.updateParents("2")
assertEquals(0, taskDao.fetch(2)!!.parent)
assertEquals(0, googleTaskDao.getByTaskId(2)!!.parent)
}
@Test
fun ignoreEmptyStringWhenUpdatingParents() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_ID, ""), with(REMOTE_PARENT, "")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(REMOTE_ID, ""), with(REMOTE_PARENT, "")))
caldavDao.updateParents()
googleTaskDao.updateParents()
assertEquals(0, taskDao.fetch(2)!!.parent)
assertEquals(0, googleTaskDao.getByTaskId(2)!!.parent)
}
@Test
fun ignoreEmptyStringWhenUpdatingParentsForList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_ID, ""), with(REMOTE_PARENT, "")))
googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "")))
googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(REMOTE_ID, ""), with(REMOTE_PARENT, "")))
caldavDao.updateParents("1")
assertEquals(0, taskDao.fetch(2)!!.parent)
}
googleTaskDao.updateParents("1")
private suspend fun getOrder(remoteId: String): Long? {
return taskDao.fetch(googleTaskDao.getByRemoteId(remoteId)!!.task)?.order
assertEquals(0, googleTaskDao.getByTaskId(2)!!.parent)
}
private suspend fun insertTop(googleTask: CaldavTask) {
private suspend fun insertTop(googleTask: GoogleTask) {
insert(googleTask, true)
}
private suspend fun insertBottom(googleTask: CaldavTask) {
private suspend fun insertBottom(googleTask: GoogleTask) {
insert(googleTask, false)
}
private suspend fun insert(googleTask: CaldavTask, top: Boolean = false) {
private suspend fun insert(googleTask: GoogleTask, top: Boolean) {
val task = newTask()
taskDao.createNew(task)
googleTaskDao.insertAndShift(
task,
googleTask.copy(task = task.id),
top
)
googleTask.task = task.id
googleTaskDao.insertAndShift(googleTask, top)
}
private suspend fun getByRemoteId(remoteId: String): CaldavTask {
return googleTaskDao.getByRemoteId(remoteId)!!
private suspend fun getByRemoteId(remoteId: String): SubsetGoogleTask {
val googleTask = googleTaskDao.getByRemoteId(remoteId)!!
val result = SubsetGoogleTask()
result.gt_id = googleTask.id
result.gt_list_id = googleTask.listId
result.gt_order = googleTask.order
result.gt_parent = googleTask.parent
return result
}
}

@ -1,31 +1,53 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue
import org.junit.Assert.*
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.GoogleTaskListMaker.ACCOUNT
import org.tasks.makers.GoogleTaskListMaker.REMOTE_ID
import org.tasks.makers.GoogleTaskListMaker.newGoogleTaskList
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class GoogleTaskListDaoTest : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var caldavDao: CaldavDao
@Test
fun noResultsForEmptyAccount() = runBlocking {
val account = CaldavAccount(
uuid = "user@gmail.com",
username = "user@gmail.com",
)
caldavDao.insert(account)
val account = GoogleTaskAccount()
account.account = "user@gmail.com"
googleTaskListDao.insert(account)
assertTrue(googleTaskListDao.getGoogleTaskFilters(account.username!!).isEmpty())
assertTrue(googleTaskListDao.getGoogleTaskFilters(account.account!!).isEmpty())
}
@Test
fun findListWithNullAccount() = runBlocking {
val list = newGoogleTaskList(with(REMOTE_ID, "1234"), with(ACCOUNT, null as String?))
list.id = googleTaskListDao.insert(list)
assertEquals(list, googleTaskListDao.findExistingList("1234"))
}
@Test
fun findListWithEmptyAccount() = runBlocking {
val list = newGoogleTaskList(with(REMOTE_ID, "1234"), with(ACCOUNT, ""))
list.id = googleTaskListDao.insert(list)
assertEquals(list, googleTaskListDao.findExistingList("1234"))
}
@Test
fun ignoreListWithAccount() = runBlocking {
val list = newGoogleTaskList(with(REMOTE_ID, "1234"), with(ACCOUNT, "user@gmail.com"))
googleTaskListDao.insert(list)
assertNull(googleTaskListDao.findExistingList("1234"))
}
}

@ -1,34 +1,34 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Assert.*
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.LocationDao
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.Geofence
import org.tasks.data.entity.Place
import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.GeofenceMaker.ARRIVAL
import org.tasks.makers.GeofenceMaker.DEPARTURE
import org.tasks.makers.GeofenceMaker.PLACE
import org.tasks.makers.GeofenceMaker.TASK
import org.tasks.makers.GeofenceMaker.newGeofence
import org.tasks.makers.PlaceMaker.LATITUDE
import org.tasks.makers.PlaceMaker.LONGITUDE
import org.tasks.makers.PlaceMaker.newPlace
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.HIDE_TYPE
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@ -36,11 +36,10 @@ import javax.inject.Inject
class LocationDaoTest : InjectingTestCase() {
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var alarmDao: AlarmDao
@Test
fun getExistingPlace() = runBlocking {
locationDao.insert(Place(latitude = 48.067222, longitude = 12.863611))
locationDao.insert(newPlace(with(LATITUDE, 48.067222), with(LONGITUDE, 12.863611)))
val place = locationDao.findPlace(48.067222.toLikeString(), 12.863611.toLikeString())
assertEquals(48.067222, place?.latitude)
assertEquals(12.863611, place?.longitude)
@ -48,7 +47,7 @@ class LocationDaoTest : InjectingTestCase() {
@Test
fun getPlaceWithLessPrecision() = runBlocking {
locationDao.insert(Place(latitude = 50.7547, longitude = -2.2279))
locationDao.insert(newPlace(with(LATITUDE, 50.7547), with(LONGITUDE, -2.2279)))
val place = locationDao.findPlace(50.754712.toLikeString(), (-2.227945).toLikeString())
assertEquals(50.7547, place?.latitude)
assertEquals(-2.2279, place?.longitude)
@ -56,7 +55,7 @@ class LocationDaoTest : InjectingTestCase() {
@Test
fun getPlaceWithMorePrecision() = runBlocking {
locationDao.insert(Place(latitude = 36.246944, longitude = -116.816944))
locationDao.insert(newPlace(with(LATITUDE, 36.246944), with(LONGITUDE, -116.816944)))
locationDao.getPlaces().forEach { println(it) }
val place = locationDao.findPlace(36.2469.toLikeString(), (-116.8169).toLikeString())
assertEquals(36.246944, place?.latitude)
@ -65,20 +64,20 @@ class LocationDaoTest : InjectingTestCase() {
@Test
fun noActiveGeofences() = runBlocking {
val place = Place()
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1)))
locationDao.insert(Geofence(task = 1, place = place.uid))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid)))
assertNull(locationDao.getGeofencesByPlace(place.uid!!))
}
@Test
fun activeArrivalGeofence() = runBlocking {
val place = Place()
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1)))
locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
val geofence = locationDao.getGeofencesByPlace(place.uid!!)
@ -88,10 +87,10 @@ class LocationDaoTest : InjectingTestCase() {
@Test
fun activeDepartureGeofence() = runBlocking {
val place = Place()
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1)))
locationDao.insert(Geofence(task = 1, place = place.uid, isDeparture = true))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true)))
val geofence = locationDao.getGeofencesByPlace(place.uid!!)
@ -101,171 +100,133 @@ class LocationDaoTest : InjectingTestCase() {
@Test
fun geofenceInactiveForCompletedTask() = runBlocking {
val place = Place()
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(COMPLETION_TIME, newDateTime())))
locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
assertNull(locationDao.getGeofencesByPlace(place.uid!!))
}
@Test
fun geofenceInactiveForDeletedTask() = runBlocking {
val place = Place()
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(DELETION_TIME, newDateTime())))
locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
assertNull(locationDao.getGeofencesByPlace(place.uid!!))
}
@Test
fun ignoreArrivalForSnoozedTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
val task = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = task,
time = newDateTime().plusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
locationDao.insert(Geofence(task = task, place = place.uid, isArrival = true))
assertTrue(locationDao.getArrivalGeofences(place.uid!!, currentTimeMillis()).isEmpty())
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().plusMinutes(15))))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
assertTrue(locationDao.getArrivalGeofences(place.uid!!, now()).isEmpty())
}
}
@Test
fun ignoreDepartureForSnoozedTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
val task = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = task,
time = newDateTime().plusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
locationDao.insert(Geofence(task = task, place = place.uid, isDeparture = true))
assertTrue(locationDao.getDepartureGeofences(place.uid!!, currentTimeMillis()).isEmpty())
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().plusMinutes(15))))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true)))
assertTrue(locationDao.getDepartureGeofences(place.uid!!, now()).isEmpty())
}
}
@Test
fun getArrivalWithElapsedSnooze() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
val task = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = task,
time = newDateTime().minusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
val geofence = Geofence(task = task, place = place.uid, isArrival = true)
.let { it.copy(id = locationDao.insert(it)) }
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!,
currentTimeMillis()
))
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().minusMinutes(15))))
val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true))
geofence.id = locationDao.insert(geofence)
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!, now()))
}
}
@Test
fun getDepartureWithElapsedSnooze() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
val task = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = task,
time = newDateTime().minusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
val geofence = Geofence(task = task, place = place.uid, isDeparture = true)
.let { it.copy(id = locationDao.insert(it)) }
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!,
currentTimeMillis()
))
taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().minusMinutes(15))))
val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true))
geofence.id = locationDao.insert(geofence)
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!, now()))
}
}
@Test
fun ignoreArrivalForHiddenTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(
with(ID, 1),
with(DUE_TIME, newDateTime().plusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
assertTrue(locationDao.getArrivalGeofences(place.uid!!, currentTimeMillis()).isEmpty())
assertTrue(locationDao.getArrivalGeofences(place.uid!!, now()).isEmpty())
}
}
@Test
fun ignoreDepartureForHiddenTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(
with(ID, 1),
with(DUE_TIME, newDateTime().plusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
locationDao.insert(Geofence(task = 1, place = place.uid, isDeparture = true))
locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true)))
assertTrue(locationDao.getDepartureGeofences(place.uid!!, currentTimeMillis()).isEmpty())
assertTrue(locationDao.getDepartureGeofences(place.uid!!, now()).isEmpty())
}
}
@Test
fun getArrivalWithElapsedHideUntil() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(
with(ID, 1),
with(DUE_TIME, newDateTime().minusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
val geofence = Geofence(task = 1, place = place.uid, isArrival = true)
.let {
it.copy(id = locationDao.insert(it))
}
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!,
currentTimeMillis()
))
val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true))
geofence.id = locationDao.insert(geofence)
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!, now()))
}
}
@Test
fun getDepartureWithElapsedHideUntil() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
freezeAt(now()).thawAfter {
val place = newPlace()
locationDao.insert(place)
taskDao.createNew(newTask(
with(ID, 1),
with(DUE_TIME, newDateTime().minusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
val geofence = Geofence(task = 1, place = place.uid, isDeparture = true)
.let { it.copy(id = locationDao.insert(it)) }
val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true))
geofence.id = locationDao.insert(geofence)
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!,
currentTimeMillis()
))
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!, now()))
}
}
}

@ -1,51 +1,45 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.filters.GtasksFilter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.R
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.GoogleTaskListMaker.REMOTE_ID
import org.tasks.makers.GoogleTaskListMaker.newGoogleTaskList
import org.tasks.makers.GoogleTaskMaker.LIST
import org.tasks.makers.GoogleTaskMaker.ORDER
import org.tasks.makers.GoogleTaskMaker.PARENT
import org.tasks.makers.GoogleTaskMaker.TASK
import org.tasks.makers.GoogleTaskMaker.newGoogleTask
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.ORDER
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.UUID
import org.tasks.preferences.Preferences
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class ManualGoogleTaskQueryTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
private lateinit var filter: GtasksFilter
private val filter: GtasksFilter = GtasksFilter(newGoogleTaskList(with(REMOTE_ID, "1234")))
@Before
override fun setUp() {
super.setUp()
preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true)
val calendar = CaldavCalendar(uuid = "1234")
runBlocking {
caldavDao.insert(CaldavAccount())
caldavDao.insert(calendar)
}
filter = GtasksFilter(calendar)
}
@Test
@ -94,17 +88,23 @@ class ManualGoogleTaskQueryTest : InjectingTestCase() {
assertEquals(1, subtasks[2].secondarySort)
}
@Test
fun ignoreDisableSubtasksPreference() = runBlocking {
preferences.setBoolean(R.string.p_use_paged_queries, true)
newTask(1, 0, 0)
newTask(2, 0, 1)
val parent = query()[0]
assertTrue(parent.hasChildren())
}
private suspend fun newTask(id: Long, order: Long, parent: Long = 0) {
taskDao.insert(TaskMaker.newTask(
with(ID, id),
with(TaskMaker.UUID, UUIDHelper.newUUID()),
with(ORDER, order),
with(PARENT, parent),
))
googleTaskDao.insert(newCaldavTask(with(CALENDAR, filter.list.uuid), with(TASK, id)))
taskDao.insert(TaskMaker.newTask(with(ID, id), with(UUID, UUIDHelper.newUUID())))
googleTaskDao.insert(newGoogleTask(with(LIST, filter.list.remoteId), with(TASK, id), with(PARENT, parent), with(ORDER, order)))
}
private suspend fun query(): List<TaskContainer> = taskDao.fetchTasks {
TaskListQuery.getQuery(preferences, filter)
TaskListQuery.getQuery(preferences, filter, it)
}
}

@ -8,12 +8,14 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.TagDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TagDataMaker.NAME
import org.tasks.makers.TagDataMaker.newTagData
import org.tasks.makers.TagMaker.TAGDATA
import org.tasks.makers.TagMaker.TAGUID
import org.tasks.makers.TagMaker.TASK
import org.tasks.makers.TagMaker.newTag
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@ -27,13 +29,13 @@ class TagDataDaoTest : InjectingTestCase() {
@Test
fun tagDataOrderedByNameIgnoresNullNames() = runBlocking {
tagDataDao.insert(TagData(name = null))
tagDataDao.createNew(newTagData(with(NAME, null as String?)))
assertTrue(tagDataDao.tagDataOrderedByName().isEmpty())
}
@Test
fun tagDataOrderedByNameIgnoresEmptyNames() = runBlocking {
tagDataDao.insert(TagData(name = ""))
tagDataDao.createNew(newTagData(with(NAME, "")))
assertTrue(tagDataDao.tagDataOrderedByName().isEmpty())
}
@ -44,19 +46,20 @@ class TagDataDaoTest : InjectingTestCase() {
@Test
fun getTagWithCaseFixesCase() = runBlocking {
tagDataDao.insert(TagData(name = "Derp"))
tagDataDao.createNew(newTagData(with(NAME, "Derp")))
assertEquals("Derp", tagDataDao.getTagWithCase("derp"))
}
@Test
fun getTagsByName() = runBlocking {
val tagData = TagData(name = "Derp").let { it.copy(id = tagDataDao.insert(it)) }
val tagData = newTagData(with(NAME, "Derp"))
tagDataDao.createNew(tagData)
assertEquals(listOf(tagData), tagDataDao.getTags(listOf("Derp")))
}
@Test
fun getTagsByNameCaseSensitive() = runBlocking {
tagDataDao.insert(TagData(name = "Derp"))
tagDataDao.createNew(newTagData(with(NAME, "Derp")))
assertTrue(tagDataDao.getTags(listOf("derp")).isEmpty())
}
@ -66,18 +69,20 @@ class TagDataDaoTest : InjectingTestCase() {
val taskTwo = newTask()
taskDao.createNew(taskOne)
taskDao.createNew(taskTwo)
val tagOne = TagData(name = "one").let { it.copy(id = tagDataDao.insert(it)) }
val tagTwo = TagData(name = "two").let { it.copy(id = tagDataDao.insert(it)) }
tagDao.insert(Tag(task = taskOne.id, taskUid = taskOne.uuid, tagUid = tagOne.remoteId))
tagDao.insert(Tag(task = taskTwo.id, taskUid = taskTwo.uuid, tagUid = tagTwo.remoteId))
val tagOne = newTagData(with(NAME, "one"))
val tagTwo = newTagData(with(NAME, "two"))
tagDataDao.createNew(tagOne)
tagDataDao.createNew(tagTwo)
tagDao.insert(newTag(with(TAGDATA, tagOne), with(TASK, taskOne)))
tagDao.insert(newTag(with(TAGDATA, tagTwo), with(TASK, taskTwo)))
assertEquals(listOf(tagOne), tagDataDao.getTagDataForTask(taskOne.id))
}
@Test
fun getEmptyTagSelections() = runBlocking {
val selections = tagDataDao.getTagSelections(listOf(1L))
assertTrue(selections.first.isEmpty())
assertTrue(selections.second.isEmpty())
assertTrue(selections.first!!.isEmpty())
assertTrue(selections.second!!.isEmpty())
}
@Test
@ -92,7 +97,7 @@ class TagDataDaoTest : InjectingTestCase() {
fun getEmptyPartialSelections() = runBlocking {
newTag(1, "tag1")
newTag(2, "tag1")
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).first.isEmpty())
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).first!!.isEmpty())
}
@Test
@ -106,15 +111,15 @@ class TagDataDaoTest : InjectingTestCase() {
fun getEmptyCommonSelections() = runBlocking {
newTag(1, "tag1")
newTag(2, "tag2")
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).second.isEmpty())
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).second!!.isEmpty())
}
@Test
fun getSelectionsWithNoTags() = runBlocking {
newTag(1)
val selections = tagDataDao.getTagSelections(listOf(1L))
assertTrue(selections.first.isEmpty())
assertTrue(selections.second.isEmpty())
assertTrue(selections.first!!.isEmpty())
assertTrue(selections.second!!.isEmpty())
}
@Test
@ -123,14 +128,14 @@ class TagDataDaoTest : InjectingTestCase() {
newTag(2)
val selections = tagDataDao.getTagSelections(listOf(1L, 2L))
assertEquals(setOf("tag1"), selections.first)
assertTrue(selections.second.isEmpty())
assertTrue(selections.second!!.isEmpty())
}
private suspend fun newTag(taskId: Long, vararg tags: String) {
val task = newTask(with(ID, taskId))
taskDao.createNew(task)
for (tag in tags) {
tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = tag))
tagDao.insert(newTag(with(TASK, task), with(TAGUID, tag)))
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save