Compare commits

..

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

@ -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 .idea
*.iml *.iml
.gradle .gradle
@ -10,4 +9,3 @@ local.properties
Thumbs.db Thumbs.db
/captures/ /captures/
/fastlane/report.xml /fastlane/report.xml
/compose-metrics/

@ -1 +1 @@
3.3.4 3.0.3

@ -1,421 +1,555 @@
### 13.11.1 (2024-07-15) ### 11.13 (2021-12-31)
* Fix crash when collapsing list picker sections * Add option to play a sound when a task is completed
* Fix crash in database migration * Accept audio attachments shared from other apps
* Enabled Managed DAVx5 * Removed native EteSync v1 support
* Update translations * EteSync v1 accounts can still be synchronized with the EteSync app
* Bulgarian - @StoyanDimitrov * Bug fixes
* Translation updates
### 13.11 (2024-07-14)
* New icon picker with over 2,100 icons! (pro feature)
* Fix Todo Agenda Widget integration [todoagenda/#145](https://github.com/andstatus/todoagenda/issues/145)
* Fix menu search bar on Android 10 and below [#2966](https://github.com/tasks/tasks/issues/2966)
* Update translations
* Brazilian Portuguese - Jose Delvani
* Bulgarian - @StoyanDimitrov * Bulgarian - @StoyanDimitrov
* Catalan - @Seveorr, @jtorrensamer * Chinese (Simplified) - @sr093906
* Chinese (Simplified) - 大王叫我来巡山 * Chinese (Traditional) - @dixon777
* Chinese (Traditional) - hugoalh * Finnish - @CSharpest, Rami Lehtinen
* French - @FlorianLeChat * French - @FlorianLeChat
* Spanish - gallegonovato * Hungarian - kaciokos
* Turkish - @oersen * Italian - J. Lavoie, @Fs00
* Norwegian Bokmål - @comradekingu
* Persian - @Ahmadhosseinbor
* Spanish - @aplopez, @FlorianLeChat
* Ukrainian - @IhorHordiichuk * Ukrainian - @IhorHordiichuk
### 13.10 (2024-07-05) ### 11.12.3 (2021-11-22)
* Add search bar to drawer * Fix reminders
* 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 * Update translations
* Arabic - @islam2hamy * Indonesian - when we were sober
* Brazilian Portuguese - Jose Delvani * Kurdish (Northern) - Pêşeroja paşerojê
* Chinese (Simplified) - 大王叫我来巡山 * Romanian - @Steinhagen
* Chinese (Traditional) - hugoalh
* Croatian - @milotype
* Finnish - Rami Lehtinen, @CSharpest
* German - min7-i
* Spanish - gallegonovato
* Turkish - @oersen
### 13.9.9 (2024-05-30) ### 11.12.2 (2021-11-13)
* Fix import backup crashes * Fix reminders
* Fix showing completed subtasks in edit screen * Fix reminder preference backup
* Update translations
* Interlingua - @softinterlingua
* Tamil - @balogic
### 13.9.7 (2024-05-23) ### 11.12.1 (2021-11-05)
* Add default reminders when adding start/due dates to existing tasks [#1846](https://github.com/tasks/tasks/issues/1846) * Fix reminders
* Fix import backup crash * Update translations
* Bulgarian - @StoyanDimitrov
* Croatian - @milotype
* Norwegian Bokmål - @HumanNr4584093104
* Romanian - Simona Iacob
* Russian - @NikGreens
* Tamil - @balogic
* Turkish - @ersen0
### 13.9.6 (2024-05-18) ### 11.12 (2021-10-26)
* Fix widget crash [#2873](https://github.com/tasks/tasks/issues/2873) * Add option to notify at start date
* Fix recurrence unable to finish [#2874](https://github.com/tasks/tasks/issues/2874) * Widget tweaks for Android 12
* Fix edit screen being cleared when reopening app [#2857](https://github.com/tasks/tasks/issues/2857) * Fix crash when deleting tasks (Thanks @fschrempf!)
* Fix performance regressions * Fix truncated calendar picker
* Simplified internal alarm scheduling logic
* Update translations * Update translations
* Arabic - @islam2hamy * Basque - Sergio Varela
* Brazilian Portuguese - @laralem
* Bulgarian - @StoyanDimitrov * 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
### 13.9 (2024-05-01) ### 11.11 (2021-09-21)
* @elmuffo: Add swipe-to-snooze [#2839](https://github.com/tasks/tasks/pull/2839) * Add 'Due now' filter criteria - Thanks @tkterris!
* @IlyaBizyaev: Add option to use quick tile without unlocking device [#2847](https://github.com/tasks/tasks/pull/2847) * Fix crash on Android 12 - Thanks @tkterris!
* @liz-desartiges: Add support for Z Flip 5 cover screen [#2843](https://github.com/tasks/tasks/pull/2843) * Fix preference display issue - Thanks @Groctel!
* @purushyb: Fix drawer not updating after editing items [#2855](https://github.com/tasks/tasks/pull/2855) * Target Android 12
* @hady-exc: Migrate tag picker screen to Compose [#2849](https://github.com/tasks/tasks/pull/2849) * Ignore link clicks during multi-select
* @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
* Update translations * Update translations
* Brazilian Portuguese - @mayhmemo * Arabic - @mhmdanas, @machiav3lli
* Basque - @Thadah
* Brazilian Portuguese - @laralem
* Bulgarian - @StoyanDimitrov * Bulgarian - @StoyanDimitrov
* Catalan - @ferranpujolcamins
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype * Croatian - @milotype
* Czech - Odweta * Czech - @vitSkalicky
* German - @macpac59 * Danish - @Tntdruid
* Italian - @ppasserini * Dutch - @fvbommel
* Spanish - gallegonovato * 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 * Swedish - @bittin
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk * Ukrainian - @IhorHordiichuk
* Vietnamese - @ngocanhtve * Vietnamese - bruh
### 13.7 (2024-02-07) ### 11.10.2 (2021-07-15)
* Fix returning to previous filter after search [#2700](https://github.com/tasks/tasks/pull/2700) * Fix location-based reminders
* Fix wearable notifications on Android 14+ * Fix preference backup
* 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
* Update translations * Update translations
* Azerbaijani - Shaban Mamedov * Arabic - git ty, @mhmdanas
* Bulgarian - @StoyanDimitrov * Basque - Sergio Varela
* Catalan - raulmagdalena
* Chinese (Simplified) - 大王叫我来巡山
* Chinese (Traditional) - @abc0922001
* Croatian - @milotype * Croatian - @milotype
* Dutch - @mm4c * Czech - @vitSkalicky, @p-bo
* Esperanto - Don Zouras * Dutch - Beardhatcode, @fvbommel
* Finnish - @millerii * French - @FlorianLeChat
* French - J. Lavoie * German - K. Herbert, @franconian, @ecxod, @bluedeepimpact
* German - @CennoxX * Indonesian - when we were sober
* Hebrew - @elig0n
* Interlingua - @softinterlingua * Interlingua - @softinterlingua
* Odia - @SubhamJena * Italian - J. Lavoie
* Persian - @Monirzadeh * Lithuanian - @70h
* Spanish - gallegonovato * Norwegian Bokmål - @Jerome2103
* Swedish - @bittin * Portuguese - @laralem
* Turkish - @oersen * Russian - @KovalevArtem, @Blueberryy
* Ukrainian - Сергій * Simplified Chinese - @sr093906, @Geeyun-JY3
* Vietnamese - @ngocanhtve * Sinhala - HelaBasa
* Spanish - @FlorianLeChat, @fitojb
### 13.6.3 (2023-11-25) * Turkish - Oğuz Ersen, @emintufan
* Ukrainian - @IhorHordiichuk
* Revert "Preserve modification times on initial sync" [#2460](https://github.com/tasks/tasks/issues/2640) * Urdu - Maaz
* Fix unnecessary DecSync work * Vietnamese - bruh
### 13.6.2 (2023-10-30)
* Fix updating modification timestamp on edits
### 13.6.1 (2023-10-27) ### 11.10.1 (2021-05-26)
* Push pending changes when app is backgrounded * Improve Android 12 compatibility
* Don't require internet connection for DAVx5/EteSync/DecSync sync * Update status bar styles
* 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
* Update translations * Update translations
* Chinese (Simplified) - Eric * Arabic - @mhmdanas
* Croatian - @milotype * Basque - Sergio Varela
* Czech - @ceskyDJ * Catalan - @toram
* Finnish - @millerii * Chinese (Traditional) - @kisaragi-hiu
* French - Lionel HANNEQUIN, Bruno Duyé * Croatian - @ggdorman
* Japanese - Kazushi Hayama * Czech - @vitSkalicky
* Portuguese - @loucurapt * Esperanto - @J053Fabi0, @jakubfabijan
* Romanian - @ygorigor * French - K. Herbert, J. Lavoie
* Swedish - @bittin * 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) ### 11.10 (2021-04-19)
* Change priority with multi-select [#2257](https://github.com/tasks/tasks/pull/2452) - @vulewuxe86 * Markdown support ([Documentation](https://tasks.org/docs/markdown))
* Automatically select newly copied tasks [#2246](https://github.com/tasks/tasks/pull/2446) - @vulewuxe86 * Samsung DeX support - Thanks @mhmdanas!
* Reduce minimum size for widgets [#2436](https://github.com/tasks/tasks/pull/2436) - @histefanhere * Update to Google Play Billing v3
* Replace deprecated method call [#2526](https://github.com/tasks/tasks/pull/2526) - @kmj-99 * Remove background sync for legacy EteSync v1 accounts
* 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
* Update translations * Update translations
* Brazilian Portuguese - @gorgonun * Arabic - @mhmdanas
* Bulgarian - @StoyanDimitrov, @salif * Brazilian Portuguese - @daylightdev
* Catalan - Joan Montané
* Chinese (Simplified) - Poesty Li
* Chinese (Traditional) - @abc0922001
* Dutch - @fvbommel * Dutch - @fvbommel
* French - @FlorianLeChat * French - @FlorianLeChat, J. Lavoie
* German - @qwerty287, deep map, @franconian * German - J. Lavoie
* Hungarian - Kaci * Greek - Michalis, Eugenia Russell
* Italian - @ppasserini * Indonesian - @liimee
* Japanese - Kazushi Hayama, Naga * Italian - J. Lavoie, @Fs00
* Japanese - @kisaragi-hiu
* Kannada - @shashank-p
* Russian - @zhelemysh, Nikita Epifanov
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat * Spanish - @FlorianLeChat
* Swedish - @Anaemix, @bittin * Turkish - Oğuz Ersen
* Turkish - @emintufan, @oersen
* Ukrainian - @IhorHordiichuk * 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 * Fix date translation issue - Thanks @mhmdanas!
* Added Burmese translations - @htetoh * Fix misc translation strings - Thanks J. Lavoie!
* Update translations * Update translations
* Chinese (Simplified) - Poesty Li * Dutch - @fvbommel
* Croatian - @milotype * French - @FlorianLeChat
* Japanese - Kazushi Hayama * German - @franconian, Achim Schumacher, J. Lavoie
* Polish - @alex-ter * Hungarian - kaciokos
* Russian - @alex-ter * Indonesian - when we were sober
* Italian - @Fs00
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk * 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 * Update translations
* Bulgarian - @StoyanDimitrov * Arabic - @mhmdanas
* Czech - @ceskyDJ * 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 * Dutch - @fvbommel
* French - @FlorianLeChat * French - @FlorianLeChat
* Italian - @ppasserini * German - Achim Schumacher
* Hungarian - kaciokos
* Indonesian - when we were sober
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat * Spanish - @FlorianLeChat
* Ukrainian - @IhorHordiichuk
### 13.4 - (2023-07-16) ### 11.8 (2021-03-15)
* Sorting improvements * CalDAV: Send shared list invites
* Add subtask sort configuration * Compatible with Tasks.org, Nextcloud, ownCloud, and sabre/dav
* Update sort menu button design * Show shared list invite status in list settings
* Don't show subtasks of hidden tasks in 'My Tasks' * Fix drawer count when list is shared with 2+ users
* Fix Google Tasks sync issue * Removed legacy EteSync v1 list management features
* Dropped support for Android 6.0
* Update translations * Update translations
* Bulgarian - @StoyanDimitrov * Arabic - @mhmdanas
* Catalan - @and4po, Eudald Puy Polls
* Croatian - @milotype
* Dutch - @fvbommel * Dutch - @fvbommel
* German - @schneidr * Esperanto - @jakubfabijan
* Hungarian - Kaci * French - @FlorianLeChat
* Japanese - Naga * German - @Jerome2103
* Korean - Sunjae Choi * Hungarian - kaciokos
* Portuguese - @laralem * Indonesian - when we were sober, @andhikapangestu29
* Swedish - @bittin * 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 * CalDAV: Display shared list members in list settings
* Configure sort grouping * Compatible with Tasks.org, Nextcloud, ownCloud, OpenXchange, and sabre/dav
* Configure sorting within sort group * CalDAV: List owners can remove shared list members from list
* Configure completed task sorting * Compatible with Tasks.org, Nextcloud, ownCloud, and sabre/dav
* Fix Google Task list chips showing on widget * Fix time zone issue in recurrence picker
* Update translations * Update translations
* Bulgarian - @StoyanDimitrov * Arabic - @mhmdanas
* Catalan - @and4po * Basque - Sergio Varela
* Chinese (Simplified) - Poesty Li
* Croatian - @milotype
* Dutch - @fvbommel * Dutch - @fvbommel
* French - @FlorianLeChat * French - @FlorianLeChat
* German - @qwerty287, @franconian * Hungarian - kaciokos
* Hungarian - Kaci * Indonesian - @putulopi
* Italian - @ppasserini * Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat * Spanish - @FlorianLeChat
* Turkish - @emintufan, Oğuz Ersen
* Ukrainian - @IhorHordiichuk * Ukrainian - @IhorHordiichuk
### 13.2.4 - (2023-05-24) ### 11.6.1 (2021-03-11)
* 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) * F-Droid: Fix OpenStreetMap crash
* 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) ### 11.6 (2021-03-04)
* Fix manual sorting crash [#2141](https://github.com/tasks/tasks/issues/2141)
* Fix manual sorting bug [#2101](https://github.com/tasks/tasks/issues/2101) * CalDAV: Display indicator in drawer when a list is shared with other users
* Fix multiple accounts on same server [#2301](https://github.com/tasks/tasks/issues/2301) * Compatible with Tasks.org, Nextcloud, ownCloud, OpenXchange, and sabre/dav
* Don't set `COUNT=0` on recurrence rules [#2158](https://github.com/tasks/tasks/issues/2158) * CalDAV: Don't upload changes to read-only lists
* Improve task list performance [#2062](https://github.com/tasks/tasks/issues/2062) ([#931](https://github.com/tasks/tasks/issues/931))
* Attempt to hide inactive widgets in settings [#2145](https://github.com/tasks/tasks/issues/2145) * Remove unnecessary icon-mirroring for RTL users
* Disable persistent reminders on Android 14+ ([#1385](https://github.com/tasks/tasks/issues/1385) and
* Android 14+ no longer supports persistent reminders 😢 [#1391](https://github.com/tasks/tasks/pull/1391)) - Thanks to @mhmdanas
* 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
* Update translations * Update translations
* Brazilian Portuguese - @lnux-usr * Arabic - @mhmdanas
* Basque - Sergio Varela
* Bulgarian - @StoyanDimitrov * Bulgarian - @StoyanDimitrov
* Catalan - @and4po * Czech - @vitSkalicky
* Chinese (Simplified) - Poesty Li
* Chinese (Traditional) - Chih-Hsuan Yen
* Croatian - @milotype
* Dutch - @fvbommel * Dutch - @fvbommel
* Esperanto - Don Zouras
* Finnish - @millerii
* French - @FlorianLeChat * French - @FlorianLeChat
* Italian - @ppasserini * Hungarian - kaciokos
* Japanese - @kisaragi-hiu, Naga * Indonesian - @putulopi
* Korean - Sunjae Choi, @o20n3 * Russian - Nikita Epifanov
* Romanian - @simonaiacob * Simplified Chinese - @sr093906
* Russian - @AHOHNMYC * Sinhala - HelaBasa
* Spanish - @FlorianLeChat * Spanish - @FlorianLeChat
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk * 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) * Fix CalDAV sync error
* Sync when brought to the foreground [#2096](https://github.com/tasks/tasks/issues/2096) * Report errors when generating recurrence dates
* 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
### 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) * Fix 'repeat until' date
* Use default Android network security configuration * 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 * Update translations
* Bulgarian - @StoyanDimitrov * Basque - Sergio Varela
* Chinese (Simplified) - Eric * Croatian - @ggdorman
* Croatian - @milotype
* Dutch - @fvbommel * Dutch - @fvbommel
* Finnish - @millerii
* French - @FlorianLeChat * French - @FlorianLeChat
* German - @helloworldtest123 * Hungarian - kaciokos
* Hungarian - Kaci * Norwegian Bokmål - @comradekingu
* Italian - @ppasserini * Polish - @alex-ter
* Lithuanian - @70h
* Russian - Nikita Epifanov * Russian - Nikita Epifanov
* Simplified Chinese - @sr093906
* Spanish - @FlorianLeChat * Spanish - @FlorianLeChat
* Turkish - @ersen0 * Turkish - Oğuz Ersen
* Ukrainian - @IhorHordiichuk * Ukrainian - @IhorHordiichuk
* Urdu - Maaz
### 13.0.2 (2022-11-22)
### 11.5 (2021-02-17)
* Fix persistent notifications on Android 13
* Fix Samsung crash on too many reminders (DAVx5, EteSync, DecSync CC) * Sync snooze time with Tasks.org, DAVx⁵, CalDAV, EteSync, and DecSync
* Fix crash on too many tasks for Astrid Manual Sorting * Compatible with Thunderbird
* Fix RTL text in task edit customization screen * New map theme preference
* Fix priority button order * 10 new icons
* F-Droid: Use Nominatim for reverse geocoding
### 13.0.1 (2022-10-20) * Google Play: Use OpenStreetMap tiles when Play Services not available
* Google Play: Use Android location services when Play Services not available
* 🚨 Major internal changes to task edit screen. Please report any bugs! 🚨 * Tasks.org accounts: Use Google Places for map search
* 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)
* Update translations * Update translations
* Asturian - @enolp
* Basque - Sergio Varela
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Czech - Shimon
* Dutch - @fvbommel * Dutch - @fvbommel
* French - @FlorianLeChat, J. Lavoie * French - @FlorianLeChat
* German - @qwerty287 * Hungarian - kaciokos
* Italian - @ppasserini * 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 * Norwegian Bokmål - @comradekingu
* Persian - @latelateprogrammer * Polish - @alex-ter
* Polish - @ebogucka * Russian - Nikita Epifanov
* Portuguese - @laralem * Simplified Chinese - @sr093906
* Romanian - @simonaiacob
* Russian - @Allineer, Nikita Epifanov
* Sinhala - @Dilshan-H
* Spanish - @FlorianLeChat * Spanish - @FlorianLeChat
* Turkish - @ersen0 * Traditional Chinese - @realpineapplemilk
* Ukrainian - @IhorHordiichuk, @artemmolotov * Turkish - @emintufan, Oğuz Ersen
* Vietnamese - @unbiaseduser
### 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,52 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.7) CFPropertyList (3.0.5)
base64
nkf
rexml rexml
addressable (2.8.7) addressable (2.8.0)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 5.0)
artifactory (3.0.17) artifactory (3.0.15)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.3.0) aws-eventstream (1.2.0)
aws-partitions (1.958.0) aws-partitions (1.534.0)
aws-sdk-core (3.201.3) aws-sdk-core (3.123.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.1)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1.0)
aws-sdk-kms (1.88.0) aws-sdk-kms (1.51.0)
aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-core (~> 3, >= 3.122.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.156.0) aws-sdk-s3 (1.107.0)
aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-core (~> 3, >= 3.122.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.4)
aws-sigv4 (1.9.0) aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
base64 (0.2.0) claide (1.0.3)
claide (1.1.0)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander (4.6.0) commander (4.6.0)
highline (~> 2.0.0) highline (~> 2.0.0)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.6.5) digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107) domain_name (0.5.20190701)
dotenv (2.8.1) unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.111.0) excon (0.88.0)
faraday (1.10.3) faraday (1.8.0)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0) faraday-httpclient (~> 1.0.1)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0) faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0) faraday-net_http_persistent (~> 1.1)
faraday-patron (~> 1.0) faraday-patron (~> 1.0)
faraday-rack (~> 1.0) faraday-rack (~> 1.0)
faraday-retry (~> 1.0) multipart-post (>= 1.2, < 3)
ruby2_keywords (>= 0.0.4) ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7) faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0) faraday (>= 0.8.0)
@ -58,24 +55,21 @@ GEM
faraday-em_synchrony (1.0.0) faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0) faraday-excon (1.1.0)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.0.4) faraday-net_http (1.0.1)
multipart-post (~> 2)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
faraday-rack (1.0.0) faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.3.1) fastimage (2.2.5)
fastlane (2.222.0) fastlane (2.198.1)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0) aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0) babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0) bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2) colored
commander (~> 4.6) commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0) dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0) emoji_regex (>= 0.1, < 4.0)
@ -87,32 +81,30 @@ GEM
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3) google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1) google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31) google-cloud-storage (~> 1.31)
highline (~> 2.0) highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0) json (< 3.0.0)
jwt (>= 2.1.0, < 3) jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0) mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0) multipart-post (~> 2.0.0)
naturally (~> 2.2) naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0) optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0) plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0) rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5) security (= 0.1.3)
simctl (~> 1.6.3) simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0) 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-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0) word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0) xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.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) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0) google-apis-androidpublisher_v3 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.11.3) google-apis-core (0.4.1)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@ -120,89 +112,93 @@ GEM
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
google-apis-iamcredentials_v1 (0.17.0) webrick
google-apis-core (>= 0.11.0, < 2.a) google-apis-iamcredentials_v1 (0.8.0)
google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.4, < 2.a)
google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.6.0)
google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.4, < 2.a)
google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.9.0)
google-cloud-core (1.7.0) google-apis-core (>= 0.4, < 2.a)
google-cloud-env (>= 1.0, < 3.a) google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.4.0) google-cloud-errors (1.2.0)
google-cloud-storage (1.47.0) google-cloud-storage (1.34.1)
addressable (~> 2.8) addressable (~> 2.5)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0) google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.8.1) googleauth (1.1.0)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
highline (2.0.3) highline (2.0.3)
http-cookie (1.0.6) http-cookie (1.0.4)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.6.2) jmespath (1.4.0)
json (2.7.2) json (2.6.1)
jwt (2.8.2) jwt (2.3.0)
base64 memoist (0.16.2)
mini_magick (4.13.2) mini_magick (4.11.0)
mini_mime (1.1.5) mini_mime (1.1.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.1) multipart-post (2.0.0)
nanaimo (0.3.0) nanaimo (0.3.0)
naturally (2.2.1) naturally (2.2.1)
nkf (0.2.0) optparse (0.1.1)
optparse (0.5.0)
os (1.1.4) os (1.1.4)
plist (3.7.1) plist (3.6.0)
public_suffix (5.1.1) public_suffix (4.0.6)
rake (13.2.1) rake (13.0.6)
representable (3.2.0) representable (3.1.1)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.3.6) rexml (3.2.5)
strscan
rouge (2.0.7) rouge (2.0.7)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
security (0.1.5) security (0.1.3)
signet (0.19.0) signet (0.16.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simctl (1.6.10) simctl (1.6.8)
CFPropertyList CFPropertyList
naturally naturally
strscan (3.1.0)
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (3.0.2) terminal-table (1.8.0)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-screen (0.8.2) tty-screen (0.8.1)
tty-spinner (0.9.3) tty-spinner (0.9.3)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
uber (0.1.0) 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) word_wrap (1.0.0)
xcodeproj (1.19.0) xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.3.0) nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0) xcpretty (0.3.0)
rouge (~> 2.0.7) rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1) xcpretty-travis-formatter (1.0.1)

@ -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) [![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) [![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 ### 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 com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.android.application) id("com.android.application")
id("checkstyle")
id("com.google.gms.google-services") id("com.google.gms.google-services")
id("com.google.firebase.crashlytics") id("com.google.firebase.crashlytics")
kotlin("android") 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("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 { repositories {
compilerOptions { mavenCentral()
jvmTarget.set(JvmTarget.JVM_17) 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 { android {
val commonTest = "src/commonTest/java"
sourceSets["test"].java.srcDir(commonTest)
sourceSets["androidTest"].java.srcDirs("src/androidTest/java", commonTest)
bundle { bundle {
language { language {
enableSplit = false enableSplit = false
@ -37,25 +47,32 @@ android {
viewBinding = true viewBinding = true
dataBinding = true dataBinding = true
compose = true compose = true
buildConfig = true
} }
lint { lint {
disable("InvalidPeriodicWorkRequestInterval")
lintConfig = file("lint.xml") lintConfig = file("lint.xml")
textOutput = File("stdout") textOutput("stdout")
textReport = true textReport = true
} }
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = Versions.compileSdk
defaultConfig { defaultConfig {
testApplicationId = "org.tasks.test" testApplicationId = "org.tasks.test"
applicationId = "org.tasks" applicationId = "org.tasks"
versionCode = 131106 versionCode = 111301
versionName = "13.11.1" versionName = "11.13"
targetSdk = libs.versions.android.targetSdk.get().toInt() targetSdk = Versions.targetSdk
minSdk = libs.versions.android.minSdk.get().toInt() minSdk = Versions.minSdk
testInstrumentationRunner = "org.tasks.TestRunner" testInstrumentationRunner = "org.tasks.TestRunner"
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
}
}
} }
signingConfigs { signingConfigs {
@ -74,15 +91,21 @@ android {
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_1_8
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.compose
} }
flavorDimensions += listOf("store") kotlinOptions {
jvmTarget = "1.8"
}
@Suppress("LocalVariableName") @Suppress("LocalVariableName")
buildTypes { buildTypes {
debug { getByName("debug") {
configure<CrashlyticsExtension> { configure<CrashlyticsExtension> {
mappingFileUploadEnabled = false mappingFileUploadEnabled = false
} }
@ -94,9 +117,9 @@ android {
resValue("string", "tasks_caldav_url", tasks_caldav_url ?: "https://caldav.tasks.org") 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_nominatim_url", tasks_caldav_url ?: "https://nominatim.tasks.org")
resValue("string", "tasks_places_url", tasks_caldav_url ?: "https://places.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_mapbox_key: String? by project
val tasks_google_key: String? by project val tasks_google_key: String? by project
resValue("string", "mapbox_key", tasks_mapbox_key ?: "") resValue("string", "mapbox_key", tasks_mapbox_key ?: "")
@ -107,34 +130,25 @@ android {
} }
} }
flavorDimensions("store")
productFlavors { productFlavors {
create("generic") { create("generic") {
dimension = "store" dimension = "store"
} }
create("googleplay") { create("googleplay") {
isDefault = true
dimension = "store" dimension = "store"
} }
} }
packaging {
resources {
excludes += setOf("META-INF/*.kotlin_module", "META-INF/INDEX.LIST")
}
}
testOptions { packagingOptions {
managedDevices { exclude("META-INF/*.kotlin_module")
localDevices {
create("pixel2api30") {
device = "Pixel 2"
apiLevel = 30
systemImageSource = "aosp-atd"
}
}
}
} }
}
namespace = "org.tasks" configure<CheckstyleExtension> {
configFile = project.file("google_checks.xml")
toolVersion = "8.16"
} }
configurations.all { configurations.all {
@ -151,131 +165,98 @@ val genericImplementation by configurations
val googleplayImplementation by configurations val googleplayImplementation by configurations
dependencies { dependencies {
implementation(projects.data) coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation(projects.kmp) implementation("com.gitlab.abaker:dav4jvm:deb2c9aef8")
implementation(projects.icons) implementation("com.gitlab.abaker:ical4android:0e928b567c")
coreLibraryDesugaring(libs.desugar.jdk.libs) implementation("com.gitlab.bitfireAT:cert4android:26a91a729f")
implementation(libs.bitfire.dav4jvm) { implementation("com.github.tasks.opentasks:opentasks-provider:a1faa1b") {
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) {
exclude("com.github.tasks.opentasks", "opentasks-contract") exclude("com.github.tasks.opentasks", "opentasks-contract")
} }
implementation(libs.dmfs.rfc5545.datetime)
implementation(libs.dmfs.recur)
implementation(libs.dmfs.jems)
implementation(libs.dagger.hilt) implementation("com.google.dagger:hilt-android:${Versions.hilt}")
ksp(libs.dagger.hilt.compiler) kapt("com.google.dagger:hilt-compiler:${Versions.hilt}")
ksp(libs.androidx.hilt.compiler) kapt("androidx.hilt:hilt-compiler:${Versions.hilt_androidx}")
implementation(libs.androidx.hilt.work) implementation("androidx.hilt:hilt-work:${Versions.hilt_androidx}")
implementation(libs.androidx.datastore) implementation("androidx.fragment:fragment-ktx:1.4.0")
implementation(libs.androidx.fragment.ktx) implementation("androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}")
implementation(libs.androidx.lifecycle.runtime) implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.lifecycle}")
implementation(libs.androidx.lifecycle.runtime.compose) implementation("androidx.room:room-ktx:${Versions.room}")
implementation(libs.androidx.lifecycle.viewmodel) kapt("androidx.room:room-compiler:${Versions.room}")
implementation(libs.androidx.room) implementation("androidx.appcompat:appcompat:1.4.0")
implementation(libs.androidx.appcompat) implementation("androidx.paging:paging-runtime:2.1.2")
implementation(libs.iconics) implementation("io.noties.markwon:core:${Versions.markwon}")
implementation(libs.markwon) implementation("io.noties.markwon:editor:${Versions.markwon}")
implementation(libs.markwon.editor) implementation("io.noties.markwon:ext-tasklist:${Versions.markwon}")
implementation(libs.markwon.linkify) implementation("io.noties.markwon:ext-strikethrough:${Versions.markwon}")
implementation(libs.markwon.strikethrough) implementation("io.noties.markwon:ext-tables:${Versions.markwon}")
implementation(libs.markwon.tables) implementation("io.noties.markwon:linkify:${Versions.markwon}")
implementation(libs.markwon.tasklist)
debugImplementation(libs.facebook.flipper) debugImplementation("com.facebook.flipper:flipper:${Versions.flipper}")
debugImplementation(libs.facebook.flipper.network) debugImplementation("com.facebook.flipper:flipper-network-plugin:${Versions.flipper}")
debugImplementation(libs.facebook.soloader) debugImplementation("com.facebook.soloader:soloader:0.10.1")
debugImplementation(libs.leakcanary) debugImplementation("com.squareup.leakcanary:leakcanary-android:${Versions.leakcanary}")
debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-tooling:${Versions.compose}")
debugImplementation(libs.kotlin.reflect) debugImplementation("org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}")
implementation(libs.kotlin.jdk8) implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Versions.kotlin}")
implementation(libs.kotlinx.immutable) implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.4")
implementation(libs.kotlinx.serialization) implementation("com.squareup.okhttp3:okhttp:${Versions.okhttp}")
implementation(libs.okhttp) implementation("com.google.code.gson:gson:2.8.8")
implementation(libs.persistent.cookiejar) implementation("com.google.android.material:material:1.5.0-rc01")
implementation(libs.material) implementation("androidx.constraintlayout:constraintlayout:2.1.2")
implementation(libs.androidx.compose.material3) implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation(libs.androidx.constraintlayout) implementation("androidx.preference:preference:1.1.1")
implementation(libs.androidx.swiperefreshlayout) implementation("com.jakewharton.timber:timber:5.0.1")
implementation(libs.androidx.preference) implementation("com.google.android.apps.dashclock:dashclock-api:2.0.0")
implementation(libs.timber) implementation("com.twofortyfouram:android-plugin-api-for-locale:1.0.2") {
implementation(libs.dashclock.api)
implementation(libs.locale) {
isTransitive = false isTransitive = false
} }
implementation(libs.jchronic) { implementation("com.rubiconproject.oss:jchronic:0.2.6") {
isTransitive = false isTransitive = false
} }
implementation(libs.shortcut.badger) implementation("me.leolin:ShortcutBadger:1.1.22@aar")
implementation(libs.google.api.tasks) implementation("com.google.apis:google-api-services-tasks:v1-rev20210709-1.32.1")
implementation(libs.google.api.drive) implementation("com.google.apis:google-api-services-drive:v3-rev20210725-1.32.1")
implementation(libs.google.oauth2) implementation("com.google.auth:google-auth-library-oauth2-http:0.26.0")
implementation(libs.androidx.work) implementation("androidx.work:work-runtime:${Versions.work}")
implementation(libs.etebase) implementation("androidx.work:work-runtime-ktx:${Versions.work}")
implementation(libs.colorpicker) implementation("com.etebase:client:2.3.2")
implementation(libs.appauth) implementation("com.github.QuadFlask:colorpicker:0.0.15")
implementation(libs.osmdroid) implementation("net.openid:appauth:0.8.1")
implementation(libs.retrofit) implementation("org.osmdroid:osmdroid-android:6.1.11@aar")
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(libs.accompanist.flowlayout) implementation("androidx.compose.ui:ui:${Versions.compose}")
implementation(libs.accompanist.permissions) implementation("androidx.compose.foundation:foundation:${Versions.compose}")
implementation(libs.accompanist.systemuicontroller) 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_theme_adapter}")
implementation("androidx.activity:activity-compose:1.4.0")
releaseCompileOnly("androidx.compose.ui:ui-tooling:${Versions.compose}")
googleplayImplementation(platform(libs.firebase)) googleplayImplementation("com.google.firebase:firebase-crashlytics:${Versions.crashlytics}")
googleplayImplementation("com.google.firebase:firebase-crashlytics") googleplayImplementation("com.google.firebase:firebase-analytics:${Versions.analytics}") {
googleplayImplementation("com.google.firebase:firebase-analytics") {
exclude("com.google.android.gms", "play-services-ads-identifier") exclude("com.google.android.gms", "play-services-ads-identifier")
} }
googleplayImplementation("com.google.firebase:firebase-config-ktx") googleplayImplementation("com.google.firebase:firebase-config-ktx:${Versions.remote_config}")
googleplayImplementation(libs.play.services.location) googleplayImplementation("com.google.android.gms:play-services-location:19.0.0")
googleplayImplementation(libs.play.services.maps) googleplayImplementation("com.google.android.gms:play-services-maps:18.0.1")
googleplayImplementation(libs.play.billing.ktx) googleplayImplementation("com.android.billingclient:billing-ktx:3.0.3")
googleplayImplementation(libs.play.review)
googleplayImplementation(libs.play.services.oss.licenses)
androidTestImplementation(libs.dagger.hilt.testing) androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.hilt}")
kspAndroidTest(libs.dagger.hilt.compiler) kaptAndroidTest("com.google.dagger:hilt-compiler:${Versions.hilt}")
kspAndroidTest(libs.androidx.hilt.compiler) kaptAndroidTest("androidx.hilt:hilt-compiler:${Versions.hilt_androidx}")
androidTestImplementation(libs.mockito.android) androidTestImplementation("org.mockito:mockito-android:${Versions.mockito}")
androidTestImplementation(libs.make.it.easy) androidTestImplementation("com.natpryce:make-it-easy:${Versions.make_it_easy}")
androidTestImplementation(libs.androidx.test.runner) androidTestImplementation("androidx.test:runner:${Versions.androidx_test}")
androidTestImplementation(libs.androidx.test.rules) androidTestImplementation("androidx.test:rules:${Versions.androidx_test}")
androidTestImplementation(libs.androidx.junit) androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation(libs.okhttp.mockwebserver) androidTestImplementation("com.squareup.okhttp3:mockwebserver:${Versions.okhttp}")
testImplementation(libs.junit) testImplementation("junit:junit:4.13.2")
testImplementation(libs.kotlinx.coroutines.test) testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2")
testImplementation(libs.make.it.easy) testImplementation("com.natpryce:make-it-easy:${Versions.make_it_easy}")
testImplementation(libs.androidx.test.core) testImplementation("androidx.test:core:${Versions.androidx_test}")
testImplementation(libs.mockito.core) testImplementation("org.mockito:mockito-core:${Versions.mockito}")
testImplementation(libs.xpp3) 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,962 @@
- 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: 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: 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: 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: 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
- artifact: androidx.emoji2:emoji2-views-helper:+
name: emoji2-views-helper
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/emoji2#1.0.0
- artifact: androidx.emoji2:emoji2:+
name: emoji2
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/emoji2#1.0.0
- artifact: androidx.resourceinspection:resourceinspection-annotation:+
name: resourceinspection-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/releases/resourceinspection#1.0.0

15
app/proguard.pro vendored

@ -16,7 +16,9 @@
-dontwarn javax.inject.** -dontwarn javax.inject.**
-dontwarn com.google.j2objc.annotations.** -dontwarn com.google.j2objc.annotations.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -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 # https://github.com/square/okhttp/blob/0b74bba08805c28f6aede626cf06f213ef6480f2/README.md
-dontwarn okhttp3.** -dontwarn okhttp3.**
@ -47,13 +49,4 @@
-dontnote java.nio.file.Files, java.nio.file.Path -dontnote java.nio.file.Files, java.nio.file.Path
-dontnote **.ILicensingService -dontnote **.ILicensingService
-dontnote sun.misc.Unsafe -dontnote sun.misc.Unsafe
-dontwarn 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.** { *; }

@ -5,372 +5,452 @@
*/ */
package com.todoroo.andlib.utility package com.todoroo.andlib.utility
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.tasks.SuspendFreeze import org.tasks.Freeze.Companion.freezeAt
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.TestUtilities.withLocale
import org.tasks.date.DateTimeUtils import org.tasks.date.DateTimeUtils
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.Context.is24HourOverride
import org.tasks.kmp.formatDayOfWeek
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.TextStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.Locale import java.time.format.FormatStyle
import java.util.*
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class DateUtilitiesTest { class DateUtilitiesTest {
@After @After
fun after() { fun after() {
is24HourOverride = null DateUtilities.is24HourOverride = null
} }
@Test @Test
fun testGet24HourTime() { fun testGet24HourTime() {
is24HourOverride = true DateUtilities.is24HourOverride = true
assertEquals("09:05", getTimeString(DateTime(2014, 1, 4, 9, 5, 36).millis, is24HourFormat)) assertEquals("09:05", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 9, 5, 36)))
assertEquals("13:00", getTimeString(DateTime(2014, 1, 4, 13, 0, 1).millis, is24HourFormat)) assertEquals("13:00", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 13, 0, 1)))
} }
@Test @Test
fun testGetTime() { fun testGetTime() {
is24HourOverride = false DateUtilities.is24HourOverride = false
assertEquals("9:05 AM", getTimeString(DateTime(2014, 1, 4, 9, 5, 36).millis, is24HourFormat)) assertEquals("9:05 AM", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 9, 5, 36)))
assertEquals("1:05 PM", getTimeString(DateTime(2014, 1, 4, 13, 5, 36).millis, is24HourFormat)) assertEquals("1:05 PM", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 13, 5, 36)))
} }
@Test @Test
fun testGetTimeWithNoMinutes() { fun testGetTimeWithNoMinutes() {
is24HourOverride = false DateUtilities.is24HourOverride = false
assertEquals("1 PM", getTimeString(DateTime(2014, 1, 4, 13, 0, 59).millis, is24HourFormat)) // derp? assertEquals("1 PM", DateUtilities.getTimeString(null, DateTime(2014, 1, 4, 13, 0, 59))) // derp?
} }
@Test @Test
fun testGetDateStringWithYear() = runBlocking { fun testGetDateStringWithYear() {
assertEquals("Jan 4, 2014", getRelativeDay(DateTime(2014, 1, 4, 0, 0, 0).millis)) assertEquals("Jan 4, 2014", DateUtilities.getDateString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 4, 0, 0, 0)))
} }
@Test @Test
fun testGetDateStringHidingYear() = runBlocking { fun testGetDateStringHidingYear() {
freezeAt(DateTimeUtils.newDate(2014, 2, 1)) { freezeAt(DateTimeUtils.newDate(2014, 2, 1)) {
assertEquals("Jan 1", getRelativeDay(DateTime(2014, 1, 1).millis)) assertEquals("Jan 1", DateUtilities.getDateString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 1)))
} }
} }
@Test @Test
fun testGetDateStringWithDifferentYear() = runBlocking { fun testGetDateStringWithDifferentYear() {
freezeAt(DateTimeUtils.newDate(2013, 12, 1)) { freezeAt(DateTimeUtils.newDate(2013, 12, 1)) {
assertEquals("Jan 1, 2014", getRelativeDay(DateTime(2014, 1, 1, 0, 0, 0).millis)) assertEquals("Jan 1, 2014", DateUtilities.getDateString(ApplicationProvider.getApplicationContext(), DateTime(2014, 1, 1, 0, 0, 0)))
} }
} }
@Test @Test
fun testGetWeekdayLongString() = withLocale(Locale.US) { fun testGetWeekdayLongString() {
assertEquals("Sunday", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 29).millis, TextStyle.FULL)) assertEquals("Sunday", DateUtilities.getWeekday(DateTimeUtils.newDate(2013, 12, 29), Locale.US))
assertEquals("Monday", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 30).millis, TextStyle.FULL)) assertEquals("Monday", DateUtilities.getWeekday(DateTimeUtils.newDate(2013, 12, 30), Locale.US))
assertEquals("Tuesday", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 31).millis, TextStyle.FULL)) assertEquals("Tuesday", DateUtilities.getWeekday(DateTimeUtils.newDate(2013, 12, 31), Locale.US))
assertEquals("Wednesday", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 1).millis, TextStyle.FULL)) assertEquals("Wednesday", DateUtilities.getWeekday(DateTimeUtils.newDate(2014, 1, 1), Locale.US))
assertEquals("Thursday", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 2).millis, TextStyle.FULL)) assertEquals("Thursday", DateUtilities.getWeekday(DateTimeUtils.newDate(2014, 1, 2), Locale.US))
assertEquals("Friday", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 3).millis, TextStyle.FULL)) assertEquals("Friday", DateUtilities.getWeekday(DateTimeUtils.newDate(2014, 1, 3), Locale.US))
assertEquals("Saturday", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 4).millis, TextStyle.FULL)) assertEquals("Saturday", DateUtilities.getWeekday(DateTimeUtils.newDate(2014, 1, 4), Locale.US))
} }
@Test @Test
fun testGetWeekdayShortString() = withLocale(Locale.US) { fun testGetWeekdayShortString() {
assertEquals("Sun", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 29).millis, TextStyle.SHORT)) assertEquals("Sun", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2013, 12, 29), Locale.US))
assertEquals("Mon", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 30).millis, TextStyle.SHORT)) assertEquals("Mon", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2013, 12, 30), Locale.US))
assertEquals("Tue", formatDayOfWeek(DateTimeUtils.newDate(2013, 12, 31).millis, TextStyle.SHORT)) assertEquals("Tue", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2013, 12, 31), Locale.US))
assertEquals("Wed", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 1).millis, TextStyle.SHORT)) assertEquals("Wed", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2014, 1, 1), Locale.US))
assertEquals("Thu", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 2).millis, TextStyle.SHORT)) assertEquals("Thu", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2014, 1, 2), Locale.US))
assertEquals("Fri", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 3).millis, TextStyle.SHORT)) assertEquals("Fri", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2014, 1, 3), Locale.US))
assertEquals("Sat", formatDayOfWeek(DateTimeUtils.newDate(2014, 1, 4).millis, TextStyle.SHORT)) assertEquals("Sat", DateUtilities.getWeekdayShort(DateTimeUtils.newDate(2014, 1, 4), Locale.US))
} }
@Test @Test
fun getRelativeFullDate() = withLocale(Locale.US) { fun getRelativeFullDate() {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"Sunday, January 14", "Sunday, January 14",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.US,
FormatStyle.FULL))
} }
} }
@Test @Test
fun getRelativeFullDateWithYear() = withLocale(Locale.US) { fun getRelativeFullDateWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"Sunday, January 14, 2018", "Sunday, January 14, 2018",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.US,
FormatStyle.FULL))
} }
} }
@Test @Test
fun getRelativeFullDateTime() = withLocale(Locale.US) { fun getRelativeFullDateTime() {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertMatches( assertMatches(
"Sunday, January 14( at)? 1:43 PM", "Sunday, January 14( at)? 1:43 PM",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 43, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 43, 1).millis,
Locale.US,
FormatStyle.FULL))
} }
} }
@Test @Test
@Ignore("Fails on CI - need to investigate") @Ignore("Fails on CI - need to investigate")
fun getRelativeDateTimeWithAlwaysDisplayFullDateOption() = withLocale(Locale.US) { fun getRelativeDateTimeWithAlwaysDisplayFullDateOption() {
freezeAt(DateTime(2020, 1, 1)) { freezeAt(DateTime(2020, 1, 1)) {
assertMatches( assertMatches(
"Thursday, January 2 at 11:50 AM", "Thursday, January 2 at 11:50 AM",
getRelativeDateTime(DateTime(2020, 1, 2, 11, 50, 1).millis, is24HourFormat, DateStyle.FULL, true, false) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2020, 1, 2, 11, 50, 1).millis,
Locale.US,
FormatStyle.FULL,
true,
false
))
} }
} }
@Test @Test
fun getRelativeFullDateTimeWithYear() = withLocale(Locale.US) { fun getRelativeFullDateTimeWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"Sunday, January 14, 2018( at)? 11:50 AM", "Sunday, January 14, 2018( at)? 11:50 AM",
getRelativeDateTime(DateTime(2018, 1, 14, 11, 50, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 11, 50, 1).millis,
Locale.US,
FormatStyle.FULL))
} }
} }
@Test @Test
fun getRelativeDayWithAlwaysDisplayFullDateOption() = withLocale(Locale.US) { fun getRelativeDayWithAlwaysDisplayFullDateOption() {
freezeAt(DateTime(2020, 1, 1)) { freezeAt(DateTime(2020, 1, 1)) {
assertEquals( assertEquals(
"Thursday, January 2", "Thursday, January 2",
getRelativeDay(DateTime(2020, 1, 2, 11, 50, 1).millis, DateStyle.FULL, alwaysDisplayFullDate = true, lowercase = true) DateUtilities.getRelativeDay(
ApplicationProvider.getApplicationContext(),
DateTime(2020, 1, 2, 11, 50, 1).millis,
Locale.US,
FormatStyle.FULL,
true,
true
)
) )
} }
} }
@Test @Test
fun getRelativeDayWithoutAlwaysDisplayFullDateOption() = withLocale(Locale.US) { fun getRelativeDayWithoutAlwaysDisplayFullDateOption() {
freezeAt(DateTime(2020, 1, 1)) { freezeAt(DateTime(2020, 1, 1)) {
assertEquals( assertEquals(
"tomorrow", "tomorrow",
getRelativeDay(DateTime(2020, 1, 2, 11, 50, 1).millis, DateStyle.FULL, lowercase = true) DateUtilities.getRelativeDay(
ApplicationProvider.getApplicationContext(),
DateTime(2020, 1, 2, 11, 50, 1).millis,
Locale.US,
FormatStyle.FULL,
false,
true
)
) )
} }
} }
@Test @Test
fun germanDateNoYear() = withLocale(Locale.GERMAN) { fun germanDateNoYear() {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"Sonntag, 14. Januar", "Sonntag, 14. Januar",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.GERMAN,
FormatStyle.FULL))
} }
} }
@Test @Test
fun germanDateWithYear() = withLocale(Locale.GERMAN) { fun germanDateWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"Sonntag, 14. Januar 2018", "Sonntag, 14. Januar 2018",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.GERMAN,
FormatStyle.FULL))
} }
} }
@Test @Test
fun koreanDateNoYear() = withLocale(Locale.KOREAN) { fun koreanDateNoYear() {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"1월 14일 일요일", "1월 14일 일요일",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.KOREAN,
FormatStyle.FULL))
} }
} }
@Test @Test
fun koreanDateWithYear() = withLocale(Locale.KOREAN) { fun koreanDateWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"2018년 1월 14일 일요일", "2018년 1월 14일 일요일",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.KOREAN,
FormatStyle.FULL))
} }
} }
@Test @Test
fun japaneseDateNoYear() = withLocale(Locale.JAPANESE) { fun japaneseDateNoYear() {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"1月14日日曜日", "1月14日日曜日",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.JAPANESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun japaneseDateWithYear() = withLocale(Locale.JAPANESE) { fun japaneseDateWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"2018年1月14日日曜日", "2018年1月14日日曜日",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.JAPANESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun chineseDateNoYear() = withLocale(Locale.CHINESE) { fun chineseDateNoYear() {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"1月14日星期日", "1月14日星期日",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.CHINESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun chineseDateWithYear() = withLocale(Locale.CHINESE) { fun chineseDateWithYear() {
SuspendFreeze.freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"2018年1月14日星期日", "2018年1月14日星期日",
getRelativeDateTime(DateTime(2018, 1, 14).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14).millis,
Locale.CHINESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun chineseDateTimeNoYear() = withLocale(Locale.CHINESE) { fun chineseDateTimeNoYear() {
freezeAt(DateTime(2018, 1, 1)) { freezeAt(DateTime(2018, 1, 1)) {
assertEquals( assertEquals(
"1月14日星期日 上午11:53", "1月14日星期日 上午11:53",
getRelativeDateTime(DateTime(2018, 1, 14, 11, 53, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 11, 53, 1).millis,
Locale.CHINESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun chineseDateTimeWithYear() = withLocale(Locale.CHINESE) { fun chineseDateTimeWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"2018年1月14日星期日 下午1:45", "2018年1月14日星期日 下午1:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.CHINESE,
FormatStyle.FULL))
} }
} }
@Test @Test
fun frenchDateTimeWithYear() = withLocale(Locale.FRENCH) { fun frenchDateTimeWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"dimanche 14 janvier 2018( à)? 13:45", "dimanche 14 janvier 2018( à)? 13:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.FRENCH,
FormatStyle.FULL))
} }
} }
@Test @Test
fun indiaDateTimeWithYear() = withLocale(Locale.forLanguageTag("hi-IN")) { fun indiaDateTimeWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"रविवार, 14 जनवरी 2018( को)? 1:45 pm", "रविवार, 14 जनवरी 2018( को)? 1:45 pm",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("hi-IN"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun russiaDateTimeNoYear() = withLocale(Locale.forLanguageTag("ru")) { fun russiaDateTimeNoYear() {
freezeAt(DateTime(2018, 12, 12)) { freezeAt(DateTime(2018, 12, 12)) {
assertMatches( assertMatches(
"воскресенье, 14 января,? 13:45", "воскресенье, 14 января,? 13:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("ru"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun russiaDateTimeWithYear() = withLocale(Locale.forLanguageTag("ru")) { fun russiaDateTimeWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"воскресенье, 14 января 2018 г.,? 13:45", "воскресенье, 14 января 2018 г.,? 13:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("ru"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun brazilDateTimeNoYear() = withLocale(Locale.forLanguageTag("pt-br")) { fun brazilDateTimeNoYear() {
freezeAt(DateTime(2018, 12, 12)) { freezeAt(DateTime(2018, 12, 12)) {
assertEquals( assertEquals(
"domingo, 14 de janeiro 13:45", "domingo, 14 de janeiro 13:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("pt-br"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun brazilDateTimeWithYear() = withLocale(Locale.forLanguageTag("pt-br")) { fun brazilDateTimeWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertEquals( assertEquals(
"domingo, 14 de janeiro de 2018 13:45", "domingo, 14 de janeiro de 2018 13:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("pt-br"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun spainDateTimeNoYear() = withLocale(Locale.forLanguageTag("es")) { fun spainDateTimeNoYear() {
freezeAt(DateTime(2018, 12, 12)) { freezeAt(DateTime(2018, 12, 12)) {
assertMatches( assertMatches(
"domingo, 14 de enero,? 13:45", "domingo, 14 de enero,? 13:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("es"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun spainDateTimeWithYear() = withLocale(Locale.forLanguageTag("es")) { fun spainDateTimeWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"domingo, 14 de enero de 2018,? 13:45", "domingo, 14 de enero de 2018,? 13:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("es"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun hebrewDateTimeNoYear() = withLocale(Locale.forLanguageTag("iw")) { fun hebrewDateTimeNoYear() {
freezeAt(DateTime(2018, 12, 12)) { freezeAt(DateTime(2018, 12, 12)) {
assertMatches( assertMatches(
"יום ראשון, 14 בינואר( בשעה)? 13:45", "יום ראשון, 14 בינואר( בשעה)? 13:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("iw"),
FormatStyle.FULL))
} }
} }
@Test @Test
fun hebrewDateTimeWithYear() = withLocale(Locale.forLanguageTag("iw")) { fun hebrewDateTimeWithYear() {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"יום ראשון, 14 בינואר 2018( בשעה)? 13:45", "יום ראשון, 14 בינואר 2018( בשעה)? 13:45",
getRelativeDateTime(DateTime(2018, 1, 14, 13, 45, 1).millis, is24HourFormat, DateStyle.FULL) DateUtilities.getRelativeDateTime(
) ApplicationProvider.getApplicationContext(),
DateTime(2018, 1, 14, 13, 45, 1).millis,
Locale.forLanguageTag("iw"),
FormatStyle.FULL))
} }
} }
private fun assertMatches(regex: String, actual: String) = private fun assertMatches(regex: String, actual: String) =
assertTrue("expected=$regex\nactual=$actual", actual.matches(Regex(regex))) assertTrue("expected=$regex\nactual=$actual", actual.matches(Regex(regex)))
private val is24HourFormat: Boolean
get() = InstrumentationRegistry.getInstrumentation().targetContext.is24HourFormat
} }

@ -1,17 +1,16 @@
package com.todoroo.andlib.utility package com.todoroo.andlib.utility
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.tasks.Freeze import org.tasks.Freeze
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.Locale import java.time.format.FormatStyle
import java.util.*
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RelativeDayTest { class RelativeDayTest {
@ -68,12 +67,12 @@ class RelativeDayTest {
checkRelativeDay(DateTime().plusDays(7), "January 7, 2014", "Jan 7, 2014") checkRelativeDay(DateTime().plusDays(7), "January 7, 2014", "Jan 7, 2014")
} }
private fun checkRelativeDay(now: DateTime, full: String, abbreviated: String) = runBlocking { private fun checkRelativeDay(now: DateTime, full: String, abbreviated: String) {
assertEquals( assertEquals(
full, full,
getRelativeDay(now.millis, DateStyle.LONG)) DateUtilities.getRelativeDay(ApplicationProvider.getApplicationContext(), now.millis, Locale.US, FormatStyle.LONG))
assertEquals( assertEquals(
abbreviated, abbreviated,
getRelativeDay(now.millis)) DateUtilities.getRelativeDay(ApplicationProvider.getApplicationContext(), now.millis, Locale.US, FormatStyle.MEDIUM))
} }
} }

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

@ -3,7 +3,6 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -12,9 +11,6 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.* 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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskContainerMaker.PARENT import org.tasks.makers.TaskContainerMaker.PARENT
@ -28,7 +24,6 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: TaskAdapter private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>() private val tasks = ArrayList<TaskContainer>()
@ -38,7 +33,7 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
super.setUp() super.setUp()
tasks.clear() tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(object : TaskAdapterDataSource { adapter.setDataSource(object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position] override fun getItem(position: Int) = tasks[position]
@ -199,18 +194,22 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
private fun addTask(vararg properties: PropertyValue<in TaskContainer?, *>) = runBlocking { private fun addTask(vararg properties: PropertyValue<in TaskContainer?, *>) = runBlocking {
val t = newTaskContainer(*properties) val t = newTaskContainer(*properties)
tasks.add(t)
val task = t.task val task = t.task
taskDao.createNew(task) taskDao.createNew(task)
val caldavTask = CaldavTask(task = t.id, calendar = "calendar") val caldavTask = CaldavTask(t.id, "calendar")
if (task.parent > 0) { if (task.parent > 0) {
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent) caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent)
} }
tasks.add( caldavTask.id = caldavDao.insert(caldavTask)
t.copy( t.caldavTask = caldavTask.toSubset()
caldavTask = caldavTask.copy( }
id = caldavDao.insert(caldavTask)
) 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.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue 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.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.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -13,17 +13,18 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery 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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.GoogleTaskListMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.GoogleTaskListMaker.newGoogleTaskList
import org.tasks.makers.CaldavTaskMaker.newCaldavTask 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.PARENT
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
@ -37,11 +38,10 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: GoogleTaskManualSortAdapter private lateinit var adapter: GoogleTaskManualSortAdapter
private val tasks = ArrayList<TaskContainer>() 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 { private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position] override fun getItem(position: Int) = tasks[position]
@ -416,33 +416,33 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
preferences.clear() preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true) preferences.setBoolean(R.string.p_manual_sort, true)
tasks.clear() tasks.clear()
adapter = GoogleTaskManualSortAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) adapter = GoogleTaskManualSortAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource) adapter.setDataSource(dataSource)
} }
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking { 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 val adjustedTo = if (from < to) to + 1 else to
adapter.moved(from, adjustedTo, indent) adapter.moved(from, adjustedTo, indent)
} }
private fun checkOrder(order: Long, index: Int, parent: Long = 0) = runBlocking { 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(order, googleTask.order)
assertEquals(parent, googleTask.parent) assertEquals(parent, googleTask.parent)
} }
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking { private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking {
val task = newTask(*properties) val task = newTask(*properties)
val parent = task.parent
task.parent = 0
taskDao.createNew(task) taskDao.createNew(task)
googleTaskDao.insertAndShift( googleTaskDao.insertAndShift(
task, newGoogleTask(
newCaldavTask( with(TASK, task.id),
with(TASK, task.id), with(LIST, "1234"),
with(CALENDAR, "1234"), with(GoogleTaskMaker.PARENT, parent)),
), false)
false
)
task.id 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) })
}
}

@ -1,9 +1,12 @@
package com.todoroo.astrid.adapter 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.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao 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.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -11,12 +14,10 @@ import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.Task
import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.PARENT import org.tasks.makers.TaskMaker.PARENT
@ -32,11 +33,10 @@ class OfflineSubtaskTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: TaskAdapter private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>() private val tasks = ArrayList<TaskContainer>()
private val filter = runBlocking { MyTasksFilter.create() } private val filter = BuiltInFilterExposer.getMyTasksFilter(ApplicationProvider.getApplicationContext<Context>().resources)
private val dataSource = object : TaskAdapterDataSource { private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position] override fun getItem(position: Int) = tasks[position]
@ -48,7 +48,7 @@ class OfflineSubtaskTest : InjectingTestCase() {
super.setUp() super.setUp()
preferences.clear() preferences.clear()
tasks.clear() tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource) adapter.setDataSource(dataSource)
} }
@ -84,6 +84,6 @@ class OfflineSubtaskTest : InjectingTestCase() {
} }
private fun query() = runBlocking { private fun query() = runBlocking {
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) }) tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter, it) })
} }
} }

@ -2,7 +2,9 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -11,9 +13,7 @@ import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.filters.TodayFilter
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.DUE_DATE import org.tasks.makers.TaskMaker.DUE_DATE
@ -76,7 +76,7 @@ class RecursiveLoopTest : InjectingTestCase() {
} }
private suspend fun getTasks() = taskDao.fetchTasks { private suspend fun getTasks() = taskDao.fetchTasks {
getQuery(preferences, TodayFilter.create()) getQuery(preferences, BuiltInFilterExposer.getTodayFilter(context.resources), it)
} }
private suspend fun addTask(vararg properties: PropertyValue<in Task?, *>): Long { private suspend fun addTask(vararg properties: PropertyValue<in Task?, *>): Long {

@ -1,290 +1,52 @@
package com.todoroo.astrid.alarms 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.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.data.Alarm
import org.tasks.data.createDueDate import org.tasks.data.AlarmDao
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.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule 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.DateTime
import org.tasks.time.DateTimeUtils2
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class AlarmJobServiceTest : InjectingTestCase() { class AlarmJobServiceTest : InjectingTestCase() {
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var jobs: NotificationQueue
@Inject lateinit var alarmService: AlarmService @Inject lateinit var alarmService: AlarmService
@Test @Test
fun testNoAlarms() = runBlocking { fun scheduleAlarm() = runBlocking {
testResults(emptyList(), 0) val task = newTask()
} taskDao.createNew(task)
val alarmTime = DateTime(2017, 9, 24, 19, 57)
@Test val alarm = Alarm(task.id, alarmTime.millis)
fun futureAlarmWithNoPastAlarm() = runBlocking { alarm.id = alarmDao.insert(alarm)
freezeAt(DateTime(2024, 5, 17, 23, 20)) { alarmService.scheduleAllAlarms()
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)
)
)
testResults( assertEquals(listOf(AlarmEntry(alarm)), jobs.getJobs())
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)
)
}
} }
@Test @Test
fun ignoreStaleAlarm() = runBlocking { fun ignoreStaleAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) { val alarmTime = DateTime(2017, 9, 24, 19, 57)
taskDao.insert( val task = newTask(with(REMINDER_LAST, alarmTime.endOfMinute()))
Task( taskDao.createNew(task)
dueDate = createDueDate( alarmDao.insert(Alarm(task.id, alarmTime.millis))
Task.URGENCY_SPECIFIC_DAY, alarmService.scheduleAllAlarms()
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
)
}
}
private suspend fun testResults(notifications: List<Notification>, nextAlarm: Long) { assertTrue(jobs.getJobs().isEmpty())
val actualNextAlarm = alarmService.triggerAlarms {
assertEquals(notifications, it)
it.forEach { taskDao.setLastNotified(it.taskId, DateTimeUtils2.currentTimeMillis()) }
}
assertEquals(nextAlarm, actualNextAlarm)
} }
} }

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

@ -11,12 +11,14 @@ import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao import org.tasks.data.GoogleTaskAccount
import org.tasks.data.dao.GoogleTaskListDao import org.tasks.data.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule 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
import org.tasks.makers.RemoteGtaskListMaker.newRemoteList import org.tasks.makers.RemoteGtaskListMaker.newRemoteList
import javax.inject.Inject import javax.inject.Inject
@ -27,7 +29,6 @@ class GtasksListServiceTest : InjectingTestCase() {
@Inject lateinit var taskDeleter: TaskDeleter @Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var googleTaskListDao: GoogleTaskListDao @Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var caldavDao: CaldavDao
private lateinit var gtasksListService: GtasksListService private lateinit var gtasksListService: GtasksListService
@ -43,14 +44,13 @@ class GtasksListServiceTest : InjectingTestCase() {
newRemoteList( newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "Default"))) with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "Default")))
assertEquals( assertEquals(
CaldavCalendar(id = 1, account = "account", uuid = "1", name = "Default"), newGtaskList(with(ID, 1L), with(REMOTE_ID, "1"), with(NAME, "Default")),
googleTaskListDao.getById(1L) googleTaskListDao.getById(1L))
)
} }
@Test @Test
fun testGetListByRemoteId() = runBlocking { fun testGetListByRemoteId() = runBlocking {
val list = CaldavCalendar(uuid = "1") val list = newGtaskList(with(REMOTE_ID, "1"))
list.id = googleTaskListDao.insertOrReplace(list) list.id = googleTaskListDao.insertOrReplace(list)
assertEquals(list, googleTaskListDao.getByRemoteId("1")) assertEquals(list, googleTaskListDao.getByRemoteId("1"))
} }
@ -62,24 +62,22 @@ class GtasksListServiceTest : InjectingTestCase() {
@Test @Test
fun testDeleteMissingList() = runBlocking { 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")) val taskList = newRemoteList(with(RemoteGtaskListMaker.REMOTE_ID, "2"))
setLists(taskList) setLists(taskList)
assertEquals( assertEquals(
listOf(CaldavCalendar(id = 2, account = "account", uuid = "2", name = "Default")), listOf(newGtaskList(with(ID, 2L), with(REMOTE_ID, "2"))),
googleTaskListDao.getLists("account") googleTaskListDao.getLists("account"))
)
} }
@Test @Test
fun testUpdateListName() = runBlocking { fun testUpdateListName() = runBlocking {
googleTaskListDao.insertOrReplace( googleTaskListDao.insertOrReplace(
CaldavCalendar(id = 1, uuid = "1", name = "oldName", account = "account") newGtaskList(with(ID, 1L), with(REMOTE_ID, "1"), with(NAME, "oldName")))
)
setLists( setLists(
newRemoteList( newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "newName"))) with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "newName")))
assertEquals("newName", googleTaskListDao.getById(1)!!.name) assertEquals("newName", googleTaskListDao.getById(1)!!.title)
} }
@Test @Test
@ -89,11 +87,8 @@ class GtasksListServiceTest : InjectingTestCase() {
} }
private suspend fun setLists(vararg list: TaskList) { private suspend fun setLists(vararg list: TaskList) {
val account = CaldavAccount( val account = GoogleTaskAccount("account")
username = "account", googleTaskListDao.insert(account)
uuid = "account",
)
caldavDao.insert(account)
gtasksListService.updateLists(account, listOf(*list)) 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 package com.todoroo.astrid.model
import com.todoroo.astrid.dao.TaskDao 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.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -10,7 +10,7 @@ import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeClock import org.tasks.SuspendFreeze.Companion.freezeClock
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -23,7 +23,7 @@ class TaskTest : InjectingTestCase() {
freezeClock { freezeClock {
val task = Task() val task = Task()
taskDao.createNew(task) taskDao.createNew(task)
assertEquals(currentTimeMillis(), task.creationDate) assertEquals(DateTimeUtils.currentTimeMillis(), task.creationDate)
} }
} }

@ -0,0 +1,386 @@
package com.todoroo.astrid.reminders
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_DUE
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.Companion.freezeClock
import org.tasks.R
import org.tasks.data.TaskDao
import org.tasks.date.DateTimeUtils.newDateTime
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.HIDE_TYPE
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 dontScheduleStartDateReminderWhenFlagNotSet() {
service.scheduleAlarm(
newTask(
with(ID, 1L),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE),
with(DUE_TIME, newDateTime())
)
)
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenFlagNotSet() {
service.scheduleAlarm(newTask(with(ID, 1L), with(DUE_TIME, newDateTime())))
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenTimeNotSet() {
service.scheduleAlarm(newTask(with(ID, 1L), with(REMINDERS, Task.NOTIFY_AT_DEADLINE)))
assertTrue(jobs.isEmpty())
}
@Test
fun schedulePastStartDate() {
freezeClock {
val dueDate = newDateTime().minusDays(1)
val task = newTask(
with(ID, 1L),
with(DUE_TIME, dueDate),
with(HIDE_TYPE, HIDE_UNTIL_DUE),
with(REMINDERS, Task.NOTIFY_AT_START)
)
service.scheduleAlarm(task)
verify(
ReminderEntry(
1,
dueDate.startOfDay().withHourOfDay(18).millis,
ReminderService.TYPE_START
)
)
}
}
@Test
fun scheduleFutureStartDate() {
val dueDate = newDateTime().plusDays(1)
val task = newTask(
with(ID, 1L),
with(DUE_TIME, dueDate),
with(HIDE_TYPE, HIDE_UNTIL_DUE),
with(REMINDERS, Task.NOTIFY_AT_START)
)
service.scheduleAlarm(task)
verify(
ReminderEntry(
1,
dueDate.startOfDay().withHourOfDay(18).millis,
ReminderService.TYPE_START
)
)
}
@Test
fun schedulePastDueDate() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, 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, 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 = 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, newDateTime().plusDays(1)),
with(COMPLETION_TIME, 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, newDateTime().plusDays(1)),
with(DELETION_TIME, newDateTime()),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenAlreadyReminded() {
val now = 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, newDateTime()),
with(SNOOZE_TIME, newDateTime().minusMinutes(5)),
with(REMINDER_LAST, 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 = 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
freezeClock {
val now = 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
freezeClock {
val now = 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
freezeClock {
val now = 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 = 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 package com.todoroo.astrid.repeats
import org.tasks.data.entity.Task import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.service.TaskCompleter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.TaskDao import org.tasks.data.TaskDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule 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 import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class RepeatWithSubtasksTests : InjectingTestCase() { class RepeatWithSubtasksTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskCompleter: TaskCompleter @Inject lateinit var repeat: RepeatTaskHelper
@Test @Test
fun uncompleteGrandchildren() = runBlocking { fun uncompleteGrandchildren() = runBlocking {
val grandparent = taskDao.createNew( val grandparent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY")))
Task( val parent = taskDao.createNew(newTask(with(PARENT, grandparent)))
recurrence = "RRULE:FREQ=DAILY" val child = taskDao.createNew(newTask(
) with(PARENT, parent),
) with(COMPLETION_TIME, DateTime())
val parent = taskDao.createNew( ))
Task(
parent = grandparent
)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
assertTrue(taskDao.fetch(child)!!.isCompleted) repeat.handleRepeat(taskDao.fetch(grandparent)!!)
taskCompleter.setComplete(grandparent)
assertFalse(taskDao.fetch(child)!!.isCompleted) assertFalse(taskDao.fetch(child)!!.isCompleted)
} }
@Test @Test
fun uncompleteGoogleTaskChildren() = runBlocking { fun uncompleteGoogleTaskChildren() = runBlocking {
val parent = taskDao.createNew( val parent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY")))
Task( val child = taskDao.createNew(newTask(
recurrence = "RRULE:FREQ=DAILY" with(PARENT, parent),
) with(COMPLETION_TIME, DateTime())
) ))
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
assertTrue(taskDao.fetch(child)!!.isCompleted)
taskCompleter.setComplete(parent) repeat.handleRepeat(taskDao.fetch(parent)!!)
assertFalse(taskDao.fetch(child)!!.isCompleted) assertFalse(taskDao.fetch(child)!!.isCompleted)
} }

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

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

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

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

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

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

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

@ -1,27 +1,27 @@
package com.todoroo.astrid.subtasks 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.dao.TaskDao
import kotlinx.coroutines.runBlocking import com.todoroo.astrid.data.Task
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.tasks.data.dao.TaskListMetadataDao import org.tasks.data.TaskListMetadataDao
import org.tasks.data.entity.Task
import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
abstract class SubtasksTestCase : InjectingTestCase() { abstract class SubtasksTestCase : InjectingTestCase() {
lateinit var updater: SubtasksFilterUpdater lateinit var updater: SubtasksFilterUpdater
lateinit var filter: AstridOrderingFilter lateinit var filter: Filter
@Inject lateinit var taskListMetadataDao: TaskListMetadataDao @Inject lateinit var taskListMetadataDao: TaskListMetadataDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
filter = runBlocking { MyTasksFilter.create() } filter = BuiltInFilterExposer.getMyTasksFilter(InstrumentationRegistry.getTargetContext().resources)
preferences.clear(SubtasksFilterUpdater.ACTIVE_TASKS_ORDER) preferences.clear(SubtasksFilterUpdater.ACTIVE_TASKS_ORDER)
updater = SubtasksFilterUpdater(taskListMetadataDao, taskDao) updater = SubtasksFilterUpdater(taskListMetadataDao, taskDao)
} }

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

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

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

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

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

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

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

@ -2,6 +2,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.andlib.utility.DateUtilities.now
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -9,15 +10,11 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Test import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt 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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskContainerMaker import org.tasks.makers.TaskContainerMaker
import org.tasks.makers.TaskContainerMaker.CREATED import org.tasks.makers.TaskContainerMaker.CREATED
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -104,24 +101,22 @@ class CaldavDaoShiftTests : InjectingTestCase() {
fun ignoreMovedTasksWhenShiftingDown() = runBlocking { fun ignoreMovedTasksWhenShiftingDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17) val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created)) addTask(with(CREATED, created))
caldavDao.update(caldavDao.getTask(tasks[0].id).apply { this?.deleted = caldavDao.update(caldavDao.getTask(tasks[0].id).apply { this?.deleted = now() }!!)
currentTimeMillis()
}!!)
caldavDao.shiftDown("calendar", 0, created.toAppleEpoch()) caldavDao.shiftDown("calendar", 0, created.toAppleEpoch())
assertNull(taskDao.fetch(tasks[0].id)!!.order) assertNull(caldavDao.getTasks(tasks[0].id)[0].order)
} }
@Test @Test
fun ignoreDeletedTasksWhenShiftingDown() = runBlocking { fun ignoreDeletedTasksWhenShiftingDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17) val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created)) 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()) caldavDao.shiftDown("calendar", 0, created.toAppleEpoch())
assertNull(taskDao.fetch(tasks[0].id)!!.order) assertNull(caldavDao.getTasks(tasks[0].id)[0].order)
} }
@Test @Test
@ -139,11 +134,10 @@ class CaldavDaoShiftTests : InjectingTestCase() {
} }
private suspend fun checkOrder(dateTime: DateTime?, task: TaskContainer) { private suspend fun checkOrder(dateTime: DateTime?, task: TaskContainer) {
val order = taskDao.fetch(task.id)!!.order
if (dateTime == null) { if (dateTime == null) {
assertNull(order) assertNull(caldavDao.getTask(task.id)!!.order)
} else { } 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?, *>) { private suspend fun addTask(calendar: String, vararg properties: PropertyValue<in TaskContainer?, *>) {
val t = TaskContainerMaker.newTaskContainer(*properties) val t = TaskContainerMaker.newTaskContainer(*properties)
tasks.add(t)
val task = t.task val task = t.task
taskDao.createNew(task) taskDao.createNew(task)
val caldavTask = CaldavTask(task = t.id, calendar = calendar) val caldavTask = CaldavTask(t.id, calendar)
if (task.parent > 0) { if (task.parent > 0) {
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent) caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent)
} }
tasks.add( caldavTask.id = caldavDao.insert(caldavTask)
t.copy( t.caldavTask = caldavTask.toSubset()
caldavTask = caldavTask.copy(
id = caldavDao.insert(caldavTask)
)
)
)
} }
}
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.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.*
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test 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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.CREATION_TIME import org.tasks.makers.TaskMaker.CREATION_TIME
@ -33,7 +27,8 @@ class CaldavDaoTests : InjectingTestCase() {
fun insertNewTaskAtTopOfEmptyList() = runBlocking { fun insertNewTaskAtTopOfEmptyList() = runBlocking {
val task = newTask() val task = newTask()
taskDao.createNew(task) 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) checkOrder(null, task.id)
} }
@ -45,8 +40,9 @@ class CaldavDaoTests : InjectingTestCase() {
val second = newTask(with(CREATION_TIME, created.plusSeconds(1))) val second = newTask(with(CREATION_TIME, created.plusSeconds(1)))
taskDao.createNew(first) taskDao.createNew(first)
taskDao.createNew(second) taskDao.createNew(second)
caldavDao.insert(first, CaldavTask(task = first.id, calendar = "calendar"), true) caldavDao.insert(first, CaldavTask(first.id, "calendar"), true)
caldavDao.insert(second, CaldavTask(task = second.id, calendar = "calendar"), true)
caldavDao.insert(second, CaldavTask(second.id, "calendar"), true)
checkOrder(null, first.id) checkOrder(null, first.id)
checkOrder(created.minusSeconds(1), second.id) checkOrder(created.minusSeconds(1), second.id)
@ -59,8 +55,9 @@ class CaldavDaoTests : InjectingTestCase() {
val second = newTask(with(CREATION_TIME, created.plusSeconds(1))) val second = newTask(with(CREATION_TIME, created.plusSeconds(1)))
taskDao.createNew(first) taskDao.createNew(first)
taskDao.createNew(second) taskDao.createNew(second)
caldavDao.insert(first, CaldavTask(task = first.id, calendar = "calendar"), false) caldavDao.insert(first, CaldavTask(first.id, "calendar"), false)
caldavDao.insert(second, CaldavTask(task = second.id, calendar = "calendar"), false)
caldavDao.insert(second, CaldavTask(second.id, "calendar"), false)
checkOrder(null, first.id) checkOrder(null, first.id)
checkOrder(null, second.id) checkOrder(null, second.id)
@ -73,8 +70,9 @@ class CaldavDaoTests : InjectingTestCase() {
val second = newTask(with(CREATION_TIME, created)) val second = newTask(with(CREATION_TIME, created))
taskDao.createNew(first) taskDao.createNew(first)
taskDao.createNew(second) taskDao.createNew(second)
caldavDao.insert(first, CaldavTask(task = first.id, calendar = "calendar"), false) caldavDao.insert(first, CaldavTask(first.id, "calendar"), false)
caldavDao.insert(second, CaldavTask(task = second.id, calendar = "calendar"), false)
caldavDao.insert(second, CaldavTask(second.id, "calendar"), false)
checkOrder(null, first.id) checkOrder(null, first.id)
checkOrder(created.plusSeconds(1), second.id) checkOrder(created.plusSeconds(1), second.id)
@ -84,14 +82,16 @@ class CaldavDaoTests : InjectingTestCase() {
fun insertNewTaskAtBottomOfEmptyList() = runBlocking { fun insertNewTaskAtBottomOfEmptyList() = runBlocking {
val task = newTask() val task = newTask()
taskDao.createNew(task) 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) checkOrder(null, task.id)
} }
@Test @Test
fun noResultsForEmptyAccounts() = runBlocking { fun noResultsForEmptyAccounts() = runBlocking {
val caldavAccount = CaldavAccount(uuid = UUIDHelper.newUUID()) val caldavAccount = CaldavAccount()
caldavAccount.uuid = UUIDHelper.newUUID()
caldavDao.insert(caldavAccount) caldavDao.insert(caldavAccount)
assertTrue(caldavDao.getCaldavFilters(caldavAccount.uuid!!).isEmpty()) 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(dateTime: DateTime, task: Long) = checkOrder(dateTime.toAppleEpoch(), task)
private suspend fun checkOrder(order: Long?, task: Long) { private suspend fun checkOrder(order: Long?, task: Long) {
val sortOrder = taskDao.fetch(task)!!.order val sortOrder = caldavDao.getTask(task)!!.order
if (order == null) { if (order == null) {
assertNull(sortOrder) assertNull(sortOrder)
} else { } else {
assertEquals(order, sortOrder) assertEquals(order, sortOrder)
} }
} }
} }

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

@ -9,20 +9,15 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Test 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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.GoogleTaskMaker.LIST
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID import org.tasks.makers.GoogleTaskMaker.PARENT
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT import org.tasks.makers.GoogleTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.GoogleTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.newCaldavTask 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 org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject import javax.inject.Inject
@ -32,135 +27,140 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao @Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
runBlocking { runBlocking {
caldavDao.insert(CaldavAccount(uuid = "account", accountType = TYPE_GOOGLE_TASKS)) googleTaskListDao.insert(newGtaskList())
caldavDao.insert(CaldavCalendar(account = "account", uuid = "calendar"))
} }
} }
@Test @Test
fun insertAtTopOfEmptyList() = runBlocking { fun insertAtTopOfEmptyList() = runBlocking {
insertTop(newCaldavTask(with(REMOTE_ID, "1234"))) insertTop(newGoogleTask(with(REMOTE_ID, "1234")))
val tasks = googleTaskDao.getByLocalOrder("calendar") val tasks = googleTaskDao.getByLocalOrder("1")
assertEquals(1, tasks.size.toLong()) assertEquals(1, tasks.size.toLong())
val task = tasks[0] val task = tasks[0]
assertEquals("1234", googleTaskDao.getByTaskId(task.id)?.remoteId) assertEquals("1234", task.remoteId)
assertEquals(0L, task.order) assertEquals(0, task.order)
} }
@Test @Test
fun insertAtBottomOfEmptyList() = runBlocking { fun insertAtBottomOfEmptyList() = runBlocking {
insertBottom(newCaldavTask(with(REMOTE_ID, "1234"))) insertBottom(newGoogleTask(with(REMOTE_ID, "1234")))
val tasks = googleTaskDao.getByLocalOrder("calendar") val tasks = googleTaskDao.getByLocalOrder("1")
assertEquals(1, tasks.size.toLong()) assertEquals(1, tasks.size.toLong())
val task = tasks[0] val task = tasks[0]
assertEquals("1234", googleTaskDao.getByTaskId(task.id)?.remoteId) assertEquals("1234", task.remoteId)
assertEquals(0L, task.order) assertEquals(0, task.order)
} }
@Test @Test
fun getPreviousIsNullForTopTask() = runBlocking { fun getPreviousIsNullForTopTask() = runBlocking {
insert(newCaldavTask()) googleTaskDao.insertAndShift(newGoogleTask(), true)
assertNull(googleTaskDao.getPrevious("1", 0, 0)) assertNull(googleTaskDao.getPrevious("1", 0, 0))
} }
@Test @Test
fun getPrevious() = runBlocking { fun getPrevious() = runBlocking {
insertTop(newCaldavTask()) insertTop(newGoogleTask())
insertTop(newCaldavTask(with(REMOTE_ID, "1234"))) insertTop(newGoogleTask(with(REMOTE_ID, "1234")))
assertEquals("1234", googleTaskDao.getPrevious("calendar", 0, 1)) assertEquals("1234", googleTaskDao.getPrevious("1", 0, 1))
} }
@Test @Test
fun insertAtTopOfList() = runBlocking { fun insertAtTopOfList() = runBlocking {
insertTop(newCaldavTask(with(REMOTE_ID, "1234"))) insertTop(newGoogleTask(with(REMOTE_ID, "1234")))
insertTop(newCaldavTask(with(REMOTE_ID, "5678"))) insertTop(newGoogleTask(with(REMOTE_ID, "5678")))
val tasks = googleTaskDao.getByLocalOrder("calendar") val tasks = googleTaskDao.getByLocalOrder("1")
assertEquals(2, tasks.size.toLong()) assertEquals(2, tasks.size.toLong())
val top = tasks[0] val top = tasks[0]
assertEquals("5678", googleTaskDao.getByTaskId(top.id)?.remoteId) assertEquals("5678", top.remoteId)
assertEquals(0L, top.order) assertEquals(0, top.order)
} }
@Test @Test
fun insertAtTopOfListShiftsExisting() = runBlocking { fun insertAtTopOfListShiftsExisting() = runBlocking {
insertTop(newCaldavTask(with(REMOTE_ID, "1234"))) insertTop(newGoogleTask(with(REMOTE_ID, "1234")))
insertTop(newCaldavTask(with(REMOTE_ID, "5678"))) insertTop(newGoogleTask(with(REMOTE_ID, "5678")))
val tasks = googleTaskDao.getByLocalOrder("calendar") val tasks = googleTaskDao.getByLocalOrder("1")
assertEquals(2, tasks.size.toLong()) assertEquals(2, tasks.size.toLong())
val bottom = tasks[1] val bottom = tasks[1]
assertEquals("1234", googleTaskDao.getByTaskId(bottom.id)?.remoteId) assertEquals("1234", bottom.remoteId)
assertEquals(1L, bottom.order) assertEquals(1, bottom.order)
} }
@Test @Test
fun getTaskFromRemoteId() = runBlocking { fun getTaskFromRemoteId() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234"))) googleTaskDao.insert(newGoogleTask(with(REMOTE_ID, "1234"), with(TASK, 4)))
assertEquals(1L, googleTaskDao.getTask("1234")) assertEquals(4L, googleTaskDao.getTask("1234"))
} }
@Test @Test
fun getRemoteIdForTask() = runBlocking { fun getRemoteIdForTask() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234"))) googleTaskDao.insert(newGoogleTask(with(REMOTE_ID, "1234"), with(TASK, 4)))
assertEquals("1234", googleTaskDao.getRemoteId(1L)) assertEquals("1234", googleTaskDao.getRemoteId(4L))
} }
@Test @Test
fun moveDownInList() = runBlocking { fun moveDownInList() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false)
insert(newCaldavTask(with(REMOTE_ID, "2"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false)
insert(newCaldavTask(with(REMOTE_ID, "3"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false)
val two = getByRemoteId("2") val two = getByRemoteId("2")
googleTaskDao.move(taskDao.fetch(two.task)!!, "calendar", 0, 0) googleTaskDao.move(two, 0, 0)
assertEquals(0L, getOrder("2")) assertEquals(0, googleTaskDao.getByRemoteId("2")!!.order)
assertEquals(1L, getOrder("1")) assertEquals(1, googleTaskDao.getByRemoteId("1")!!.order)
assertEquals(2L, getOrder("3")) assertEquals(2, googleTaskDao.getByRemoteId("3")!!.order)
} }
@Test @Test
fun moveUpInList() = runBlocking { fun moveUpInList() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false)
insert(newCaldavTask(with(REMOTE_ID, "2"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false)
insert(newCaldavTask(with(REMOTE_ID, "3"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false)
val one = getByRemoteId("1") val one = getByRemoteId("1")
googleTaskDao.move(taskDao.fetch(one.task)!!, "calendar", 0, 1) googleTaskDao.move(one, 0, 1)
assertEquals(0L, getOrder("2")) assertEquals(0, googleTaskDao.getByRemoteId("2")!!.order)
assertEquals(1L, getOrder("1")) assertEquals(1, googleTaskDao.getByRemoteId("1")!!.order)
assertEquals(2L, getOrder("3")) assertEquals(2, googleTaskDao.getByRemoteId("3")!!.order)
} }
@Test @Test
fun moveToTop() = runBlocking { fun moveToTop() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false)
insert(newCaldavTask(with(REMOTE_ID, "2"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false)
insert(newCaldavTask(with(REMOTE_ID, "3"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false)
val three = getByRemoteId("3") val three = getByRemoteId("3")
googleTaskDao.move(taskDao.fetch(three.task)!!, "calendar", 0, 0) googleTaskDao.move(three, 0, 0)
assertEquals(0L, getOrder("3")) assertEquals(0, googleTaskDao.getByRemoteId("3")!!.order)
assertEquals(1L, getOrder("1")) assertEquals(1, googleTaskDao.getByRemoteId("1")!!.order)
assertEquals(2L, getOrder("2")) assertEquals(2, googleTaskDao.getByRemoteId("2")!!.order)
} }
@Test @Test
fun moveToBottom() = runBlocking { fun moveToBottom() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "1")), false)
insert(newCaldavTask(with(REMOTE_ID, "2"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "2")), false)
insert(newCaldavTask(with(REMOTE_ID, "3"))) googleTaskDao.insertAndShift(newGoogleTask(with(REMOTE_ID, "3")), false)
val one = getByRemoteId("1") val one = getByRemoteId("1")
googleTaskDao.move(taskDao.fetch(one.task)!!, "calendar", 0, 2) googleTaskDao.move(one, 0, 2)
assertEquals(0L, getOrder("2")) assertEquals(0, googleTaskDao.getByRemoteId("2")!!.order)
assertEquals(1L, getOrder("3")) assertEquals(1, googleTaskDao.getByRemoteId("3")!!.order)
assertEquals(2L, getOrder("1")) 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 @Test
fun dontAllowEmptyParent() = runBlocking { 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") googleTaskDao.updatePosition("1234", "", "0")
@ -169,7 +169,7 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Test @Test
fun updatePositionWithNullParent() = runBlocking { 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") googleTaskDao.updatePosition("1234", null, "0")
@ -178,7 +178,7 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Test @Test
fun updatePosition() = runBlocking { 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") googleTaskDao.updatePosition("1234", "abcd", "0")
@ -187,87 +187,86 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Test @Test
fun updateParents() = runBlocking { fun updateParents() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123"))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_PARENT, "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 @Test
fun updateParentsByList() = runBlocking { fun updateParentsByList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123"))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_PARENT, "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 @Test
fun updateParentsMustMatchList() = runBlocking { fun updateParentsMustMatchList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123"))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "2"), with(REMOTE_PARENT, "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 @Test
fun updateParentsByListMustMatchList() = runBlocking { fun updateParentsByListMustMatchList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123"))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "2"), with(REMOTE_PARENT, "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 @Test
fun ignoreEmptyStringWhenUpdatingParents() = runBlocking { fun ignoreEmptyStringWhenUpdatingParents() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, ""))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_ID, ""), with(REMOTE_PARENT, ""))) 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 @Test
fun ignoreEmptyStringWhenUpdatingParentsForList() = runBlocking { fun ignoreEmptyStringWhenUpdatingParentsForList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, ""))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1"), with(REMOTE_ID, "")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_ID, ""), with(REMOTE_PARENT, ""))) googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(REMOTE_ID, ""), with(REMOTE_PARENT, "")))
caldavDao.updateParents("1") googleTaskDao.updateParents("1")
assertEquals(0, taskDao.fetch(2)!!.parent)
}
private suspend fun getOrder(remoteId: String): Long? { assertEquals(0, googleTaskDao.getByTaskId(2)!!.parent)
return taskDao.fetch(googleTaskDao.getByRemoteId(remoteId)!!.task)?.order
} }
private suspend fun insertTop(googleTask: CaldavTask) { private suspend fun insertTop(googleTask: GoogleTask) {
insert(googleTask, true) insert(googleTask, true)
} }
private suspend fun insertBottom(googleTask: CaldavTask) { private suspend fun insertBottom(googleTask: GoogleTask) {
insert(googleTask, false) insert(googleTask, false)
} }
private suspend fun insert(googleTask: CaldavTask, top: Boolean = false) { private suspend fun insert(googleTask: GoogleTask, top: Boolean) {
val task = newTask() val task = newTask()
taskDao.createNew(task) taskDao.createNew(task)
googleTaskDao.insertAndShift( googleTask.task = task.id
task, googleTaskDao.insertAndShift(googleTask, top)
googleTask.copy(task = task.id),
top
)
} }
private suspend fun getByRemoteId(remoteId: String): CaldavTask { private suspend fun getByRemoteId(remoteId: String): SubsetGoogleTask {
return googleTaskDao.getByRemoteId(remoteId)!! 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 package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue import org.junit.Assert.*
import org.junit.Test 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.InjectingTestCase
import org.tasks.injection.ProductionModule 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 import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class GoogleTaskListDaoTest : InjectingTestCase() { class GoogleTaskListDaoTest : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao @Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var caldavDao: CaldavDao
@Test @Test
fun noResultsForEmptyAccount() = runBlocking { fun noResultsForEmptyAccount() = runBlocking {
val account = CaldavAccount( val account = GoogleTaskAccount()
uuid = "user@gmail.com", account.account = "user@gmail.com"
username = "user@gmail.com", googleTaskListDao.insert(account)
)
caldavDao.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 package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.*
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.caldav.GeoUtils.toLikeString 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.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule 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.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_TIME import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.HIDE_TYPE import org.tasks.makers.TaskMaker.HIDE_TYPE
import org.tasks.makers.TaskMaker.ID import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -36,11 +36,10 @@ import javax.inject.Inject
class LocationDaoTest : InjectingTestCase() { class LocationDaoTest : InjectingTestCase() {
@Inject lateinit var locationDao: LocationDao @Inject lateinit var locationDao: LocationDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var alarmDao: AlarmDao
@Test @Test
fun getExistingPlace() = runBlocking { 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()) val place = locationDao.findPlace(48.067222.toLikeString(), 12.863611.toLikeString())
assertEquals(48.067222, place?.latitude) assertEquals(48.067222, place?.latitude)
assertEquals(12.863611, place?.longitude) assertEquals(12.863611, place?.longitude)
@ -48,7 +47,7 @@ class LocationDaoTest : InjectingTestCase() {
@Test @Test
fun getPlaceWithLessPrecision() = runBlocking { 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()) val place = locationDao.findPlace(50.754712.toLikeString(), (-2.227945).toLikeString())
assertEquals(50.7547, place?.latitude) assertEquals(50.7547, place?.latitude)
assertEquals(-2.2279, place?.longitude) assertEquals(-2.2279, place?.longitude)
@ -56,7 +55,7 @@ class LocationDaoTest : InjectingTestCase() {
@Test @Test
fun getPlaceWithMorePrecision() = runBlocking { 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) } locationDao.getPlaces().forEach { println(it) }
val place = locationDao.findPlace(36.2469.toLikeString(), (-116.8169).toLikeString()) val place = locationDao.findPlace(36.2469.toLikeString(), (-116.8169).toLikeString())
assertEquals(36.246944, place?.latitude) assertEquals(36.246944, place?.latitude)
@ -65,20 +64,20 @@ class LocationDaoTest : InjectingTestCase() {
@Test @Test
fun noActiveGeofences() = runBlocking { fun noActiveGeofences() = runBlocking {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1))) 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!!)) assertNull(locationDao.getGeofencesByPlace(place.uid!!))
} }
@Test @Test
fun activeArrivalGeofence() = runBlocking { fun activeArrivalGeofence() = runBlocking {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1))) 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!!) val geofence = locationDao.getGeofencesByPlace(place.uid!!)
@ -88,10 +87,10 @@ class LocationDaoTest : InjectingTestCase() {
@Test @Test
fun activeDepartureGeofence() = runBlocking { fun activeDepartureGeofence() = runBlocking {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1))) 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!!) val geofence = locationDao.getGeofencesByPlace(place.uid!!)
@ -101,171 +100,133 @@ class LocationDaoTest : InjectingTestCase() {
@Test @Test
fun geofenceInactiveForCompletedTask() = runBlocking { fun geofenceInactiveForCompletedTask() = runBlocking {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(COMPLETION_TIME, newDateTime()))) 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!!)) assertNull(locationDao.getGeofencesByPlace(place.uid!!))
} }
@Test @Test
fun geofenceInactiveForDeletedTask() = runBlocking { fun geofenceInactiveForDeletedTask() = runBlocking {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(DELETION_TIME, newDateTime()))) 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!!)) assertNull(locationDao.getGeofencesByPlace(place.uid!!))
} }
@Test @Test
fun ignoreArrivalForSnoozedTask() = runBlocking { fun ignoreArrivalForSnoozedTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter { freezeAt(now()).thawAfter {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
val task = taskDao.createNew(newTask()) taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().plusMinutes(15))))
alarmDao.insert( locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true)))
Alarm(
task = task, assertTrue(locationDao.getArrivalGeofences(place.uid!!, now()).isEmpty())
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())
} }
} }
@Test @Test
fun ignoreDepartureForSnoozedTask() = runBlocking { fun ignoreDepartureForSnoozedTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter { freezeAt(now()).thawAfter {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
val task = taskDao.createNew(newTask()) taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().plusMinutes(15))))
alarmDao.insert( locationDao.insert(newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true)))
Alarm(
task = task, assertTrue(locationDao.getDepartureGeofences(place.uid!!, now()).isEmpty())
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())
} }
} }
@Test @Test
fun getArrivalWithElapsedSnooze() = runBlocking { fun getArrivalWithElapsedSnooze() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter { freezeAt(now()).thawAfter {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
val task = taskDao.createNew(newTask()) taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().minusMinutes(15))))
alarmDao.insert( val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true))
Alarm( geofence.id = locationDao.insert(geofence)
task = task,
time = newDateTime().minusMinutes(15).millis, assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!, now()))
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()
))
} }
} }
@Test @Test
fun getDepartureWithElapsedSnooze() = runBlocking { fun getDepartureWithElapsedSnooze() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter { freezeAt(now()).thawAfter {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
val task = taskDao.createNew(newTask()) taskDao.createNew(newTask(with(ID, 1), with(SNOOZE_TIME, newDateTime().minusMinutes(15))))
alarmDao.insert( val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true))
Alarm( geofence.id = locationDao.insert(geofence)
task = task,
time = newDateTime().minusMinutes(15).millis, assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!, now()))
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()
))
} }
} }
@Test @Test
fun ignoreArrivalForHiddenTask() = runBlocking { fun ignoreArrivalForHiddenTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter { freezeAt(now()).thawAfter {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask( taskDao.createNew(newTask(
with(ID, 1), with(ID, 1),
with(DUE_TIME, newDateTime().plusMinutes(15)), with(DUE_TIME, newDateTime().plusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME))) 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 @Test
fun ignoreDepartureForHiddenTask() = runBlocking { fun ignoreDepartureForHiddenTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter { freezeAt(now()).thawAfter {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask( taskDao.createNew(newTask(
with(ID, 1), with(ID, 1),
with(DUE_TIME, newDateTime().plusMinutes(15)), with(DUE_TIME, newDateTime().plusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME))) 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 @Test
fun getArrivalWithElapsedHideUntil() = runBlocking { fun getArrivalWithElapsedHideUntil() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter { freezeAt(now()).thawAfter {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask( taskDao.createNew(newTask(
with(ID, 1), with(ID, 1),
with(DUE_TIME, newDateTime().minusMinutes(15)), with(DUE_TIME, newDateTime().minusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME))) with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
val geofence = Geofence(task = 1, place = place.uid, isArrival = true) val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(ARRIVAL, true))
.let { geofence.id = locationDao.insert(geofence)
it.copy(id = locationDao.insert(it))
} assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!, now()))
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!,
currentTimeMillis()
))
} }
} }
@Test @Test
fun getDepartureWithElapsedHideUntil() = runBlocking { fun getDepartureWithElapsedHideUntil() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter { freezeAt(now()).thawAfter {
val place = Place() val place = newPlace()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask( taskDao.createNew(newTask(
with(ID, 1), with(ID, 1),
with(DUE_TIME, newDateTime().minusMinutes(15)), with(DUE_TIME, newDateTime().minusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME))) with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
val geofence = Geofence(task = 1, place = place.uid, isDeparture = true) val geofence = newGeofence(with(TASK, 1), with(PLACE, place.uid), with(DEPARTURE, true))
.let { it.copy(id = locationDao.insert(it)) } geofence.id = locationDao.insert(geofence)
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!, assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!, now()))
currentTimeMillis()
))
} }
} }
} }

@ -1,51 +1,45 @@
package org.tasks.data package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with 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.dao.TaskDao
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.R 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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.GoogleTaskListMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.GoogleTaskListMaker.newGoogleTaskList
import org.tasks.makers.CaldavTaskMaker.newCaldavTask 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
import org.tasks.makers.TaskMaker.ID import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.ORDER import org.tasks.makers.TaskMaker.UUID
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class ManualGoogleTaskQueryTest : InjectingTestCase() { class ManualGoogleTaskQueryTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
private lateinit var filter: GtasksFilter private val filter: GtasksFilter = GtasksFilter(newGoogleTaskList(with(REMOTE_ID, "1234")))
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
preferences.clear() preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true) preferences.setBoolean(R.string.p_manual_sort, true)
val calendar = CaldavCalendar(uuid = "1234")
runBlocking {
caldavDao.insert(CaldavAccount())
caldavDao.insert(calendar)
}
filter = GtasksFilter(calendar)
} }
@Test @Test
@ -94,17 +88,23 @@ class ManualGoogleTaskQueryTest : InjectingTestCase() {
assertEquals(1, subtasks[2].secondarySort) 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) { private suspend fun newTask(id: Long, order: Long, parent: Long = 0) {
taskDao.insert(TaskMaker.newTask( taskDao.insert(TaskMaker.newTask(with(ID, id), with(UUID, UUIDHelper.newUUID())))
with(ID, id), googleTaskDao.insert(newGoogleTask(with(LIST, filter.list.remoteId), with(TASK, id), with(PARENT, parent), with(ORDER, order)))
with(TaskMaker.UUID, UUIDHelper.newUUID()),
with(ORDER, order),
with(PARENT, parent),
))
googleTaskDao.insert(newCaldavTask(with(CALENDAR, filter.list.uuid), with(TASK, id)))
} }
private suspend fun query(): List<TaskContainer> = taskDao.fetchTasks { 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.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test 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.InjectingTestCase
import org.tasks.injection.ProductionModule 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.ID
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject import javax.inject.Inject
@ -27,13 +29,13 @@ class TagDataDaoTest : InjectingTestCase() {
@Test @Test
fun tagDataOrderedByNameIgnoresNullNames() = runBlocking { fun tagDataOrderedByNameIgnoresNullNames() = runBlocking {
tagDataDao.insert(TagData(name = null)) tagDataDao.createNew(newTagData(with(NAME, null as String?)))
assertTrue(tagDataDao.tagDataOrderedByName().isEmpty()) assertTrue(tagDataDao.tagDataOrderedByName().isEmpty())
} }
@Test @Test
fun tagDataOrderedByNameIgnoresEmptyNames() = runBlocking { fun tagDataOrderedByNameIgnoresEmptyNames() = runBlocking {
tagDataDao.insert(TagData(name = "")) tagDataDao.createNew(newTagData(with(NAME, "")))
assertTrue(tagDataDao.tagDataOrderedByName().isEmpty()) assertTrue(tagDataDao.tagDataOrderedByName().isEmpty())
} }
@ -44,19 +46,20 @@ class TagDataDaoTest : InjectingTestCase() {
@Test @Test
fun getTagWithCaseFixesCase() = runBlocking { fun getTagWithCaseFixesCase() = runBlocking {
tagDataDao.insert(TagData(name = "Derp")) tagDataDao.createNew(newTagData(with(NAME, "Derp")))
assertEquals("Derp", tagDataDao.getTagWithCase("derp")) assertEquals("Derp", tagDataDao.getTagWithCase("derp"))
} }
@Test @Test
fun getTagsByName() = runBlocking { 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"))) assertEquals(listOf(tagData), tagDataDao.getTags(listOf("Derp")))
} }
@Test @Test
fun getTagsByNameCaseSensitive() = runBlocking { fun getTagsByNameCaseSensitive() = runBlocking {
tagDataDao.insert(TagData(name = "Derp")) tagDataDao.createNew(newTagData(with(NAME, "Derp")))
assertTrue(tagDataDao.getTags(listOf("derp")).isEmpty()) assertTrue(tagDataDao.getTags(listOf("derp")).isEmpty())
} }
@ -66,18 +69,20 @@ class TagDataDaoTest : InjectingTestCase() {
val taskTwo = newTask() val taskTwo = newTask()
taskDao.createNew(taskOne) taskDao.createNew(taskOne)
taskDao.createNew(taskTwo) taskDao.createNew(taskTwo)
val tagOne = TagData(name = "one").let { it.copy(id = tagDataDao.insert(it)) } val tagOne = newTagData(with(NAME, "one"))
val tagTwo = TagData(name = "two").let { it.copy(id = tagDataDao.insert(it)) } val tagTwo = newTagData(with(NAME, "two"))
tagDao.insert(Tag(task = taskOne.id, taskUid = taskOne.uuid, tagUid = tagOne.remoteId)) tagDataDao.createNew(tagOne)
tagDao.insert(Tag(task = taskTwo.id, taskUid = taskTwo.uuid, tagUid = tagTwo.remoteId)) 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)) assertEquals(listOf(tagOne), tagDataDao.getTagDataForTask(taskOne.id))
} }
@Test @Test
fun getEmptyTagSelections() = runBlocking { fun getEmptyTagSelections() = runBlocking {
val selections = tagDataDao.getTagSelections(listOf(1L)) val selections = tagDataDao.getTagSelections(listOf(1L))
assertTrue(selections.first.isEmpty()) assertTrue(selections.first!!.isEmpty())
assertTrue(selections.second.isEmpty()) assertTrue(selections.second!!.isEmpty())
} }
@Test @Test
@ -92,7 +97,7 @@ class TagDataDaoTest : InjectingTestCase() {
fun getEmptyPartialSelections() = runBlocking { fun getEmptyPartialSelections() = runBlocking {
newTag(1, "tag1") newTag(1, "tag1")
newTag(2, "tag1") newTag(2, "tag1")
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).first.isEmpty()) assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).first!!.isEmpty())
} }
@Test @Test
@ -106,15 +111,15 @@ class TagDataDaoTest : InjectingTestCase() {
fun getEmptyCommonSelections() = runBlocking { fun getEmptyCommonSelections() = runBlocking {
newTag(1, "tag1") newTag(1, "tag1")
newTag(2, "tag2") newTag(2, "tag2")
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).second.isEmpty()) assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).second!!.isEmpty())
} }
@Test @Test
fun getSelectionsWithNoTags() = runBlocking { fun getSelectionsWithNoTags() = runBlocking {
newTag(1) newTag(1)
val selections = tagDataDao.getTagSelections(listOf(1L)) val selections = tagDataDao.getTagSelections(listOf(1L))
assertTrue(selections.first.isEmpty()) assertTrue(selections.first!!.isEmpty())
assertTrue(selections.second.isEmpty()) assertTrue(selections.second!!.isEmpty())
} }
@Test @Test
@ -123,14 +128,14 @@ class TagDataDaoTest : InjectingTestCase() {
newTag(2) newTag(2)
val selections = tagDataDao.getTagSelections(listOf(1L, 2L)) val selections = tagDataDao.getTagSelections(listOf(1L, 2L))
assertEquals(setOf("tag1"), selections.first) assertEquals(setOf("tag1"), selections.first)
assertTrue(selections.second.isEmpty()) assertTrue(selections.second!!.isEmpty())
} }
private suspend fun newTag(taskId: Long, vararg tags: String) { private suspend fun newTag(taskId: Long, vararg tags: String) {
val task = newTask(with(ID, taskId)) val task = newTask(with(ID, taskId))
taskDao.createNew(task) taskDao.createNew(task)
for (tag in tags) { 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