Compare commits

..

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

@ -1,4 +0,0 @@
github: abaker
liberapay: tasks
patreon: tasks
custom: tasks.org/donate

@ -1,52 +0,0 @@
name: Assemble bundle
on:
push:
branches:
- main
workflow_dispatch:
workflow_call:
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@v6
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- 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@v6
with:
name: release
path: |
app/build/outputs/**
wear/build/outputs/**

@ -1,96 +0,0 @@
name: Run automated checks
on:
pull_request:
workflow_call:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Lint checks
run: bundle exec fastlane lint
- name: Archive lint reports
uses: actions/upload-artifact@v6
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@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- 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@v6
if: ${{ always() }}
with:
name: test-reports-${{ matrix.flavor }}
path: app/build/reports/**

@ -1,47 +0,0 @@
name: Update Dependency Diff
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- main
paths:
- 'gradle/libs.versions.toml'
pull_request:
paths:
- 'gradle/libs.versions.toml'
workflow_dispatch:
jobs:
update-deps:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Update dependency diffs
run: ./update_dependency_diff
- name: Commit changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add deps_*.txt
git diff --staged --quiet || git commit -m "Update dependency diffs"
git push

@ -1,32 +0,0 @@
name: Deploy
on:
workflow_dispatch:
permissions:
contents: read
env:
FASTLANE: ${{ secrets.FASTLANE }}
jobs:
bundle:
uses: ./.github/workflows/bundle.yml
secrets: inherit
deploy:
runs-on: ubuntu-latest
needs: [ bundle ]
steps:
- uses: actions/checkout@v6
- name: Fastlane key
run: |
echo "$FASTLANE" > ./fastlane.json
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: actions/download-artifact@v7
with:
name: release
path: .
- name: Deploy
run: bundle exec fastlane deploy

9
.gitignore vendored

@ -1,14 +1,7 @@
.kotlin
.idea
*.iml
.gradle
build/
*.apk
*.apks
*.aab
local.properties
Thumbs.db
/captures/
/fastlane/report.xml
/compose-metrics/
.DS_Store
Thumbs.db

@ -1 +0,0 @@
3.4.8

@ -1,70 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="wear" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false" singleton="true">
<module name="tasks.Tasks.wear.main" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="CLEAR_LOGCAT" value="true" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

@ -0,0 +1,31 @@
language: android
sudo: false
jdk: oraclejdk7
env:
matrix:
- ANDROID_SDKS=android-23,sysimg-19 ANDROID_TARGET=android-19 ANDROID_ABI=armeabi-v7a
android:
components:
- tools # https://github.com/travis-ci/travis-ci/issues/5049
- android-23
- platform-tools-23.1
- build-tools-23.0.2
- extra-android-m2repository
- extra-google-m2repository
licenses:
- 'android-sdk-license-.+'
before_install:
- echo no | android create avd --force -n test -t $ANDROID_TARGET --abi $ANDROID_ABI
- emulator -avd test -no-skin -no-audio -no-window &
- ./.wait_for_emulator.sh
- adb shell input keyevent 82 &
script:
- ./gradlew :lintGoogleplayDebug
- ./gradlew :createGoogleplayDebugAndroidTestCoverageReport
after_success:
- mv build/reports/coverage/googleplay/debug/report.xml build/reports/coverage/googleplay/debug/coverage.xml
- bash <(curl -s https://codecov.io/bash)

@ -0,0 +1,17 @@
#!/bin/bash
bootanim=""
failcounter=0
until [[ "$bootanim" =~ "stopped" ]] || [[ "$bootanim" =~ "running" ]]; do
bootanim=`adb -e shell getprop init.svc.bootanim 2>&1`
echo "$bootanim"
if [[ "$bootanim" =~ "not found" ]]; then
let "failcounter += 1"
if [[ $failcounter -gt 3 ]]; then
echo "Failed to start emulator"
exit 1
fi
fi
sleep 1
done
echo "Done"

@ -1,989 +0,0 @@
### 14.8.5 (2026-01-09)
* Widget performance improvements
* What's New now opens changelog on GitHub
* Fix automatically opening keyboard for new tasks [#4035](https://github.com/tasks/tasks/issues/4035)
* Fix parent-child cycle causing crash [#4065](https://github.com/tasks/tasks/issues/4065)
* Fix state restoration issue [#4025](https://github.com/tasks/tasks/issues/4025)
* Fix all day calendar entries created on previous day
* Fix Microsoft To Do and Google Task sync errors
* Fix multiple icons missing from widget
* Update translations
* Arabic - @fahedoudeh
* Portuguese - Paulo
* Vietnamese - @ngocanhtve
### 14.8.4 (2025-12-20)
* Fix flashing widgets [#3902](https://github.com/tasks/tasks/issues/3902)
* Fix random reminder scheduling
* Fix random reminders firing immediately on recurring tasks [#3904](https://github.com/tasks/tasks/issues/3904)
* Fix deadlock when adding new task
* Fix crash in settings when backup location unavailable [#3989](https://github.com/tasks/tasks/issues/3989)
* Fix Hebrew and Indonesian support [#3928](https://github.com/tasks/tasks/issues/3928)
* Update translations
* Asturian - Xana
* Bosnian - @hasak
* Finnish - @pHamala
* Indonesian - @erigmac
* Japanese - @array, Norara
* Persian - @theuser17
* Romanian - @ygorigor
### 14.8.3 (2025-09-16)
* Fix crash on Android 10 and below
### 14.8.2 (2025-09-14)
* Fix blank widgets on Android 16 QPR1 [#3847](https://github.com/tasks/tasks/issues/3847)
* Fix all-day calendar events [#1534](https://github.com/tasks/tasks/issues/1534)
* Fix alarm synchronization [#3859](https://github.com/tasks/tasks/issues/3859)
* Fix sync failure when migrating data from EteSync to CalDAV [#3869](https://github.com/tasks/tasks/issues/3869)
* Fix removing values from Microsoft To Do [#3862](https://github.com/tasks/tasks/issues/3862)
* Fix share invites for Nextcloud [#2386](https://github.com/tasks/tasks/issues/2386)
* Fix failure to delete source data when moving to Google Tasks [#3867](https://github.com/tasks/tasks/issues/3867)
* Fix crash when clearing completed while grouping by lists
* Update translations
* Croatian - @milotype
* Dutch - @fvbommel
* German - @MisterTechnik
* Italian - @glemco
* Serbian - @vale-decem
### 14.8.1 (2025-08-24)
* System bar scrim improvements
* Recover from Google Task 'Bad request' errors
* Improve layout on Z Folds
* Crash fixes
* Update translations
* Brazilian Portuguese - odnankenobi
* Catalan - @Crashillo, @ferranpujolcamins
* Danish - ERYpTION
* Esperanto - Don Zouras
* Galician - @Crashillo, @delthia
* Hungarian - @Antmajgra, @gthrepwood
* Italian - @ppasserini
* Korean - Jiho Min
* Polish - @Antmajgra
* Portuguese - @Crashillo
* Russian - Алексей Ежков
* Spanish - @Crashillo
### 14.8 (2025-08-02)
* Synchronize **list** icons for Tasks.org and CalDAV accounts
* Does not apply to Microsoft To Do, Google Tasks, DAVx5, EteSync, or DecSync
CC accounts
* Does not apply to tags or filters
* CalDAV server must support extensible properties, e.g. Nextcloud or sabre/dav
* Target Android 15
* Return to previous view after searching
* Remove shadow from date picker sheet
* Fix updating list names and colors for Tasks.org and CalDAV accounts
* Update translations
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Czech - @Fjuro
* Dutch - @fvbommel
* Estonian - Priit Jõerüüt
* French - @FlorianLeChat
* German - @Colorful Rhino
* Hebrew - Xo
* Italian - @ppasserini
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 14.7.4 (2025-07-12)
* @devn1x: Fix escaping quotes in iCalendar [#3645](https://github.com/tasks/tasks/pull/3645)
* Limit widget to 25 items on Android 16+
* Android 16 nerfed widget performance 😢
* Fix bug when reconfiguring widget
* Fix default widget group sort order
* Update translations
* Catalan - pitroig
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* German - @Kachelkaiser
* Serbian - @vale-decem
* Swedish - Nick Wick
* Tamil - @TamilNeram
### 14.7.3 (2025-06-13)
* Fix dynamic color
* Fix Microsoft To Do sync failure
* Fix crash after deleting last list
* Fix notifications when 'Alarms & reminders' not allowed
* Update translations
* Bulgarian - 109247019824
* Dutch - @fvbommel
* Esperanto - Don Zouras
* French - @FlorianLeChat
* Hebrew - Xo
* Japanese - M_Haruki
* Persian - @theuser17
* Portuguese - @nero-bratti
* Romanian - @ygorigor
* Russian - @yurtpage
* Spanish - @orionn333
* Swedish - @Nicklasfox
* Turkish - @emintufan
### 14.7.2 (2025-05-23)
* Remove Microsoft Authentication Library from F-Droid builds [#3581](https://github.com/tasks/tasks/issues/3581)
* Remove contacts permission added by Microsoft Authentication Library
* Enable video attachments
* Fix wallpaper theme
* Fix handling multiple attachments
* Update translations
* Arabic - abdelbasset jabrane
* Bulgarian - 109247019824
* Catalan - @Crashillo
* Czech - @Fjuro
* Danish - @catsnote
* Dutch - @fvbommel
* Esperanto - Don Zouras
* Estonian - Priit Jõerüüt
* Hungarian - Kaci
* Italian - @ppasserini
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 14.7.1 (2025-05-04)
* Fix app closing itself automatically [#3366](https://github.com/tasks/tasks/issues/3366)
* Automatically set default list when connecting Microsoft To Do account
* Update translations
* Arabic - abdelbasset jabrane, @kemo-1
* Brazilian Portuguese - Jose Delvani
* Chinese (Simplified) - Sketch6580
* French - @FlorianLeChat
* German - @Kachelkaiser
### 14.7 (2025-05-03)
* Add support for Microsoft To Do work & school accounts [#3267](https://github.com/tasks/tasks/issues/3267)
* Add ability to rename or delete local account
* Prompt to sign in or import backup on first launch
* @BeaterGhalio: Fix back button closing app after search [#3426](https://github.com/tasks/tasks/issues/3426)
* @codokie: Automirrored icons fix [#3499](https://github.com/tasks/tasks/pull/3499)
* @codokie: Fix ltr-rtl alignment for text input [#3489](https://github.com/tasks/tasks/pull/3489)
* Use system language picker on Android 33+
* Don't show 'due date' as a start date option for DAVx5, EteSync, DecSync CC [#1558](https://github.com/tasks/tasks/issues/1558)
* Prevent attempts to delete or rename Microsoft To Do default list
* Don't handle system 'Clear storage' button
* Update minimum Android version to 8
* Fix backup import dropping tags [#3556](https://github.com/tasks/tasks/issues/3556)
* Fix start date chip when grouping by start date [#3509](https://github.com/tasks/tasks/issues/3509)
* Update translations
* Brazilian Portuguese - @sobeitnow0, dedakir923
* Czech - @Fjuro
* Dutch - Jay Tromp
* German - min7-i
* Hebrew - Xo
* Portuguese - @wm-pucrs
* Russian - @hady-exc, Maksim_220 Кабанов
* Slovak - @jose1711
* Spanish - Nucl3arSnake, @diamondtipdr
* Tamil - @TamilNeram
### 14.6.2 (2025-04-06)
* Show error indicators if 'When started' or 'When due' reminders are used
without start or due times
* Fix delay when saving tasks
* Fix populating clock picker with initial value instead of 00:00
* Fix displaying selected calendar month
* Fix grouping by start date in descending order
* Update translations
* Arabic - abdelbasset jabrane
* Danish - @catsnote
* Esperanto - Don Zouras
* German - @Kachelkaiser
* Hebrew - @elid34
* Italian - @Fs00
* Slovak - @jose1711
* Turkish - @emintufan
### 14.6.1 (2025-03-30)
* Restore default sort mode for existing installs
* Fix grouping by due date descending
* Remove shadow from launcher icons
### 14.6 (2025-03-25)
* Add dynamic theme color - requires pro subscription
* Update translation
* Brazilian Portuguese - dedakir923
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Estonian - Priit Jõerüüt
* Italian - @ppasserini
* Japanese - YuzuMikan
* Swedish - @Ziron
* Ukrainian - @IhorHordiichuk
### 14.5.4 (2025-03-24)
* Updated remaining date and time pickers to Material 3
* App will remember if you change calendar or clock to text input
* Text input now supported on start and due date pickers
* Remove calendar and clock mode settings
* Open date picker to currently selected month
* Replaced upgrade pop-up with a banner [#1429](https://github.com/tasks/tasks/issues/1429)
* @hady-exc: Fix date picker time zone issues [#3248](https://github.com/tasks/tasks/pull/3248)
* Fix date time picker font scaling issues [#3437](https://github.com/tasks/tasks/issues/3437)
* Fix save task on keyboard done [#3288](https://github.com/tasks/tasks/issues/3288)
* Fix applying date time when dismissing date time pickers
* Fix 3 button navigation bar padding in landscape mode
* Fix out of memory errors in backup import/export
* Update translations
* Brazilian Portuguese - dedakir923
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Dutch - @fvbommel
* Estonian - Priit Jõerüüt
* French - @FlorianLeChat
* German - @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Romanian - @ygorigor
* Tamil - @TamilNeram
* Turkish - @emintufan
### 14.5.3 (2025-03-20)
* Updated date and time pickers to Material 3
* Remove 'Start of week' preference
* This feature can't be supported with Material 3 calendars
### 14.5.2 (2025-03-15)
* Fix items hidden under menu search bar [#3406](https://github.com/tasks/tasks/issues/3406)
* Attempt to fix layout on some foldables
* Fix checking for tasks.org account [#3397](https://github.com/tasks/tasks/issues/3397)
* Slightly reduce donation nagging frequency [#3397](https://github.com/tasks/tasks/issues/3397)
* Update translations
* Danish - Øjvind Fritjof Arnfred
* Hungarian - Kaci
* Malayalam - Clouds Liberty
* Russian - @GREAT-DNG
* Swedish - @bittin
* Tamil - @TamilNeram
### 14.5.1 (2025-03-11)
* Fix performance issue when opening search
* Fix Microsoft To Do authentication crash
* Fix crash on task list screen
* Update translation
* Brazilian Portuguese - dedakir923
* Bulgarian - 109247019824
* Chinese (Simplified) - 大王叫我来巡山
* Dutch - @fvbommel
* Esperanto - Don Zouras
* Estonian - Priit Jõerüüt
* French - @FlorianLeChat
* German - Colorful Rhino
* Italian - @ppasserini
* Kannada - Abilash S
* Persian - @mamad-zahiri
* Ukrainian - @IhorHordiichuk
### 14.5 (2025-03-04)
* Material 3 - work in progress
* Side navigation drawer
* Improve support for foldables
* Improve edge-to-edge support
* Remove options for top app bar and disabling collapsing app bar
* Some features are being removed in order to make development easier for the
upcoming desktop app. The features may return again in a future release.
* Save backup files and attachments to Nextcloud [#1289](https://github.com/tasks/tasks/issues/1289)
* Dismiss notification dialog when pressing cancel [#2116](https://github.com/tasks/tasks/issues/2116)
* Performance improvements
* Fix Microsoft To Do sync failure
* Fix missing list chips for subtasks in custom filters
* Fix for database timeouts
* Fix infinite subtask recursion
* Update translations
* Belarusian - @fobo66
* Estonian - Priit Jõerüüt
* German - Colorful Rhino
* Japanese - M_Haruki
* Nahuatl - Benjamin Bruce
* Slovak - @jose1711
* Ukrainian - @IhorHordiichuk
### 14.4.8 (2025-02-04)
* Performance improvements
* Update translations
* German - Colorful Rhino, @Kachelkaiser
* Nepali - Sagun Khatri
### 14.4.7 (2025-02-01)
* Database improvements
* Update translations
* Estonian - Priit Jõerüüt
* German - @Kachelkaiser
### 14.4.6 (2025-01-29)
* Database performance improvements
* Additional debug logging
* Update translations
* Danish - ERYpTION
* Estonian - Priit Jõerüüt
* German - @franconian, Colorful Rhino, @Kachelkaiser
* Italian - @ppasserini
* Korean - Sunjae Choi
* Nepali - Sagun Khatri
* Slovak - @jose1711
* Swedish - Nick Wick
### 14.4.5 (2025-01-22)
* Performance improvements
* DAVx5 sync performance improvements
* Update translations
* Bosnian - @hasak
* Esperanto - Don Zouras
* Estonian - Priit Jõerüüt, @dermezl
* Italian - @ppasserini
* Nepali - @sagunkhatri
### 14.4.4 (2025-01-19)
* Fix list pickers [#3269](https://github.com/tasks/tasks/issues/3269)
### 14.4.3 (2025-01-18)
* Preserve reminder recurrence when copying tasks
* Refresh task list after changing settings
* Fix missing chips for local lists
* Fix changes being lost when completing task from edit screen
* Update translations
* German - @franconian
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 14.4.2 (2025-01-16)
* Fix crash on missing account
* Update translations
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Croatian - @milotype
* Dutch - @fvbommel
* Esperanto - Don Zouras
* French - @FlorianLeChat, @CennoxX
* German - @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Russian - @hady-exc
* Slovak - @jose1711
* Ukrainian - @IhorHordiichuk
### 14.4.1 (2025-01-11)
* Microsoft To Do support [#2011](https://github.com/tasks/tasks/issues/2011)
* This feature is in early access, please report any bugs!
* Enable under 'Advanced' settings
* Add configuration option for new lines in titles
* @TonSilver - Copy comments to clipboard with long press [#3212](https://github.com/tasks/tasks/pull/3212)
* @jheld - Attempt to fix F-Droid build with colorpicker fork [#2028](https://github.com/tasks/tasks/issues/2028)
* Subscription changes
* Multiple Google Task accounts are now free to use
* Tasker plugins are now free to use
* Fix crash on empty shortcut labels
* Fix missing settings button on Android 10 and below
* Update translations
* Bulgarian - 109247019824
* Chinese (Simplified) - 大王叫我来巡山, Sketch6580
* Czech - @AtmosphericIgnition
* Dutch - @fvbommel
* Esperanto - Don Zouras
* French - @FlorianLeChat, @lfavole
* German - @franconian, Colorful Rhino
* Hungarian - Kaci
* Italian - @ppasserini
* Slovak - @jose1711
* Swedish - @Ziron, @bittin
* Turkish - @emintufan
### 14.3.1 (2025-01-02)
* Fix edit screen disappearing on rotation
* Fix notification bundling issue
* Fix scrolling in custom filter settings
* Remove map theme and desaturation options
* Update translations
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - 大王叫我来巡山
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @p-rogalski
* Italian - @ppasserini
* Korean - Sunjae Choi
* Swedish - @bittin
### 14.3 (2024-12-24)
* "Add widget to home screen" shortcut in list settings
* "Add shortcut to home screen" shortcut in list settings
* Shortcuts use list icon and color
* Fix long running sync indicators [#3045](https://github.com/tasks/tasks/issues/3045)
* @hady-exc: Migrate list setting screens to Compose [#3163](https://github.com/tasks/tasks/pull/3163)
* Update translations
* Bosnian - @hasak
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Esperanto - Don Zouras
* Finnish - @pHamala, @Ricky-Tigg
* German - @p-rogalski, @franconian, @Atalanttore
* Hungarian - Kaci
* Italian - @ppasserini
* Korean - Sunjae Choi
* Spanish - gallegonovato
* Swedish - Nick Wick
### 14.2.1 (2024-12-03)
* Fix save button when 'Back button saves task' is enabled [#3149](https://github.com/tasks/tasks/issues/3149)
* Fix customizing edit screen order screen
### 14.2 (2024-12-02)
* Updated edit screen task title
* Show full title
* Removed collapse on scroll
* Removed floating action button
* Add separate alarms and reminders warning
* Capitalize tag picker text field
* Update translations
* Bulgarian - @StoyanDimitrov
* Catalan - raulmagdalena
* Chinese (Simplified) - 大王叫我来巡山
* Dutch - @fvbommel
* French - @FlorianLeChat
* Italian - @ppasserini
* Spanish - gallegonovato
* Ukrainian - @nathalier
### 14.1.1 (2024-11-26)
* Show warning when quiet hours are in effect
* Fix escape character in some localizations [#3046](https://github.com/tasks/tasks/issues/3046)
* Fix comment delete button color [#3102](https://github.com/tasks/tasks/issues/3102)
* Update translations
* Bosnian - @hasak
* Bulgarian - @StoyanDimitrov
* Catalan - raulmagdalena
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Dutch - @fvbommel
* Esperanto - Don Zouras
* French - @FlorianLeChat
* Hungarian - Kaci
* Italian - @ppasserini
* Polish - @rom4nik
* Spanish - gallegonovato
* Swedish - Nick Wick
### 14.1 (2024-11-20)
* Add 'Help & Feedback > Send application logs'
* Delete snoozed reminders when completing tasks
* Fix duplicated tasks when using 'Share' [#2404](https://github.com/tasks/tasks/issues/2404)
* Don't show sync indicator on startup when sync is not used
* Update translations
* Bosnian - @hasak
* Brazilian Portuguese - kowih83264
* Croatian - @milotype
* German - min7-i
### 14.0.1 (2024-11-10)
* Fix widget crash
* Fix EteSync sync failure [#3092](https://github.com/tasks/tasks/issues/3092)
* Minor Wear OS improvements
* Update translations
* Hungarian - Kaci
* Italian - @ppasserini
* Kannada - @historicattle
* Marathi - @historicattle
* Spanish - gallegonovato
* Swedish - @bittin
### 14.0 (2024-11-05)
* Wear OS support (Google Play only)
* Move drawer items to top unless searching
* Fix drawer item layout issues
* Update translations
* Brazilian Portuguese - Nicolas Suzuki, pogoyar888
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - 大王叫我来巡山
* Chinese (Traditional) - hugoalh
* Dutch - Luna, @fvbommel
* French - @FlorianLeChat
* German - @p-rogalski, @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Spanish - gallegonovato
* Swedish - @bittin
* Turkish - @oersen
* Ukrainian - @IhorHordiichuk
### 13.11.2 (2024-09-29)
* Target Android 14
* Fix crash in location picker [#2990](https://github.com/tasks/tasks/issues/2990)
* Fix SQLite crash [#3045](https://github.com/tasks/tasks/issues/3045)
* Update translations
* Arabic - @sanabel-al-firdaws
* Belarusian - @katalim
* Brazilian Portuguese - Jose Delvani
* Catalan - raulmagdalena, @truita
* Chinese (Traditional) - @abc0922001
* Croatian - @milotype
* Czech - atmosphericignition
* Danish - Tntdruid, Luna
* Dutch - @VIMVa
* Esperanto - Don Zouras
* Estonian - @dermezl
* German - @Atalanttore, @tct123
* Italian - @ppasserini
* Norwegian Bokmål - @RonnyAL
* Swedish - @JonatanWick, @bittin
### 13.11.1 (2024-07-15)
* Fix crash when collapsing list picker sections
* Fix crash in database migration
* Enabled Managed DAVx5
* Update translations
* Bulgarian - @StoyanDimitrov
### 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
* Catalan - @Seveorr, @jtorrensamer
* Chinese (Simplified) - 大王叫我来巡山
* Chinese (Traditional) - hugoalh
* French - @FlorianLeChat
* Spanish - gallegonovato
* Turkish - @oersen
* Ukrainian - @IhorHordiichuk
### 13.10 (2024-07-05)
* Add search bar to drawer
* Add search bar to list picker
* Move 'Manage drawer' to ⚙️ > Navigation drawer
* Android 13+ users must grant additional reminder permissions
* Fix completing task multiple times from notification
* Fix deleting new subtasks from edit screen
* ~~Enable Managed DAVx5~~
* Update translations
* Arabic - @islam2hamy
* Brazilian Portuguese - Jose Delvani
* Chinese (Simplified) - 大王叫我来巡山
* Chinese (Traditional) - hugoalh
* Croatian - @milotype
* Finnish - Rami Lehtinen, @CSharpest
* German - min7-i
* Spanish - gallegonovato
* Turkish - @oersen
### 13.9.9 (2024-05-30)
* Fix import backup crashes
* Fix showing completed subtasks in edit screen
### 13.9.7 (2024-05-23)
* Add default reminders when adding start/due dates to existing tasks [#1846](https://github.com/tasks/tasks/issues/1846)
* Fix import backup crash
### 13.9.6 (2024-05-18)
* Fix widget crash [#2873](https://github.com/tasks/tasks/issues/2873)
* Fix recurrence unable to finish [#2874](https://github.com/tasks/tasks/issues/2874)
* Fix edit screen being cleared when reopening app [#2857](https://github.com/tasks/tasks/issues/2857)
* Fix performance regressions
* Simplified internal alarm scheduling logic
* Update translations
* Arabic - @islam2hamy
* Bulgarian - @StoyanDimitrov
### 13.9 (2024-05-01)
* @elmuffo: Add swipe-to-snooze [#2839](https://github.com/tasks/tasks/pull/2839)
* @IlyaBizyaev: Add option to use quick tile without unlocking device [#2847](https://github.com/tasks/tasks/pull/2847)
* @liz-desartiges: Add support for Z Flip 5 cover screen [#2843](https://github.com/tasks/tasks/pull/2843)
* @purushyb: Fix drawer not updating after editing items [#2855](https://github.com/tasks/tasks/pull/2855)
* @hady-exc: Migrate tag picker screen to Compose [#2849](https://github.com/tasks/tasks/pull/2849)
* @yurtpage: Add Russian app store description [#2848](https://github.com/tasks/tasks/pull/2848)
* Fix duplicate notifications [#2835](https://github.com/tasks/tasks/issues/2835)
* Fix adding '(Completed)' to calendar entries [#2832](https://github.com/tasks/tasks/issues/2832)
* Fix hiding empty items from drawer [#2831](https://github.com/tasks/tasks/issues/2831)
* Exclude old snoozed tasks from snoozed task filter
* Update translations
* Brazilian Portuguese - @mayhmemo, @gorgonun
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Esperanto - Don Zouras
* French - Lionel HANNEQUIN
* German - sorifukobexomajepasiricupuva33, min7-i
* Portuguese - @fparri, @laralem
* Spanish - gallegonovato
* Swedish - @JonatanWick
* Turkish - @emintufan, @oersen
### 13.8.1 (2024-03-24)
* Fix copy causing duplicate Google Tasks
* Fix navigation drawer crash
* Fix backup import dropping tasks
### 13.8 (2024-03-22)
* Dynamic widget theme (name-your-price subscription required)
* Replace 'until' with 'ends on' for repeating tasks [#2797](https://github.com/tasks/tasks/pull/2797) - @akwala
* Fix loading selected list on startup [#2777](https://github.com/tasks/tasks/issues/2777)
* Fix repeating tasks ending one day early
* Fix repeating task crash
* Fix backup import crash
* Fix Astrid manual ordering crash in widget
* Update translations
* Brazilian Portuguese - @mayhmemo
* Bulgarian - @StoyanDimitrov
* Catalan - @ferranpujolcamins
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Czech - Odweta
* German - @macpac59
* Italian - @ppasserini
* Spanish - gallegonovato
* Swedish - @bittin
* Ukrainian - @IhorHordiichuk
* Vietnamese - @ngocanhtve
### 13.7 (2024-02-07)
* Fix returning to previous filter after search [#2700](https://github.com/tasks/tasks/pull/2700)
* Fix wearable notifications on Android 14+
* Fix issue causing repeating tasks to not repeat
* Fix dragging a task into a subtask in another list
* Rewrote navigation drawer in Jetpack Compose
* Internal changes to navigation
* Enable multi-select when adding attachments
* Show count of tasks to be deleted when clearing completed
* Include hidden subtasks when clearing completed [#2724](https://github.com/tasks/tasks/issues/2724)
* Don't show hidden or completed tasks in snoozed filter
* Remove markdown from repeating task snackbar
* Update translations
* Azerbaijani - Shaban Mamedov
* Bulgarian - @StoyanDimitrov
* Catalan - raulmagdalena
* Chinese (Simplified) - 大王叫我来巡山
* Chinese (Traditional) - @abc0922001
* Croatian - @milotype
* Dutch - @mm4c
* Esperanto - Don Zouras
* Finnish - @millerii
* French - J. Lavoie
* German - @CennoxX
* Hebrew - @elig0n
* Interlingua - @softinterlingua
* Odia - @SubhamJena
* Persian - @Monirzadeh
* Spanish - gallegonovato
* Swedish - @bittin
* Turkish - @oersen
* Ukrainian - Сергій
* Vietnamese - @ngocanhtve
### 13.6.3 (2023-11-25)
* Revert "Preserve modification times on initial sync" [#2460](https://github.com/tasks/tasks/issues/2640)
* Fix unnecessary DecSync work
### 13.6.2 (2023-10-30)
* Fix updating modification timestamp on edits
### 13.6.1 (2023-10-27)
* Push pending changes when app is backgrounded
* Don't require internet connection for DAVx5/EteSync/DecSync sync
* Don't perform background sync for DAVx5/EteSync/DecSync
* Background sync is performed by the sync app
* Preserve modification times on initial sync [#2496](https://github.com/tasks/tasks/issues/2496)
* Replace deprecated method call [#2547](https://github.com/tasks/tasks/pull/2547) - @kmj-99
* Improve task list scrolling performance
* Fix hourly recurrence bug
* Update translations
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Czech - @ceskyDJ
* Finnish - @millerii
* French - Lionel HANNEQUIN, Bruno Duyé
* Japanese - Kazushi Hayama
* Portuguese - @loucurapt
* Romanian - @ygorigor
* Swedish - @bittin
### 13.6 (2023-10-07)
* Change priority with multi-select [#2257](https://github.com/tasks/tasks/pull/2452) - @vulewuxe86
* Automatically select newly copied tasks [#2246](https://github.com/tasks/tasks/pull/2446) - @vulewuxe86
* Reduce minimum size for widgets [#2436](https://github.com/tasks/tasks/pull/2436) - @histefanhere
* Replace deprecated method call [#2526](https://github.com/tasks/tasks/pull/2526) - @kmj-99
* Improve handling text shared to Tasks [#2485](https://github.com/tasks/tasks/issues/2485)
* Use notification audio stream for completion sound
* Notification preference 'More settings' opens channel settings directly
* Respect 'New tasks on top' preference when creating subtasks
* Automatically add due dates for recurring tasks
* Fix crash on startup
* Update translations
* Brazilian Portuguese - @gorgonun
* Bulgarian - @StoyanDimitrov, @salif
* Catalan - Joan Montané
* Chinese (Simplified) - Poesty Li
* Chinese (Traditional) - @abc0922001
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @qwerty287, deep map, @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Japanese - Kazushi Hayama, Naga
* Spanish - @FlorianLeChat
* Swedish - @Anaemix, @bittin
* Turkish - @emintufan, @oersen
* Ukrainian - @IhorHordiichuk
### 13.5.1 (2023-08-02)
* Fix crash when importing Google Tasks from a backup file
* Added Burmese translations - @htetoh
* Update translations
* Chinese (Simplified) - Poesty Li
* Croatian - @milotype
* Japanese - Kazushi Hayama
* Polish - @alex-ter
* Russian - @alex-ter
* Ukrainian - @IhorHordiichuk
* Vietnamese - @unbiaseduser
### 13.5 (2023-07-28)
* New custom recurrence picker
* Update translations
* Bulgarian - @StoyanDimitrov
* Czech - @ceskyDJ
* Dutch - @fvbommel
* French - @FlorianLeChat
* Italian - @ppasserini
* Spanish - @FlorianLeChat
### 13.4 - (2023-07-16)
* Sorting improvements
* Add subtask sort configuration
* Update sort menu button design
* Don't show subtasks of hidden tasks in 'My Tasks'
* Fix Google Tasks sync issue
* Update translations
* Bulgarian - @StoyanDimitrov
* Catalan - @and4po, Eudald Puy Polls
* Croatian - @milotype
* Dutch - @fvbommel
* German - @schneidr
* Hungarian - Kaci
* Japanese - Naga
* Korean - Sunjae Choi
* Portuguese - @laralem
* Swedish - @bittin
### 13.3.2 - (2023-06-02)
* Sorting improvements
* Configure sort grouping
* Configure sorting within sort group
* Configure completed task sorting
* Fix Google Task list chips showing on widget
* Update translations
* Bulgarian - @StoyanDimitrov
* Catalan - @and4po
* Chinese (Simplified) - Poesty Li
* Croatian - @milotype
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @qwerty287, @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Spanish - @FlorianLeChat
* Ukrainian - @IhorHordiichuk
### 13.2.4 - (2023-05-24)
* Add 'By list' sort mode [#1265](https://github.com/tasks/tasks/issues/1265)
* Save task when pressing done [#2125](https://github.com/tasks/tasks/pull/2125)
* Use ISO 8601 date formatting for backup filenames [#1550](https://github.com/tasks/tasks/pull/1550)
* Fix filter sorting bug [#1561](https://github.com/tasks/tasks/issues/1561)
* Fix manual sorting crash [#2141](https://github.com/tasks/tasks/issues/2141)
* Fix manual sorting bug [#2101](https://github.com/tasks/tasks/issues/2101)
* Fix multiple accounts on same server [#2301](https://github.com/tasks/tasks/issues/2301)
* Don't set `COUNT=0` on recurrence rules [#2158](https://github.com/tasks/tasks/issues/2158)
* Improve task list performance [#2062](https://github.com/tasks/tasks/issues/2062)
* Attempt to hide inactive widgets in settings [#2145](https://github.com/tasks/tasks/issues/2145)
* Disable persistent reminders on Android 14+
* Android 14+ no longer supports persistent reminders 😢
* Fix notifications on Android 14
* Fix crash when missing exact alarm permissions
* Update logic for adding default reminders during sync
* Don't add reminders on initial sync
* Don't add reminders if other client supports reminder sync
* Internal database changes
* You will need to reconfigure any widgets that were set to display a Google
Task list or filter. Sorry for the interruption!
* Add Odia translations - @SubhamJena
* Update translations
* Brazilian Portuguese - @lnux-usr
* Bulgarian - @StoyanDimitrov
* Catalan - @and4po
* Chinese (Simplified) - Poesty Li
* Chinese (Traditional) - Chih-Hsuan Yen
* Croatian - @milotype
* Dutch - @fvbommel
* Esperanto - Don Zouras
* Finnish - @millerii
* French - @FlorianLeChat
* Italian - @ppasserini
* Japanese - @kisaragi-hiu, Naga
* Korean - Sunjae Choi, @o20n3
* Romanian - @simonaiacob
* Russian - @AHOHNMYC
* Spanish - @FlorianLeChat
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk
### 13.1.2 (2023-02-02)
* Add default reminders to incoming iCalendar tasks [#1984](https://github.com/tasks/tasks/issues/1984)
* Sync when brought to the foreground [#2096](https://github.com/tasks/tasks/issues/2096)
* Update translations
* Arabic - haidarah esmander
* Czech - @SlavekB
* Danish - Tntdruid
* Esperanto - Don Zouras, @J053Fabi0
* Finnish - @millerii
* German - @franconian
* Italian - @ppasserini
* Japanese - Kazushi Hayama
* Korean - @o20n3
* Polish - @gnu-ewm
* Vietnamese - @unbiaseduser
### 13.1.1 (2022-12-06)
* Fix crash when opening notification settings
* Fix IAP errors in some locales
* Update translations
* Italian - @ppasserini
* Japanese - Kazushi Hayama
### 13.1.0 (2022-11-30)
* Support for DAVx5 and CalDAV read-only lists [#931](https://github.com/tasks/tasks/issues/931)
* Use default Android network security configuration
* Update translations
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Dutch - @fvbommel
* Finnish - @millerii
* French - @FlorianLeChat
* German - @helloworldtest123
* Hungarian - Kaci
* Italian - @ppasserini
* Lithuanian - @70h
* Russian - Nikita Epifanov
* Spanish - @FlorianLeChat
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk
### 13.0.2 (2022-11-22)
* Fix persistent notifications on Android 13
* Fix Samsung crash on too many reminders (DAVx5, EteSync, DecSync CC)
* Fix crash on too many tasks for Astrid Manual Sorting
* Fix RTL text in task edit customization screen
* Fix priority button order
### 13.0.1 (2022-10-20)
* 🚨 Major internal changes to task edit screen. Please report any bugs! 🚨
* Show thumbnails for attachments
* Tap on existing alarms to replace them
* Add task info row to edit screen [#1839](https://github.com/tasks/tasks/pull/1839)
* Add option to disable reminders for all-day tasks [#2003](https://github.com/tasks/tasks/pull/2003)
* Updated chip style
* Show geofence circle in place settings
* Fix removing preferences [#1981](https://github.com/tasks/tasks/pull/1981)
* Set user-agent on HTTP requests [#1978](https://github.com/tasks/tasks/issues/1978)
* Preserve HTTP session cookies [#1978](https://github.com/tasks/tasks/issues/1978)
* Sort selected tags at top of tag picker
* Android 13 support
* Runtime notification permissions
* Language preference
* Improvements to copying tasks
* Don't forget parent when copying tasks [#1964](https://github.com/tasks/tasks/pull/1964)
* Copy attachments when duplicating tasks [#812](https://github.com/tasks/tasks/issues/812)
* Fix duplicating subtasks
* Fix some missing reminders
* Incoming Google Tasks
* Tasker tasks [#1937](https://github.com/tasks/tasks/issues/1937)
* New subtasks [#1914](https://github.com/tasks/tasks/issues/1914)
* Fix Google Task creation time
* Fix EteSync stops synchronizing [#1893](https://github.com/tasks/tasks/issues/1893)
* Don't overwrite coordinates when synchronizing locations [#1667](https://github.com/tasks/tasks/issues/1667)
* Update translations
* Asturian - @enolp
* Basque - Sergio Varela
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Czech - Shimon
* Dutch - @fvbommel
* French - @FlorianLeChat, J. Lavoie
* German - @qwerty287
* Italian - @ppasserini
* Norwegian Bokmål - @comradekingu
* Persian - @latelateprogrammer
* Polish - @ebogucka
* Portuguese - @laralem
* Romanian - @simonaiacob
* Russian - @Allineer, Nikita Epifanov
* Sinhala - @Dilshan-H
* Spanish - @FlorianLeChat
* Turkish - @ersen0
* Ukrainian - @IhorHordiichuk, @artemmolotov
* Vietnamese - @unbiaseduser
[Older releases](https://github.com/tasks/tasks/blob/main/V10_12_CHANGELOG.md)

@ -1,76 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at github@tasks.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

@ -1,34 +0,0 @@
### Translation
You can translate Tasks using [Weblate](https://hosted.weblate.org/projects/tasks/android). To get started, register a new account or login with your GitHub account if you have one.
### Opening issues
Before opening an issue, please make sure that your issue:
- is not a duplicate (i.e. it has not been reported before, closed or open)
- has not been fixed
- is in English (issues in a language other than English will be closed unless someone translates them)
- does not contain multiple feature requests/bug reports. Please open a separate issue for each one.
### Code contribution
#### To get started with development:
1. [Fork](https://help.github.com/articles/fork-a-repo/) and [clone](https://help.github.com/articles/cloning-a-repository/) the repository
2. Install and launch [Android Studio's canary build](https://developer.android.com/studio/preview) (Tasks depends on some bleeding-edge features of the canary build, but in the future when those features are stabilized, you will be able to use the stable release of Android Studio)
3. Select `File > Open`, select the Tasks directory, and accept prompts to install missing SDK components
#### Set up Mapbox
1. Register at [mapbox.com](https://www.mapbox.com)
2. Add `tasks_mapbox_key_debug="<your_api_key>"` to your [`gradle.properties`](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties) file. You can create an access token or use your [default public token](https://docs.mapbox.com/help/glossary/access-token/#default-public-token)
#### Set up Google Tasks and Google Drive
1. Register at [cloud.google.com](https://cloud.google.com)
2. Enable [Google Tasks API](https://console.cloud.google.com/apis/library/tasks.googleapis.com) and [Google Drive API](https://console.cloud.google.com/apis/library/drive.googleapis.com)
3. [Create android authorization credentials](https://developers.google.com/identity/protocols/OAuth2InstalledApp#creatingcred)
#### Set up Google Maps and Google Places
1. Register at [cloud.google.com](https://cloud.google.com)
2. Enable [Google Maps SDK](https://console.cloud.google.com/apis/library/maps-android-backend.googleapis.com) and [Google Places API](https://console.cloud.google.com/apis/library/places-backend.googleapis.com)
3. [Set up an API key](https://cloud.google.com/video-intelligence/docs/common/auth#set_up_an_api_key)
4. Add `tasks_google_key_debug="<your_api_key>"` to your [`gradle.properties`](https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties) file
5. Select `Build > Select Build Variant` and choose the `googleplay` variant

@ -1,4 +0,0 @@
source "https://rubygems.org"
gem "fastlane"
gem "abbrev"

@ -1,230 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.7)
base64
nkf
rexml
abbrev (0.1.2)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.4.0)
aws-partitions (1.1196.0)
aws-sdk-core (3.240.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
aws-sdk-kms (1.118.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.208.0)
aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.3.0)
bigdecimal (4.0.1)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107)
dotenv (2.8.1)
emoji_regex (3.2.3)
excon (0.112.0)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.5)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.8.0)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.5.0)
google-cloud-storage (1.47.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.8)
domain_name (~> 0.5)
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.12.2)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
public_suffix (6.0.2)
rake (13.3.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.4.2)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.20.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
naturally
sysrandom (1.0.5)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.2)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unicode-display_width (2.6.0)
word_wrap (1.0.0)
xcodeproj (1.27.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
ruby
DEPENDENCIES
abbrev
fastlane
BUNDLED WITH
2.6.9

@ -1,28 +1,21 @@
Astrid was a popular cross-platform productivity service that was [acquired](https://web.archive.org/web/20130811052500/http://blog.astrid.com/blog/2013/05/01/yahoo-acquires-astrid/) and [discontinued](https://techcrunch.com/2013/07/06/astrid-goes-dark-august-5-goodnight-sweet-squid/) in 2013. The source code from Astrid's open source Android app serves as the basis of Tasks.
[![Build Status](https://travis-ci.org/tasks/tasks.svg?branch=master)](https://travis-ci.org/tasks/tasks) [![codecov.io](http://codecov.io/github/tasks/tasks/coverage.svg?branch=master)](http://codecov.io/github/tasks/tasks?branch=master)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=org.tasks)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/org.tasks)
[![Make a contribution](https://pledgie.com/campaigns/24281.png?skin_name=chrome)](https://pledgie.com/campaigns/24281)
Please visit [tasks.org](https://tasks.org) for end user documentation and support
<a href="https://play.google.com/store/apps/details?id=org.tasks"><img alt="Get it on Google Play" src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="60" width="185" /></a> [![Get it on F-Droid](https://f-droid.org/wiki/images/d/d3/F-Droid-button_bigger.png)](https://f-droid.org/repository/browse/?fdid=org.tasks) [![Get it on Amazon App Store](https://images-na.ssl-images-amazon.com/images/G/01/AmazonMobileApps/amazon-apps-store-us-black.png)](https://www.amazon.com/gp/product/B00QHGTL7O/ref=mas_pm_tasks_astrid_to_do_list_clone)
---
### Screenshots
[![Donate with Bitcoin](https://img.shields.io/badge/bitcoin-donate-yellow.svg?logo=bitcoin)](https://tasks.org/docs/donate)
[![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)
<img src="./graphics/screenshot_tablet_navigation.png" width="250px"/>
<img src="./graphics/screenshot_phone_light.png" width="250px"/>
<img src="./graphics/screenshot_phone_dark.png" width="250px"/>
<img src="./graphics/screenshot_phone_task_edit.png" width="250px"/>
<img src="./graphics/screenshot_phone_sort.png" width="250px"/>
<img src="./graphics/screenshot_phone_notifications.png" width="250px"/>
<img src="./graphics/screenshot_phone_widgets.png" width="250px"/>
<img src="./graphics/screenshot_tablet_landscape.png" width="500px"/>
[![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)
### Contributing
Contributions are always welcome! Whether translations, code changes, bug reports, feature requests, or otherwise, your help is appreciated. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md).
### Communication
You can submit questions to [GitHub Discussions](https://github.com/tasks/tasks/discussions).
If you have a suggestion or want to report a bug, please see [CONTRIBUTING.md](CONTRIBUTING.md).
Visit the wiki to
* [help with translations](https://github.com/tasks/tasks/wiki/Translations)
* [become a beta tester](https://github.com/tasks/tasks/wiki/Beta-Testing)
* [get started with development](https://github.com/tasks/tasks/wiki/Getting-Started-with-Development)

@ -1,579 +0,0 @@
[Newer releases](https://github.com/tasks/tasks/blob/main/CHANGELOG.md)
### 9.7.3 (2020-07-07)
* Fix Google Task bugs
### 9.7.2 (2020-06-22)
* Downgrade Mapbox SDK to remove non-free library (F-Droid only)
### 9.7.1 (2020-06-19)
* Fix crash on backup import
* Fix CalDAV/EteSync subtask move bug
### 9.7 (2020-06-12)
* Added '☰ > Manage lists'
* Drag and drop to rearrange the drawer
* Tap to edit or delete a list
* Display 2 additional snooze options - @rangzen
### 9.6 (2020-06-06)
* Add support for offline lists. Offline lists support manual ordering and infinite-depth subtasks
* Rename 'My order' to 'Astrid manual sorting' for 'My Tasks', 'Today', and tags
* Add '⚙ > Look and feel > Disable sort groups'
* Add '⚙ > Look and feel > Open last viewed list'
* Add '⚙ > Look and feel > Chips' toggles for subtasks, places, lists, and tags
* Add '⚙ > Navigation drawer > Lists'
* Add '⚙ > Task defaults > Default list'
* Add '⚙ > Task defaults > New tasks on top'
* Add '⚙ > Advanced > Astrid manual sorting'
* Fix preference reset button
### 9.5 (2020-06-03)
* Drag and drop to change subtasks in all list types
* Drag and drop to reprioritize or reschedule tasks while sorting by due date
or priority
* Bug fixes
### 9.4.1 (2020-06-01)
* Add 'Tasks settings > Advanced > Improve performance' toggle
* Bug fixes
### 9.4 (2020-05-27)
* Add collapsible group headers when sorting by due date, priority, created, or modified
### 9.3.1 (2020-05-26)
* Fix offline subtasks
### 9.3 (2020-05-22)
* Add manual sorting support for CalDAV and EteSync
### 9.2 (2020-05-13)
* 'New task' quick settings tile (Android 7+)
* Search results match place names and addresses, caldav list names, google task list names, and comments
* Fix duplicated search results
* Began migrating codebase to Kotlin
### 9.1 (2020-05-04)
* 'New task' launcher shortcut (Android 7.1+)
* Add option to disable subtask chip on widget
### 9.0 (2020-05-03)
* Show What's New after update
* Collapsible subtasks enabled by default
* 20 new icons
* Show subtask chip even if list chips are disabled
* Indent subtasks in 'Share' output
* Don't trigger location reminders for snoozed or hidden tasks
* Minimum supported version is now Android 6.0
### 8.11 (2020-04-27)
* Edit existing custom filters
* Drag and drop to rearrange filter criteria
* Swipe to delete filter criteria
* Tap on filter criteria to choose filter operator
* Offer additional built-in filters
* Add sort by creation time
* Choose any day as start of week
### 8.10 (2020-04-20)
* New widget features
* Menu button to quickly change list
* Expand and collapse subtasks
* Click on due date to reschedule
* Access widget settings from main app preferences
* Show description
* Show hidden task indicators
* New widget settings
* Row spacing: default, compact, none
* Due date: after title, below title, or hidden
* Configure header, row, and footer opacity
* Configure footer click behavior
* Show full task title
* Show full description
* Hide dividers
* Improve widget touch targets
* Expand/collapse Google Task subtasks in 'My order' mode
* Fix bug when changing sort order to/from 'My order'
* Fix crash when switching to 'My order' list with subtasks disabled
### 8.9.2 (2020-04-10)
* Fix 'Add reminder' layout issues
* Fix move between EteSync lists
* Accept date time changes when dismissing dialog
* Improve date time picker behavior in landscape mode
### 8.9.1 (2020-04-08)
* Add option to always hide check button
* Hide check button for new tasks
* Rearrange multi-select buttons
* Allow more space for time buttons in date time picker
* Fix priority button layout on smaller devices
* Fix clicking on hidden task titles
* Fix tag picker checkbox tint on Android 4.4
* Fix EteSync crash on malformed iCalendar data
### 8.9 (2020-04-06)
* Add 'Select all' option to multi-select menu
* Add 'Share' to menu and multi-select menu
* Display 'Calendar event created' snackbar after creating a calendar event
### 8.8 (2020-04-01)
* New bottom sheet due date picker
* Shortcuts and calendar displayed together (Android 6+)
* Click on due date in task list to reschedule
* Option to autoclose due date picker after selecting a date or time
* Redesigned title in edit screen
* 'Discard' in overflow menu when 'Back button saves task' enabled
* Add preference for linkifying edit screen
* Updated date and time formatting
* Minimum supported version is now Android 4.4
* Custom backup/attachment directory requires Android 5+
### 8.7.1 (2020-03-31)
* Fix multi-account Google Task synchronization
### 8.7 (2020-03-19)
* Places are now lists
* Rename a place
* Assign an icon and color to a place
* Add new navigation drawer settings
* Option to remove filters, tags, and places from drawer
* Option to hide unused tags and places in drawer
### 8.6.1 (2020-03-19)
* Fix crash on startup
### 8.6 (2020-03-17)
* Expand and collapse navigation drawer groups
### 8.5 (2020-03-13)
* Synchronize locations with CalDAV and EteSync
* Fix crash when clearing completed from recently modified filter
### 8.4 (2020-03-11)
* New chip configuration options
* Outlined or filled
* Text and icon, text only, or icon only
* Add option to disable color desaturation
* Fix EteSync shared lists
* Google Task sync requires Android 4.4+
### 8.3 (2020-03-08)
* Synchronize CalDAV and EteSync colors
* Rename CalDAV and EteSync lists
* Update Turkish translations - @emintufan
### 8.2.1 (2020-03-07)
* Increase default chip text contrast
* New purchase activity
* Fix dividers on Android 4.x
### 8.2 (2020-03-04)
* Choose your own app and widget colors with a color wheel
* Dark theme now free for all
* New 'System default' theme
* New outlined chip style
* Dark theme is now darker
* Light theme is now lighter
* Desaturate theme colors in dark mode
* Improve dialog theming consistency
* Bug fixes
### 8.1 (2020-02-21)
* Updated app settings screen
### 8.0.1 (2020-02-16)
* Fix missing sync settings on fdroid
### 8.0 (2020-02-12)
* EteSync support
### 7.8 (2020-01-24)
* Android AutoBackup integration
### 7.7 (2020-01-21)
* Add support for offline multi-level subtasks
* Update Simplified Chinese translations - @sr093906
### 7.6.1 (2020-01-17)
* Fix long press in Google Task and CalDAV lists
* Fix bug when moving multi-level CalDAV subtasks
* Preserve remote VTODO when moving CalDAV tasks
* Add Interlingua translations - @softinterlingua
### 7.6 (2020-01-10)
* Change tags with multi-select
* Fix custom filter crash on deleted tag
### 7.5 (2020-01-07)
* New tag picker
* Support self-signed SSL certificates
### 7.4.2 (2019-12-30)
* Fix Tasker plugin settings
### 7.4.1 (2019-12-27)
* Add option to enable subtasks in task list
* Performance improvements
* Ask Play Services to update security provider
* Display custom icons in tag picker
* Fix case comparison when sorting navigation drawer
### 7.4 (2019-12-16)
* Add Google Task and CalDAV subtasks from the edit screen
* 'Recently modified' shows all modifications in past 24 hours
* Fix duplicated multi-level subtask count
* Increase checkbox touch target
* Naturally order lists and filters
### 7.3.2 (2019-12-12)
* Fix slow query for subtasks
* Fix setting icon on new CalDAV list
* Fix clear completed for subtasks
* Fix crash when clearing 1000+ tasks
### 7.3.1 (2019-12-05)
* Fix crash on missing filter
### 7.3 (2019-12-03)
* Expand and collapse subtasks
### 7.2.2 (2019-12-03)
* Fix Google Task sorting
* Fix crash when deleting 500+ tasks
### 7.2.1 (2019-11-27)
* Bug fixes and minor improvements
### 7.2 (2019-11-25)
* Display Google Task and CalDAV subtasks in all lists (Android 5+)
* Remove completed tasks immediately - @creywood
### 7.1.2 (2019-11-22)
* Add CalDAV account setting for repeating tasks
* Fix CalDAV repeating tasks
* Fix Google Tasks HTTP 400 response
### 7.1.1 (2019-11-18)
* Improve subtask query performance
* Fix crash when deleting 1000+ CalDAV tasks
### 7.1 (2019-11-14)
* Display subtasks on Google Task and CalDAV widgets (Android 5+)
* Fix subtasks after backup import
* Fix chained subtask completion
### 7.0 (2019-11-12)
* Add support for CalDAV subtasks (Android 5+) - @creywood
* Display Google subtasks in all sort modes (Android 5+)
### 6.9.3 (2019-10-31)
* Fix disappearance of remotely completed recurring Google Tasks
* Fix '0 tasks' notification
* Limit to 20 active notifications due to change in Android 10
### 6.9.2 (2019-10-25)
* Fix bug forcing new Google Tasks to top
* Fix bug preventing deleted tasks from being synchronized - @creywood
### 6.9.1 (2019-10-09)
* Fix location reminders on Android 10
* Fix CalDAV time zone issue
### 6.9 (2019-09-23)
* Synchronize tags with CalDAV
* Target Android 10
* Bug fixes
### 6.8.1 (2019-08-05)
* Fix CalDAV filter migration
* Fix native date picker crash
### 6.8 (2019-07-30)
* Name your own subscription price! Upgrade, downgrade, or cancel at any time
* Choose icons for lists (requires [subscription](https://tasks.org/subscribe))
* Choose color for custom filters
* Performance improvements
* Allow duplicate CalDAV list names
* Fix duplicate tag name bug
### 6.7.3 (2019-07-16)
* Workaround for [list updated time bug](https://issuetracker.google.com/issues/136123247) in Google Tasks API
* Fix crash in CalDAV sync
### 6.7.2 (2019-07-08)
* Handle 404 errors when creating new Google Tasks
* Ignore 404 errors when deleting Google Drive files
* Don't report connection errors
### 6.7.1 (2019-07-05)
* Add location chip to task list
* Reduce chip sizes
* Accept 'send to' for more attachment types
* Synchronize multiple accounts in parallel
* Fix Google Task migration from older versions
* Fix corrupted checkbox issue
* Fix some RTL issues
### 6.7 (2019-06-13)
* Use drag and drop to indent tasks
* Add new Google Tasks to top or bottom
* Toggle hidden and completed in manually sorted Google Task lists
* Rearrange Google Tasks without a network connection
* Optional workaround for [custom order bug](https://issuetracker.google.com/issues/132432317) in Google Tasks API
* Include subtasks when moving or deleting Google Tasks
* Ignore 404 errors when fetching Google Drive folders
* Match tags in search results
* Fix stuck 'Generating notifications' notification
* Don't display sync indicator when there is no network connection
* Don't synchronize immediately after every change
* Added Estonian translations - Eraser
### 6.6.4 (2019-05-21)
* Handle [breaking change](https://issuetracker.google.com/issues/133254108) in Google Tasks API
### 6.6.3 (2019-05-08)
* Fix backup import crash
* Fix crash when refreshing purchases
* Google Tasks synchronization bug fix
### 6.6.2 (2019-04-22)
* Backup and restore preferences
* Google Task performance improvements
* Google Task and Drive support added to F-Droid and Amazon
* Add third-party licenses, changelog, and version info
* Fix backup import crash
* Fix widget bugs
### 6.6.1 (2019-04-15)
* Fix crash on devices running Android 5.1 and below
* Fix analytics opt-out
### 6.6 (2019-04-10)
* New location picker
* Choose Mapbox or Google Maps tiles
* Choose Mapbox or Google Places search
* Google Places search restricted to subscribers due to new Google Maps pricing
* Use Mapbox for reverse geocoding
* Select from previously used locations
* Dark maps
* Enable location picker in F-Droid build
* Resume support for Amazon App Store
* Fix Android Q background warning
### 6.5.6 (2019-03-27)
* Fix crash when clearing completed on a manually sorted Google Task list
* Update Ukrainian translations - nathalier
### 6.5.5 (2019-03-14)
* Bug fixes
### 6.5.4 (2019-03-11)
* Fix black screen issue
* Fix crash when task not found
### 6.5.3 (2019-02-19)
* Fix crash when upgrading from Android 7 to 8+
* Improve OneTask interoperability
* Performance improvement
### 6.5.2 (2019-02-11)
* Bug fixes
### 6.5.1 (2019-02-10)
* Bug fixes
### 6.5 (2019-02-08)
* Improve notification accuracy
* Performance improvements
* Bug fixes
* Add Tagalog translations - Topol
### 6.4.1 (2019-01-16)
* Limit number of active notifications
* Limit rate of notifications
* Fix Synology Calendar sync issue
* Fix exception when external storage is unavailable
### 6.4 (2019-01-10)
* Copy backups to Google Drive
* Improved search
* Use system file picker (Android 4.4+)
* Use system directory picker (Android 5.0+)
* Accept 'send' and 'send_multiple' actions with images
* File attachment bug fixes
### 6.3.1 (2018-11-07)
* New location row in task edit screen
* Add location departure notifications
* Set CalDAV completion percentage and status
* Bug fixes
### 6.2 (2018-10-29)
* New white theme color
* New icons
* New list and tag chips
* Linkify text when editing tasks
* Option to linkify text on task list
* Show description on task list
* Move due date next to title
* Updated hidden task visualization
* No longer require contacts permission (Oreo+)
* Dropped support for Android 4.0
### 6.1.3 (2018-10-22)
* Fix translation error
### 6.1.2 (2018-10-18)
* Remove missed call functionality due to Google Play Developer policy change
* Fix manual sort issue affecting Samsung Oreo devices
* Fix refresh issue affecting Pure Calendar Widget
* Fix memory leak
* Schedule jobs with WorkManager instead of android-job
### 6.1.1 (2018-07-20)
* Fix notification badge issues
* Allow non-SSL connections
* Allow user-defined certificate authorities
### 6.1 (2018-06-30)
* Customize launcher icon
* Customize shortcut widget icon and label
* Add custom text selection action (Android 6+)
* Target Android P
* Remove 'Tasks' from notification body
* Fix localization issues - @marmo
* Fix crash when calendar permissions are revoked
* Fix crash when opening task from widget
* Fix crash when recording audio note
* Fix crash when dismissing dialogs
* Fix crash in backup import
* Fix crash on invalid URL during CalDAV setup
* Fix crash when editing task
### 6.0.6 (2018-04-28)
* Fix crash when creating shortcuts on pre-Oreo devices
* Fix crash when Google Task or CalDAV list is missing
* Downgrade Play Services for compatibility with MicroG
### 6.0.5 (2018-04-26)
* Fix crash when deleting 1000+ tasks at once
* Fix hidden dates in date picker
* Fix crash on bad response from billing client
* Report crash when database fails to open
### 6.0.4 (2018-04-25)
* Fix crash caused by leftover Google Analytics campaign tracker
### 6.0.3 (2018-04-25)
* Fix crash when manually sorting Google Task lists
* Fix multi account Google Task sync issue
### 6.0.2 (2018-04-25)
* Fix crash caused by missing tag metadata
* Fix crash caused by missing Android System WebView
* Replace Google Analytics with Firebase Analytics
* Add Crashlytics exception reporting
### 6.0.1 (2018-04-23)
* Fix crash caused by missing Google Task metadata
### 6.0 (2018-04-23)
* Change to [annual subscription](https://tasks.org/subscribe) pricing
* [CalDAV synchronization](https://tasks.org/caldav)
* Sync with [multiple Google Task accounts](https://tasks.org/docs/google_tasks_intro.html)
* Default theme changed to blue
* Display Google Task and CalDAV chips on task list
* Display sync error icon in navigation drawer
* Move tasks between Google Task and CalDAV lists using multi-select
* Add "Don't Sync" option when choosing a Google Task or CalDAV list
* Add option to restrict background synchronization to unmetered connections
* Custom filters with due date criteria no longer set a due time of 23:59/11:59PM
* Internal improvements to notification scheduling should reduce notification delays
* Fix list animation bug

@ -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,293 +0,0 @@
@file:Suppress("UnstableApiUsage")
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
kotlin("android")
id("dagger.hilt.android.plugin")
id("com.google.android.gms.oss-licenses-plugin")
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose.compiler)
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
composeCompiler {
enableStrongSkippingMode = true
}
android {
bundle {
language {
enableSplit = false
}
}
buildFeatures {
viewBinding = true
dataBinding = true
compose = true
buildConfig = true
}
lint {
lintConfig = file("lint.xml")
textOutput = File("stdout")
textReport = true
}
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
testApplicationId = "org.tasks.test"
applicationId = "org.tasks"
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()
targetSdk = libs.versions.android.targetSdk.get().toInt()
minSdk = libs.versions.android.minSdk.get().toInt()
testInstrumentationRunner = "org.tasks.TestRunner"
}
signingConfigs {
create("release") {
val tasksKeyAlias: String? by project
val tasksStoreFile: String? by project
val tasksStorePassword: String? by project
val tasksKeyPassword: String? by project
keyAlias = tasksKeyAlias
storeFile = file(tasksStoreFile ?: "none")
storePassword = tasksStorePassword
keyPassword = tasksKeyPassword
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
flavorDimensions += listOf("store")
@Suppress("LocalVariableName")
buildTypes {
debug {
configure<CrashlyticsExtension> {
mappingFileUploadEnabled = false
}
val tasks_mapbox_key_debug: String? by project
val tasks_google_key_debug: String? by project
val tasks_caldav_url: String? by project
resValue("string", "mapbox_key", tasks_mapbox_key_debug ?: "")
resValue("string", "google_key", tasks_google_key_debug ?: "")
resValue("string", "tasks_caldav_url", tasks_caldav_url ?: "https://caldav.tasks.org")
resValue("string", "tasks_nominatim_url", tasks_caldav_url ?: "https://nominatim.tasks.org")
resValue("string", "tasks_places_url", tasks_caldav_url ?: "https://places.tasks.org")
enableUnitTestCoverage = project.hasProperty("coverage")
}
release {
val tasks_mapbox_key: String? by project
val tasks_google_key: String? by project
resValue("string", "mapbox_key", tasks_mapbox_key ?: "")
resValue("string", "google_key", tasks_google_key ?: "")
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard.pro")
signingConfig = signingConfigs.getByName("release")
}
}
productFlavors {
create("generic") {
dimension = "store"
}
create("googleplay") {
isDefault = true
dimension = "store"
}
}
packaging {
resources {
excludes += setOf("META-INF/*.kotlin_module", "META-INF/INDEX.LIST")
}
}
testOptions {
managedDevices {
localDevices {
create("pixel2api30") {
device = "Pixel 2"
apiLevel = 30
systemImageSource = "aosp-atd"
}
}
}
}
namespace = "org.tasks"
}
configurations.all {
exclude(group = "org.apache.httpcomponents")
exclude(group = "org.checkerframework")
exclude(group = "com.google.code.findbugs")
exclude(group = "com.google.errorprone")
exclude(group = "com.google.j2objc")
exclude(group = "com.google.http-client", module = "google-http-client-apache-v2")
exclude(group = "com.google.http-client", module = "google-http-client-jackson2")
}
val genericImplementation by configurations
val googleplayImplementation by configurations
dependencies {
implementation(projects.data)
implementation(projects.kmp)
implementation(projects.icons)
implementation(libs.androidx.navigation)
implementation(libs.androidx.adaptive.navigation.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bitfire.dav4jvm) {
exclude(group = "junit")
exclude(group = "org.ogce", module = "xpp3")
}
implementation(libs.bitfire.ical4android) {
exclude(group = "commons-logging")
exclude(group = "org.json", module = "json")
exclude(group = "org.codehaus.groovy", module = "groovy")
exclude(group = "org.codehaus.groovy", module = "groovy-dateutil")
}
implementation(libs.bitfire.cert4android)
implementation(libs.dmfs.opentasks.provider) {
exclude("com.github.tasks.opentasks", "opentasks-contract")
}
implementation(libs.dmfs.rfc5545.datetime)
implementation(libs.dmfs.recur)
implementation(libs.dmfs.jems)
implementation(libs.dagger.hilt)
ksp(libs.dagger.hilt.compiler)
ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.navigation)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.core.remoteviews)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.datastore)
implementation(libs.androidx.fragment.compose)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.room)
implementation(libs.androidx.sqlite)
implementation(libs.androidx.appcompat)
implementation(libs.iconics)
implementation(libs.markwon)
implementation(libs.markwon.editor)
implementation(libs.markwon.linkify)
implementation(libs.markwon.strikethrough)
implementation(libs.markwon.tables)
implementation(libs.markwon.tasklist)
debugImplementation(libs.leakcanary)
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation(libs.kotlin.reflect)
implementation(libs.kotlin.jdk8)
implementation(libs.kotlinx.immutable)
implementation(libs.kotlinx.serialization)
implementation(libs.okhttp)
implementation(libs.persistent.cookiejar)
implementation(libs.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.preference)
implementation(libs.timber)
implementation(libs.dashclock.api)
implementation(libs.locale) {
isTransitive = false
}
implementation(libs.jchronic) {
isTransitive = false
}
implementation(libs.shortcut.badger)
implementation(libs.google.api.tasks)
implementation(libs.google.api.drive)
implementation(libs.google.oauth2)
implementation(libs.androidx.work)
implementation(libs.etebase)
implementation(libs.colorpicker)
implementation(libs.appauth)
implementation(libs.osmdroid)
implementation(libs.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(libs.androidx.material.icons.extended)
implementation(libs.androidx.lifecycle.viewmodel.compose)
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.ktor)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.content.negotiation)
implementation(libs.ktor.serialization)
implementation(libs.accompanist.permissions)
googleplayImplementation(platform(libs.firebase))
googleplayImplementation(libs.firebase.crashlytics)
googleplayImplementation(libs.firebase.analytics) {
exclude("com.google.android.gms", "play-services-ads-identifier")
}
googleplayImplementation(libs.firebase.config.ktx)
googleplayImplementation(libs.play.services.location)
googleplayImplementation(libs.play.services.maps)
googleplayImplementation(libs.play.billing.ktx)
googleplayImplementation(libs.play.review)
googleplayImplementation(libs.play.services.oss.licenses)
googleplayImplementation(libs.horologist.datalayer.phone)
googleplayImplementation(libs.horologist.datalayer.grpc)
googleplayImplementation(libs.horologist.datalayer.core)
googleplayImplementation(libs.play.services.wearable)
googleplayImplementation(libs.microsoft.authentication) {
exclude("com.microsoft.device.display", "display-mask")
}
googleplayImplementation(projects.wearDatalayer)
androidTestImplementation(libs.dagger.hilt.testing)
kspAndroidTest(libs.dagger.hilt.compiler)
kspAndroidTest(libs.androidx.hilt.compiler)
androidTestImplementation(libs.mockito.android)
androidTestImplementation(libs.make.it.easy)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.okhttp.mockwebserver)
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.make.it.easy)
testImplementation(libs.androidx.test.core)
testImplementation(libs.mockito.core)
testImplementation(libs.xpp3)
}

@ -1,20 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "org.tasks.ak",
"variantName": "genericRelease",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 130804,
"versionName": "14.0.6",
"outputFile": "app-generic-release.apk"
}
],
"elementType": "File"
}

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
<issue id="MissingTranslation" severity="ignore"/>
<issue id="MissingQuantity" severity="ignore"/>
<issue id="ImpliedQuantity" severity="ignore"/>
<issue id="InvalidPackage">
<ignore regexp="net.fortuna.ical4j.util.JCacheTimeZoneCache"/>
</issue>
</lint>

64
app/proguard.pro vendored

@ -1,64 +0,0 @@
-dontobfuscate
-keep class org.tasks.** { *; }
# guava
-dontwarn sun.misc.Unsafe
-dontwarn java.lang.ClassValue
-dontwarn javax.annotation.**
-dontwarn javax.inject.**
-dontwarn com.google.j2objc.annotations.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn com.google.errorprone.annotations.**
# https://github.com/square/okhttp/blob/0b74bba08805c28f6aede626cf06f213ef6480f2/README.md
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-dontwarn org.conscrypt.**
# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
# https://gitlab.com/bitfireAT/davdroid/blob/9fc3921b3293e19bd7be7bfc3f24d799ed2446bc/app/proguard-rules.txt
-dontwarn aQute.**
-dontwarn groovy.** # Groovy-based ContentBuilder not used
-dontwarn javax.cache.** # no JCache support in Android
-dontwarn net.fortuna.ical4j.model.**
-dontwarn org.codehaus.groovy.**
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
-dontwarn com.github.erosb.jsonsKema.** # ical4android
-dontwarn org.jparsec.** # ical4android
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
-keep class at.bitfire.** { *; } # all DAVdroid code is required
# https://github.com/google/google-api-java-client-samples/blob/34c3b43cb15f4ee1b636a0e01521cc81a2451dcd/tasks-android-sample/proguard-google-api-client.txt
-keepattributes Signature,RuntimeVisibleAnnotations,AnnotationDefault
-keepclassmembers class * {
@com.google.api.client.util.Key <fields>;
}
-dontwarn com.google.api.client.extensions.android.**
-dontwarn com.google.api.client.googleapis.extensions.android.**
-dontwarn com.google.android.gms.**
-dontnote java.nio.file.Files, java.nio.file.Path
-dontnote **.ILicensingService
-dontnote sun.misc.Unsafe
-dontwarn sun.misc.Unsafe
# errors from upgrading to AGP 8
-dontwarn java.beans.Transient
-dontwarn org.joda.convert.FromString
-dontwarn org.joda.convert.ToString
-dontwarn org.json.JSONString
# material icons
-keep class androidx.compose.material.icons.outlined.** { *; }
# microsoft authentication
-dontwarn com.microsoft.device.display.DisplayMask
-dontwarn com.google.android.libraries.identity.**
-dontwarn edu.umd.cs.findbugs.annotations.**
-dontwarn com.google.crypto.tink.subtle.**
-dontwarn net.jcip.annotations.**
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { <fields>; }

@ -1,65 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.andlib.test
import android.content.res.Configuration
import android.content.res.Resources
import androidx.test.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.R.string
import java.util.*
/**
* Tests translations for consistency with the default values. You must extend this class and create
* it with your own values for strings and arrays.
*
* @author Tim Su <tim></tim>@todoroo.com>
*/
@RunWith(AndroidJUnit4::class)
class TranslationTests {
/** Loop through each locale and call runnable */
private fun forEachLocale(callback: (Resources) -> Unit) {
val locales = Locale.getAvailableLocales()
for (locale in locales) {
callback(getResourcesForLocale(locale))
}
}
private fun getResourcesForLocale(locale: Locale): Resources {
val resources = InstrumentationRegistry.getTargetContext().resources
val configuration = Configuration(resources.configuration)
configuration.locale = locale
return Resources(resources.assets, resources.displayMetrics, configuration)
}
/** check if string contains contains substrings */
private fun contains(r: Resources, resource: Int, failures: StringBuilder, expected: String) {
val translation = r.getString(resource)
if (!translation.contains(expected)) {
val locale = r.configuration.locale
val name = r.getResourceName(resource)
failures.append(String.format("%s: %s did not contain: %s\n", locale.toString(), name, expected))
}
}
/** Test dollar sign resources */
@Test
fun testSpecialStringsMatch() {
val failures = StringBuilder()
forEachLocale { r: Resources ->
contains(r, string.CFC_tag_text, failures, "?")
contains(r, string.CFC_title_contains_text, failures, "?")
contains(r, string.CFC_startBefore_text, failures, "?")
contains(r, string.CFC_dueBefore_text, failures, "?")
contains(r, string.CFC_tag_contains_text, failures, "?")
contains(r, string.CFC_gtasks_list_text, failures, "?")
}
assertEquals(failures.toString(), 0, failures.toString().replace("[^\n]".toRegex(), "").length)
}
}

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

@ -1,79 +0,0 @@
package com.todoroo.andlib.utility
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
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 java.util.Locale
@RunWith(AndroidJUnit4::class)
class RelativeDayTest {
private lateinit var defaultLocale: Locale
private val now = DateTime(2013, 12, 31, 11, 9, 42, 357)
@Before
fun setUp() {
defaultLocale = Locale.getDefault()
Locale.setDefault(Locale.US)
Freeze.freezeAt(now)
}
@After
fun tearDown() {
Locale.setDefault(defaultLocale)
Freeze.thaw()
}
@Test
fun testRelativeDayIsToday() {
checkRelativeDay(DateTime(), "Today", "Today")
}
@Test
fun testRelativeDayIsTomorrow() {
checkRelativeDay(DateTime().plusDays(1), "Tomorrow", "Tmrw")
}
@Test
fun testRelativeDayIsYesterday() {
checkRelativeDay(DateTime().minusDays(1), "Yesterday", "Yest")
}
@Test
fun testRelativeDayTwo() {
checkRelativeDay(DateTime().minusDays(2), "Sunday", "Sun")
checkRelativeDay(DateTime().plusDays(2), "Thursday", "Thu")
}
@Test
fun testRelativeDaySix() {
checkRelativeDay(DateTime().minusDays(6), "Wednesday", "Wed")
checkRelativeDay(DateTime().plusDays(6), "Monday", "Mon")
}
@Test
fun testRelativeDayOneWeek() {
checkRelativeDay(DateTime().minusDays(7), "December 24", "Dec 24")
}
@Test
fun testRelativeDayOneWeekNextYear() {
checkRelativeDay(DateTime().plusDays(7), "January 7, 2014", "Jan 7, 2014")
}
private fun checkRelativeDay(now: DateTime, full: String, abbreviated: String) = runBlocking {
assertEquals(
full,
getRelativeDay(now.millis, DateStyle.LONG))
assertEquals(
abbreviated,
getRelativeDay(now.millis))
}
}

@ -1,32 +0,0 @@
package com.todoroo.astrid.activity
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.extensions.isFromHistory
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
@Test
fun newTaskIsNotFromHistory() {
assertFalse(Intent().setFlags(FLAG_ACTIVITY_NEW_TASK).isFromHistory)
}
@Test
fun oldTaskIsNotFromHistory() {
assertFalse(Intent().setFlags(FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY).isFromHistory)
}
@Test
fun newTaskIsFromHistory() {
assertTrue(
Intent()
.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY)
.isFromHistory)
}
}

@ -1,250 +0,0 @@
package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task
import org.tasks.filters.CaldavFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import javax.inject.Inject
@HiltAndroidTest
class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: CaldavManualSortTaskAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = CaldavFilter(
calendar = CaldavCalendar(name = "calendar", uuid = "1234"),
account = CaldavAccount(accountType = TYPE_CALDAV)
)
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_manual_sort, true)
tasks.clear()
adapter = CaldavManualSortTaskAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
adapter.setDataSource(dataSource)
}
@Test
fun moveToSamePositionIsNoop() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(1)))
move(0, 0)
checkOrder(null, 0)
checkOrder(null, 1)
}
@Test
fun moveTaskToTopOfList() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(1)))
move(1, 0)
checkOrder(created.minusSeconds(1), 1)
checkOrder(null, 0)
}
@Test
fun moveTaskToBottomOfList() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(1)))
move(0, 1)
checkOrder(null, 1)
checkOrder(created.plusSeconds(2), 0)
}
@Test
fun moveDownToMiddleOfList() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(1)))
addTask(with(CREATION_TIME, created.plusSeconds(2)))
addTask(with(CREATION_TIME, created.plusSeconds(3)))
addTask(with(CREATION_TIME, created.plusSeconds(4)))
move(0, 2)
checkOrder(null, 1)
checkOrder(null, 2)
checkOrder(created.plusSeconds(3), 0)
checkOrder(created.plusSeconds(4), 3)
checkOrder(created.plusSeconds(5), 4)
}
@Test
fun moveUpToMiddleOfList() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(1)))
addTask(with(CREATION_TIME, created.plusSeconds(2)))
addTask(with(CREATION_TIME, created.plusSeconds(3)))
addTask(with(CREATION_TIME, created.plusSeconds(4)))
move(3, 1)
checkOrder(null, 0)
checkOrder(created.plusSeconds(1), 3)
checkOrder(created.plusSeconds(2), 1)
checkOrder(created.plusSeconds(3), 2)
checkOrder(null, 4)
}
@Test
fun moveDownNoShiftRequired() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(1)))
addTask(with(CREATION_TIME, created.plusSeconds(3)))
addTask(with(CREATION_TIME, created.plusSeconds(4)))
move(0, 1)
checkOrder(null, 1)
checkOrder(created.plusSeconds(2), 0)
checkOrder(null, 2)
checkOrder(null, 3)
}
@Test
fun moveUpNoShiftRequired() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(2)))
addTask(with(CREATION_TIME, created.plusSeconds(3)))
addTask(with(CREATION_TIME, created.plusSeconds(4)))
move(2, 1)
checkOrder(null, 0)
checkOrder(created.plusSeconds(1), 2)
checkOrder(null, 1)
checkOrder(null, 3)
}
@Test
fun moveToNewSubtask() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(2)))
move(1, 1, 1)
checkOrder(null, 0)
checkOrder(null, 1)
}
@Test
fun moveToTopOfExistingSubtasks() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
val parent = addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(5)), with(PARENT, parent))
addTask(with(CREATION_TIME, created.plusSeconds(2)))
move(2, 1, 1)
checkOrder(null, 0)
checkOrder(created.plusSeconds(4), 2)
checkOrder(null, 1)
}
@Test
fun indentingChangesParent() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATION_TIME, created))
addTask(with(CREATION_TIME, created.plusSeconds(2)))
move(1, 1, 1)
assertEquals(tasks[0].id, tasks[1].parent)
}
@Test
fun deindentLastMultiLevelSubtask() {
val created = DateTime(2020, 5, 17, 9, 53, 17)
val grandparent = addTask(with(CREATION_TIME, created))
val parent = addTask(with(CREATION_TIME, created.plusSeconds(5)), with(PARENT, grandparent))
addTask(with(CREATION_TIME, created.plusSeconds(1)), with(PARENT, parent))
addTask(with(CREATION_TIME, created.plusSeconds(2)), with(PARENT, parent))
move(3, 3, 1)
assertEquals(grandparent, tasks[3].parent)
checkOrder(created.plusSeconds(6), 3)
}
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking {
tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter)))
val adjustedTo = if (from < to) to + 1 else to // match DragAndDropRecyclerAdapter behavior
adapter.moved(from, adjustedTo, indent)
}
private fun checkOrder(dateTime: DateTime, index: Int) = checkOrder(dateTime.toAppleEpoch(), index)
private fun checkOrder(order: Long?, index: Int) = runBlocking {
val sortOrder = taskDao.fetch(adapter.getTask(index).id)!!.order
if (order == null) {
assertNull(sortOrder)
} else {
assertEquals(order, sortOrder)
}
}
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking {
val task = newTask(*properties)
taskDao.createNew(task)
val remoteParent = if (task.parent > 0) caldavDao.getRemoteIdForTask(task.parent) else null
caldavDao.insert(
newCaldavTask(
with(TASK, task.id),
with(CALENDAR, "1234"),
with(REMOTE_PARENT, remoteParent)))
task.id
}
}

@ -1,213 +0,0 @@
package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.*
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.TaskContainerMaker.PARENT
import org.tasks.makers.TaskContainerMaker.newTaskContainer
import javax.inject.Inject
@HiltAndroidTest
class CaldavTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>()
@Before
override fun setUp() {
super.setUp()
tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
adapter.setDataSource(object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
override fun getTaskCount() = tasks.size
})
}
@Test
fun canMoveTask() {
addTask()
addTask()
assertTrue(adapter.canMove(tasks[0], 0, tasks[1], 1))
}
@Test
fun cantMoveTaskToChildPosition() {
addTask()
addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[0]))
assertFalse(adapter.canMove(tasks[0], 0, tasks[1], 1))
assertFalse(adapter.canMove(tasks[0], 0, tasks[2], 2))
}
@Test
fun canMoveChildAboveParent() {
addTask()
addTask(with(PARENT, tasks[0]))
assertTrue(adapter.canMove(tasks[1], 1, tasks[0], 0))
}
@Test
fun canMoveChildBetweenSiblings() {
addTask()
addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[0]))
assertTrue(adapter.canMove(tasks[1], 1, tasks[2], 2))
assertTrue(adapter.canMove(tasks[2], 2, tasks[1], 1))
}
@Test
fun maxIndentNoChildren() {
addTask()
addTask()
assertEquals(1, adapter.maxIndent(0, tasks[1]))
}
@Test
fun maxIndentMultiLevelSubtask() {
addTask()
addTask(with(PARENT, tasks[0]))
addTask()
assertEquals(1, adapter.maxIndent(0, tasks[1]))
assertEquals(2, adapter.maxIndent(1, tasks[2]))
}
@Test
fun minIndentInMiddleOfSubtasks() {
addTask()
addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[0]))
assertEquals(1, adapter.minIndent(2, tasks[1]))
}
@Test
fun minIndentAtEndOfSubtasks() {
addTask()
addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[0]))
addTask()
assertEquals(0, adapter.minIndent(3, tasks[2]))
}
@Test
fun minIndentAtEndOfMultiLevelSubtask() {
addTask()
addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[1]))
addTask()
assertEquals(0, adapter.minIndent(2, tasks[1]))
}
@Test
fun minIndentInMiddleOfMultiLevelSubtasks() {
addTask()
addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[1]))
addTask(with(PARENT, tasks[0]))
addTask()
assertEquals(1, adapter.minIndent(3, tasks[2]))
}
@Test
fun movingTaskToNewParentSetsId() = runBlocking {
addTask()
addTask()
adapter.moved(1, 1, 1)
assertEquals(tasks[0].id, taskDao.fetch(tasks[1].id)!!.parent)
}
@Test
fun movingTaskToNewParentSetsRemoteId() = runBlocking {
addTask()
addTask()
adapter.moved(1, 1, 1)
val parentId = caldavDao.getTask(tasks[0].id)!!.remoteId!!
assertTrue(parentId.isNotBlank())
assertEquals(parentId, caldavDao.getTask(tasks[1].id)!!.remoteParent)
}
@Test
fun unindentingTaskRemovesParent() = runBlocking {
addTask()
addTask(with(PARENT, tasks[0]))
adapter.moved(1, 1, 0)
assertTrue(caldavDao.getTask(tasks[1].id)!!.remoteParent.isNullOrBlank())
assertEquals(0, taskDao.fetch(tasks[1].id)!!.parent)
}
@Test
fun moveSubtaskUpToParent() = runBlocking {
addTask()
addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[1]))
adapter.moved(2, 2, 1)
assertEquals(tasks[0].id, taskDao.fetch(tasks[2].id)!!.parent)
}
@Test
fun moveSubtaskUpToGrandparent() = runBlocking {
addTask()
addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[1]))
addTask(with(PARENT, tasks[2]))
adapter.moved(3, 3, 1)
assertEquals(tasks[0].id, taskDao.fetch(tasks[3].id)!!.parent)
}
private fun addTask(vararg properties: PropertyValue<in TaskContainer?, *>) = runBlocking {
val t = newTaskContainer(*properties)
val task = t.task
taskDao.createNew(task)
val caldavTask = CaldavTask(task = t.id, calendar = "calendar")
if (task.parent > 0) {
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent)
}
tasks.add(
t.copy(
caldavTask = caldavTask.copy(
id = caldavDao.insert(caldavTask)
)
)
)
}
}

@ -1,450 +0,0 @@
package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest
import 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.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task
import org.tasks.filters.CaldavFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import javax.inject.Inject
@HiltAndroidTest
class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: GoogleTaskManualSortAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = CaldavFilter(
calendar = CaldavCalendar(uuid = "1234"),
account = CaldavAccount(accountType = TYPE_GOOGLE_TASKS)
)
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
override fun getTaskCount() = tasks.size
}
@Test
fun moveTaskToTopOfList() {
addTask()
addTask()
addTask()
move(2, 0)
checkOrder(0, 2)
checkOrder(1, 0)
checkOrder(2, 1)
}
@Test
fun moveTaskToBottomOfList() {
addTask()
addTask()
addTask()
move(0, 2)
checkOrder(0, 1)
checkOrder(1, 2)
checkOrder(2, 0)
}
@Test
fun moveTaskToBottomOfListAsNewSubtask() {
addTask()
addTask()
val parent = addTask()
move(0, 2, 1)
checkOrder(0, 1)
checkOrder(1, 2)
checkOrder(0, 0, parent)
}
@Test
fun moveTaskToBottomOfListAndSubtask() {
addTask()
val parent = addTask()
addTask(with(PARENT, parent))
move(0, 2, 1)
checkOrder(0, 1)
checkOrder(0, 2, parent)
checkOrder(1, 0, parent)
}
@Test
fun moveTaskToMiddleOfList() {
addTask()
addTask()
addTask()
move(0, 1)
checkOrder(0, 1)
checkOrder(1, 0)
checkOrder(2, 2)
}
@Test
fun moveTaskDownAsNewSubtask() {
addTask()
val parent = addTask()
addTask()
move(0, 1, 1)
checkOrder(0, 1)
checkOrder(0, 0, parent)
checkOrder(1, 2)
}
@Test
fun moveTaskDownToFrontOfSubtasks() {
addTask()
val parent = addTask()
addTask(with(PARENT, parent))
move(0, 1, 1)
checkOrder(0, 1)
checkOrder(0, 0, parent)
checkOrder(1, 2, parent)
}
@Test
fun moveTaskDownToMiddleOfSubtasks() {
addTask()
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
move(0, 2, 1)
checkOrder(0, 1)
checkOrder(0, 2, parent)
checkOrder(1, 0, parent)
checkOrder(2, 3, parent)
}
@Test
fun moveTaskDownToEndOfSubtasks() {
addTask()
val parent = addTask()
addTask(with(PARENT, parent))
addTask()
move(0, 2, 1)
checkOrder(0, 1)
checkOrder(0, 2, parent)
checkOrder(1, 0, parent)
checkOrder(1, 3)
}
@Test
fun moveTaskUpAsNewSubtask() {
val parent = addTask()
addTask()
addTask()
move(2, 1, 1)
checkOrder(0, 0)
checkOrder(0, 2, parent)
checkOrder(1, 1)
}
@Test
fun moveTaskUpToFrontOfSubtasks() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask()
move(2, 1, 1)
checkOrder(0, 0)
checkOrder(0, 2, parent)
checkOrder(1, 1, parent)
}
@Test
fun moveTaskUpToMiddleOfSubtasks() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask()
move(3, 2, 1)
checkOrder(0, 0)
checkOrder(0, 1, parent)
checkOrder(1, 3, parent)
checkOrder(2, 2, parent)
}
@Test
fun moveTaskUpToEndOfSubtasks() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask()
addTask()
move(3, 2, 1)
checkOrder(0, 0)
checkOrder(0, 1, parent)
checkOrder(1, 3, parent)
checkOrder(1, 2)
}
@Test
fun indentTask() {
val parent = addTask()
addTask()
addTask()
move(1, 1, 1)
checkOrder(0, 0)
checkOrder(0, 1, parent)
checkOrder(1, 2)
}
@Test
fun moveSubtaskFromTopToBottom() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask()
move(1, 3, 1)
checkOrder(0, 0)
checkOrder(0, 2, parent)
checkOrder(1, 3, parent)
checkOrder(2, 1, parent)
}
@Test
fun moveSubtaskFromTopToBottomAtEndOfList() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
move(1, 3, 1)
checkOrder(0, 0)
checkOrder(0, 2, parent)
checkOrder(1, 3, parent)
checkOrder(2, 1, parent)
}
@Test
fun moveSubtaskFromBottomToTop() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
move(3, 1, 1)
checkOrder(0, 0)
checkOrder(0, 3, parent)
checkOrder(1, 1, parent)
checkOrder(2, 2, parent)
}
@Test
fun moveSubtaskFromTopToMiddle() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
move(1, 2, 1)
checkOrder(0, 0)
checkOrder(0, 2, parent)
checkOrder(1, 1, parent)
checkOrder(2, 3, parent)
}
@Test
fun moveSubtaskFromBottomToMiddle() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
move(3, 2, 1)
checkOrder(0, 0)
checkOrder(0, 1, parent)
checkOrder(1, 3, parent)
checkOrder(2, 2, parent)
}
@Test
fun moveSubtaskFromMiddleToTop() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
move(2, 1, 1)
checkOrder(0, 0)
checkOrder(0, 2, parent)
checkOrder(1, 1, parent)
checkOrder(2, 3, parent)
}
@Test
fun moveSubtaskFromMiddleToBottom() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
move(2, 3, 1)
checkOrder(0, 0)
checkOrder(0, 1, parent)
checkOrder(1, 3, parent)
checkOrder(2, 2, parent)
}
@Test
fun moveSubtaskUpToTopLevel() {
addTask()
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
move(3, 1, 0)
checkOrder(0, 0)
checkOrder(1, 3)
checkOrder(2, 1)
checkOrder(0, 2, parent)
checkOrder(1, 4, parent)
}
@Test
fun moveSubtaskDownToTopLevel() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask()
addTask()
move(2, 4, 0)
checkOrder(0, 0)
checkOrder(0, 1, parent)
checkOrder(1, 3, parent)
checkOrder(1, 4)
checkOrder(2, 2)
checkOrder(3, 5)
}
@Test
fun moveSubtaskToEndOfListAndDeindent() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask()
move(2, 3, 0)
checkOrder(0, 0)
checkOrder(0, 1, parent)
checkOrder(1, 3, parent)
checkOrder(1, 2)
checkOrder(2, 4)
}
@Test
fun deindentTask() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask()
move(1, 1, 0)
checkOrder(0, 0)
checkOrder(1, 1)
checkOrder(2, 2)
}
@Before
override fun setUp() {
super.setUp()
preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true)
tasks.clear()
adapter = GoogleTaskManualSortAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
adapter.setDataSource(dataSource)
}
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking {
tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter)))
val adjustedTo = if (from < to) to + 1 else to
adapter.moved(from, adjustedTo, indent)
}
private fun checkOrder(order: Long, index: Int, parent: Long = 0) = runBlocking {
val googleTask = taskDao.fetch(adapter.getTask(index).id)!!
assertEquals(order, googleTask.order)
assertEquals(parent, googleTask.parent)
}
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking {
val task = newTask(*properties)
taskDao.createNew(task)
googleTaskDao.insertAndShift(
task,
newCaldavTask(
with(TASK, task.id),
with(CALENDAR, "1234"),
),
false
)
task.id
}
}

@ -1,129 +0,0 @@
package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.entity.Task
import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import javax.inject.Inject
@HiltAndroidTest
class OfflineSubtaskTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
private val filter = runBlocking { MyTasksFilter.create() }
@Before
override fun setUp() {
super.setUp()
preferences.clear()
}
@Test
fun singleLevelSubtask() {
val parent = addTask()
val child = addTask(with(PARENT, parent))
val tasks = query()
assertEquals(child, tasks[1].id)
assertEquals(parent, tasks[1].parent)
assertEquals(1, tasks[1].indent)
}
@Test
fun multiLevelSubtasks() {
val grandparent = addTask()
val parent = addTask(with(PARENT, grandparent))
val child = addTask(with(PARENT, parent))
val tasks = query()
assertEquals(child, tasks[2].id)
assertEquals(parent, tasks[2].parent)
assertEquals(2, tasks[2].indent)
}
@Test
fun parentWithOneChildHasChildrenCountOne() {
val parent = addTask()
addTask(with(PARENT, parent))
val tasks = query()
val parentTask = tasks.find { it.id == parent }!!
assertEquals(1, parentTask.children)
}
@Test
fun parentWithMultipleChildrenHasCorrectCount() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
val tasks = query()
val parentTask = tasks.find { it.id == parent }!!
assertEquals(3, parentTask.children)
}
@Test
fun grandparentCountsAllDescendants() {
val grandparent = addTask()
val parent = addTask(with(PARENT, grandparent))
addTask(with(PARENT, parent))
val tasks = query()
val grandparentTask = tasks.find { it.id == grandparent }!!
assertEquals(2, grandparentTask.children)
}
@Test
fun leafTaskHasNoChildren() {
val parent = addTask()
val child = addTask(with(PARENT, parent))
val tasks = query()
val childTask = tasks.find { it.id == child }!!
assertEquals(0, childTask.children)
}
@Test
fun deepHierarchyCountsAllDescendants() {
val root = addTask()
val level1 = addTask(with(PARENT, root))
val level2 = addTask(with(PARENT, level1))
val level3 = addTask(with(PARENT, level2))
addTask(with(PARENT, level3))
val tasks = query()
val rootTask = tasks.find { it.id == root }!!
assertEquals(4, rootTask.children)
}
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking {
val task = newTask(*properties)
taskDao.createNew(task)
task.id
}
private fun query(): List<TaskContainer> = runBlocking {
taskDao.fetchTasks(getQuery(preferences, filter))
}
}

@ -1,91 +0,0 @@
package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.filters.TodayFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.TaskMaker.DUE_DATE
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import javax.inject.Inject
@HiltAndroidTest
class RecursiveLoopTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Before
override fun setUp() {
super.setUp()
preferences.clear()
}
@Test
fun handleSelfLoop() = runBlocking {
addTask(with(DUE_DATE, newDateTime()), with(PARENT, 1L))
val tasks = getTasks()
assertEquals(1, tasks.size)
assertEquals(1L, tasks[0].id)
}
@Test
fun handleSingleLevelLoop() = runBlocking {
val parent = addTask(with(DUE_DATE, newDateTime()))
val child = addTask(with(PARENT, parent))
taskDao.setParent(child, listOf(parent))
val tasks = getTasks()
assertEquals(2, tasks.size)
assertEquals(parent, tasks[0].id)
assertEquals(child, tasks[1].id)
}
@Test
fun handleMultiLevelLoop() = runBlocking {
val parent = addTask(with(DUE_DATE, newDateTime()))
val child = addTask(with(PARENT, parent))
val grandchild = addTask(with(PARENT, child))
taskDao.setParent(grandchild, listOf(parent))
val tasks = getTasks()
assertEquals(3, tasks.size)
assertEquals(parent, tasks[0].id)
assertEquals(child, tasks[1].id)
assertEquals(grandchild, tasks[2].id)
}
@Test
fun descendantsRecursiveLoopBothMatchFilter() = runBlocking {
val parent = addTask(with(DUE_DATE, newDateTime()))
val child = addTask(with(DUE_DATE, newDateTime()), with(PARENT, parent))
taskDao.setParent(child, listOf(parent))
val tasks = getTasks()
assertEquals(2, tasks.size)
}
private suspend fun getTasks() = taskDao.fetchTasks(
getQuery(preferences, TodayFilter.create())
)
private suspend fun addTask(vararg properties: PropertyValue<in Task?, *>): Long {
val task = newTask(*properties)
taskDao.createNew(task)
return task.id
}
}

@ -1,287 +0,0 @@
package com.todoroo.astrid.alarms
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.createDueDate
import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Notification
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@HiltAndroidTest
class AlarmJobServiceTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var alarmService: AlarmService
@Test
fun testNoAlarms() = runBlocking {
testResults(emptyList(), 0)
}
@Test
fun futureAlarmWithNoPastAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 18).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
testResults(emptyList(), DateTime(2024, 5, 18, 18, 0).millis)
}
}
@Test
fun pastAlarmWithNoFutureAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
0
)
}
}
@Test
fun pastRecurringAlarmWithFutureRecurrence() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(
Alarm(
type = Alarm.TYPE_REL_END,
repeat = 1,
interval = TimeUnit.HOURS.toMillis(6)
)
)
)
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
DateTime(2024, 5, 18, 0, 0).millis
)
}
}
@Test
fun pastAlarmsRemoveSnoozed() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(
Alarm(type = Alarm.TYPE_REL_END),
Alarm(time = DateTimeUtils2.currentTimeMillis(), type = Alarm.TYPE_SNOOZE)
)
)
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
0
)
assertEquals(
listOf(Alarm(id = 1, task = 1, time = 0, type = Alarm.TYPE_REL_END)),
alarmService.getAlarms(1)
)
}
}
@Test
fun alarmsOneMinuteApart() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME,
DateTime(2024, 5, 17, 23, 20).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
taskDao.insert(Task())
alarmService.synchronizeAlarms(
taskId = 2,
alarms = mutableSetOf(
Alarm(
type = Alarm.TYPE_SNOOZE,
time = DateTime(2024, 5, 17, 23, 21).millis)
)
)
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
DateTime(2024, 5, 17, 23, 21).millis
)
}
}
@Test
fun futureSnoozeOverrideOverdue() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(
Alarm(type = Alarm.TYPE_REL_END),
Alarm(
time = DateTimeUtils2.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5),
type = Alarm.TYPE_SNOOZE
)
)
)
testResults(
emptyList(),
DateTimeUtils2.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)
)
}
}
@Test
fun ignoreStaleAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
reminderLast = DateTime(2024, 5, 17, 18, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
}
@Test
fun dontScheduleForCompletedTask() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
completionDate = DateTime(2024, 5, 17, 14, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
}
@Test
fun dontScheduleForDeletedTask() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
deletionDate = DateTime(2024, 5, 17, 14, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
}
private suspend fun testResults(notifications: List<Notification>, nextAlarm: Long) {
val actualNextAlarm = alarmService.triggerAlarms {
assertEquals(notifications, it)
it.forEach { taskDao.setLastNotified(it.taskId, DateTimeUtils2.currentTimeMillis()) }
}
assertEquals(nextAlarm, actualNextAlarm)
}
}

@ -1,138 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.dao
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@HiltAndroidTest
class TaskDaoTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter
/** Test basic task creation, fetch, and save */
@Test
fun testTaskCreation() = runBlocking {
assertEquals(0, taskDao.getAll().size)
// create task "happy"
var task = Task()
task.title = "happy"
taskDao.createNew(task)
assertEquals(1, taskDao.getAll().size)
val happyId = task.id
assertNotSame(Task.NO_ID, happyId)
task = taskDao.fetch(happyId)!!
assertEquals("happy", task.title)
// create task "sad"
task = Task()
task.title = "sad"
taskDao.createNew(task)
assertEquals(2, taskDao.getAll().size)
// rename sad to melancholy
val sadId = task.id
assertNotSame(Task.NO_ID, sadId)
task.title = "melancholy"
taskDao.save(task)
assertEquals(2, taskDao.getAll().size)
// check state
task = taskDao.fetch(happyId)!!
assertEquals("happy", task.title)
task = taskDao.fetch(sadId)!!
assertEquals("melancholy", task.title)
}
/** Test various task fetch conditions */
@Test
fun testTaskConditions() = runBlocking {
// create normal task
var task = Task()
task.title = "normal"
taskDao.createNew(task)
// create blank task
task = Task()
task.title = ""
taskDao.createNew(task)
// create hidden task
task = Task()
task.title = "hidden"
task.hideUntil = currentTimeMillis() + 10000
taskDao.createNew(task)
// create task with deadlines
task = Task()
task.title = "deadlineInFuture"
task.dueDate = currentTimeMillis() + 10000
taskDao.createNew(task)
task = Task()
task.title = "deadlineInPast"
task.dueDate = currentTimeMillis() - 10000
taskDao.createNew(task)
// create completed task
task = Task()
task.title = "completed"
task.completionDate = currentTimeMillis() - 10000
taskDao.createNew(task)
// check is active
assertEquals(5, taskDao.getActiveTasks().size)
// check is visible
assertEquals(5, taskDao.getActiveTasks().size)
}
/** Test task deletion */
@Test
fun testTDeletion() = runBlocking {
assertEquals(0, taskDao.getAll().size)
// create task "happy"
val task = Task()
task.title = "happy"
taskDao.createNew(task)
assertEquals(1, taskDao.getAll().size)
// delete
taskDeleter.delete(task)
assertEquals(0, taskDao.getAll().size)
}
/** Test save without prior create doesn't work */
@Test
fun testSaveWithoutCreate() = runBlocking {
// try to save task "happy"
val task = Task()
task.title = "happy"
task.id = 1L
taskDao.save(task)
assertEquals(0, taskDao.getAll().size)
}
/** Test passing invalid task indices to various things */
@Test
fun testInvalidIndex() = runBlocking {
assertEquals(0, taskDao.getAll().size)
assertNull(taskDao.fetch(1))
taskDeleter.delete(listOf(1L))
// make sure db still works
assertEquals(0, taskDao.getAll().size)
}
}

@ -1,142 +0,0 @@
package com.todoroo.astrid.gcal
import android.Manifest
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.provider.CalendarContract
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import androidx.core.net.toUri
import androidx.test.core.app.ApplicationProvider
import androidx.test.rule.GrantPermissionRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.tasks.TestUtilities.withTZ
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTime
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidTest
class GCalHelperTest : InjectingTestCase() {
@get:Rule
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
)
@Inject lateinit var gcalHelper: GCalHelper
private var testCalendarId: Long = -1
@Before
override fun setUp() {
super.setUp()
testCalendarId = createTestCalendar()
}
@After
fun tearDown() {
if (testCalendarId > 0) {
try {
val context = ApplicationProvider.getApplicationContext<Context>()
context.contentResolver.delete(
ContentUris.withAppendedId(Calendars.CONTENT_URI, testCalendarId),
null,
null
)
} catch (e: Exception) {
Timber.e(e)
}
}
}
@Test fun allDayEventInNewYork() = assertAllDayEvent("America/New_York") // UTC-5
@Test fun allDayEventInBerlin() = assertAllDayEvent("Europe/Berlin") // UTC+1
@Test fun allDayEventInAuckland() = assertAllDayEvent("Pacific/Auckland") // UTC+13
@Test fun allDayEventInTokyo() = assertAllDayEvent("Asia/Tokyo") // UTC+9
@Test fun allDayEventInHonolulu() = assertAllDayEvent("Pacific/Honolulu") // UTC-10
@Test fun allDayEventInChatham() = assertAllDayEvent("Pacific/Chatham") // UTC+13:45
private fun assertAllDayEvent(timezone: String) = withTZ(timezone) {
val task = Task(dueDate = DateTime(2024, 12, 20).millis)
val eventUri = gcalHelper.createTaskEvent(task, testCalendarId.toString())
?: throw RuntimeException("Event not created")
val event = queryEvent(eventUri.toString()) ?: throw RuntimeException("Event not found")
assertEquals(
"DTSTART should be Dec 20 00:00 UTC",
DateTime(2024, 12, 20, timeZone = DateTime.UTC).millis,
event.dtStart
)
assertEquals(
"DTEND should be Dec 21 00:00 UTC",
DateTime(2024, 12, 21, timeZone = DateTime.UTC).millis,
event.dtEnd
)
}
private fun createTestCalendar(): Long {
val context = ApplicationProvider.getApplicationContext<Context>()
val values = ContentValues().apply {
put(Calendars.ACCOUNT_NAME, "test@test.com")
put(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
put(Calendars.NAME, "Test Calendar")
put(Calendars.CALENDAR_DISPLAY_NAME, "Test Calendar")
put(Calendars.CALENDAR_COLOR, 0xFF0000)
put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
put(Calendars.OWNER_ACCOUNT, "test@test.com")
put(Calendars.VISIBLE, 1)
put(Calendars.SYNC_EVENTS, 1)
}
val uri = Calendars.CONTENT_URI.buildUpon()
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(Calendars.ACCOUNT_NAME, "test@test.com")
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
.build()
val calendarUri = context.contentResolver.insert(uri, values)
return ContentUris.parseId(calendarUri!!)
}
private fun queryEvent(eventUri: String): CalendarEvent? {
val context = ApplicationProvider.getApplicationContext<Context>()
val cursor = context.contentResolver.query(
eventUri.toUri(),
arrayOf(
Events.DTSTART,
Events.DTEND,
Events.ALL_DAY,
Events.EVENT_TIMEZONE
),
null,
null,
null
)
return cursor?.use {
if (it.moveToFirst()) {
CalendarEvent(
dtStart = it.getLong(0),
dtEnd = it.getLong(1),
allDay = it.getInt(2) == 1,
timezone = it.getString(3)
)
} else null
}
}
private data class CalendarEvent(
val dtStart: Long,
val dtEnd: Long,
val allDay: Boolean,
val timezone: String?
)
}

@ -1,93 +0,0 @@
package com.todoroo.astrid.gtasks
import com.google.api.services.tasks.model.TaskList
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.RemoteGtaskListMaker
import org.tasks.makers.RemoteGtaskListMaker.newRemoteList
import javax.inject.Inject
@HiltAndroidTest
class GtasksListServiceTest : InjectingTestCase() {
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var caldavDao: CaldavDao
private lateinit var gtasksListService: GtasksListService
@Before
override fun setUp() {
super.setUp()
gtasksListService = GtasksListService(caldavDao, taskDeleter, localBroadcastManager)
}
@Test
fun testCreateNewList() = runBlocking {
setLists(
newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "Default")))
assertEquals(
CaldavCalendar(id = 1, account = "account", uuid = "1", name = "Default"),
caldavDao.getCalendarById(1L)
)
}
@Test
fun testGetListByRemoteId() = runBlocking {
val list = CaldavCalendar(uuid = "1")
caldavDao.insert(list)
assertEquals(list, caldavDao.getCalendarByUuid("1"))
}
@Test
fun testGetListReturnsNullWhenNotFound() = runBlocking {
assertNull(caldavDao.getCalendarByUuid("1"))
}
@Test
fun testDeleteMissingList() = runBlocking {
caldavDao.insert(CaldavCalendar(account = "account", uuid = "1"))
val taskList = newRemoteList(with(RemoteGtaskListMaker.REMOTE_ID, "2"))
setLists(taskList)
assertEquals(
listOf(CaldavCalendar(id = 2, account = "account", uuid = "2", name = "Default")),
caldavDao.getCalendarsByAccount("account")
)
}
@Test
fun testUpdateListName() = runBlocking {
val calendar = CaldavCalendar(uuid = "1", name = "oldName", account = "account")
caldavDao.insert(calendar)
setLists(
newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "newName")))
assertEquals("newName", caldavDao.getCalendarById(calendar.id)!!.name)
}
@Test
fun testNewListLastSyncIsZero() = runBlocking {
setLists(TaskList().setId("1"))
assertEquals(0L, caldavDao.getCalendarByUuid("1")!!.lastSync)
}
private suspend fun setLists(vararg list: TaskList) {
val account = CaldavAccount(
username = "account",
uuid = "account",
)
caldavDao.insert(account)
gtasksListService.updateLists(account, listOf(*list))
}
}

@ -1,34 +0,0 @@
package com.todoroo.astrid.model
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeClock
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@HiltAndroidTest
class TaskTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Test
fun testSavedTaskHasCreationDate() = runBlocking {
freezeClock {
val task = Task()
taskDao.createNew(task)
assertEquals(currentTimeMillis(), task.creationDate)
}
}
@Test
fun testReadTaskFromDb() = runBlocking {
val task = Task()
taskDao.createNew(task)
val fromDb = taskDao.fetch(task.id)
assertEquals(task, fromDb)
}
}

@ -1,66 +0,0 @@
package com.todoroo.astrid.repeats
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskCompleter
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@HiltAndroidTest
class RepeatWithSubtasksTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskCompleter: TaskCompleter
@Test
fun uncompleteGrandchildren() = runBlocking {
val grandparent = taskDao.createNew(
Task(
recurrence = "RRULE:FREQ=DAILY"
)
)
val parent = taskDao.createNew(
Task(
parent = grandparent
)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
assertTrue(taskDao.fetch(child)!!.isCompleted)
taskCompleter.setComplete(grandparent)
assertFalse(taskDao.fetch(child)!!.isCompleted)
}
@Test
fun uncompleteGoogleTaskChildren() = runBlocking {
val parent = taskDao.createNew(
Task(
recurrence = "RRULE:FREQ=DAILY"
)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
assertTrue(taskDao.fetch(child)!!.isCompleted)
taskCompleter.setComplete(parent)
assertFalse(taskDao.fetch(child)!!.isCompleted)
}
}

@ -1,92 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service
import org.tasks.data.entity.Task
import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.data.dao.TagDataDao
import org.tasks.injection.InjectingTestCase
import java.util.*
import javax.inject.Inject
@HiltAndroidTest
class QuickAddMarkupTest : InjectingTestCase() {
private val tags = ArrayList<String>()
@Inject lateinit var tagDataDao: TagDataDao
private var task: Task? = null
@Test
fun testTags() {
whenTitleIs("this #cool")
assertTitleBecomes("this")
assertTagsAre("cool")
whenTitleIs("#cool task")
assertTitleBecomes("task")
assertTagsAre("cool")
whenTitleIs("doggie #nice #cute")
assertTitleBecomes("doggie")
assertTagsAre("nice", "cute")
}
@Test
fun testContexts() {
whenTitleIs("eat @home")
assertTitleBecomes("eat")
assertTagsAre("home")
whenTitleIs("buy oatmeal @store @morning")
assertTitleBecomes("buy oatmeal")
assertTagsAre("store", "morning")
whenTitleIs("look @ me")
assertTitleBecomes("look @ me")
assertTagsAre()
}
// --- helpers
@Test
fun testPriorities() {
whenTitleIs("eat !1")
assertTitleBecomes("eat")
assertPriority(Task.Priority.LOW)
whenTitleIs("super cool!")
assertTitleBecomes("super cool!")
whenTitleIs("stay alive !4")
assertTitleBecomes("stay alive")
assertPriority(Task.Priority.HIGH)
}
@Test
fun testMixed() {
whenTitleIs("eat #food !2")
assertTitleBecomes("eat")
assertTagsAre("food")
assertPriority(Task.Priority.MEDIUM)
}
private fun assertTagsAre(vararg expectedTags: String) {
val expected = listOf(*expectedTags)
assertEquals(expected.toString(), tags.toString())
}
private fun assertTitleBecomes(title: String) {
assertEquals(title, task!!.title)
}
private fun whenTitleIs(title: String) = runBlocking {
task = Task()
task!!.title = title
tags.clear()
TitleParser.parse(tagDataDao, task!!, tags)
}
private fun assertPriority(priority: Int) {
assertEquals(priority, task!!.priority)
}
}

@ -1,100 +0,0 @@
package com.todoroo.astrid.service
import com.todoroo.astrid.api.PermaSql.VALUE_EOD
import com.todoroo.astrid.api.PermaSql.VALUE_EOD_NEXT_WEEK
import com.todoroo.astrid.api.PermaSql.VALUE_EOD_TOMORROW
import org.tasks.data.entity.Task
import org.tasks.data.entity.Task.Companion.DUE_DATE
import org.tasks.data.entity.Task.Companion.HIDE_UNTIL
import org.tasks.data.entity.Task.Companion.URGENCY_SPECIFIC_DAY
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.R
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.createDueDate
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import javax.inject.Inject
@HiltAndroidTest
class TaskCreatorTest : InjectingTestCase() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCreator: TaskCreator
@Test
fun setStartAndDueFromFilter() = runBlocking {
val task = freezeAt(DateTime(2021, 2, 4, 14, 56, 34, 126)) {
taskCreator.create(mapOf(
HIDE_UNTIL.name!! to VALUE_EOD,
DUE_DATE.name!! to VALUE_EOD_TOMORROW
), null)
}
assertEquals(DateTime(2021, 2, 4).millis, task.hideUntil)
assertEquals(
createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis),
task.dueDate
)
}
@Test
fun setDefaultStartWithFilterDue() = runBlocking {
preferences.setString(R.string.p_default_hideUntil_key, Task.HIDE_UNTIL_DUE.toString())
val task = freezeAt(DateTime(2021, 2, 4, 14, 56, 34, 126)) {
taskCreator.create(mapOf(
DUE_DATE.name!! to VALUE_EOD
), null)
}
assertEquals(DateTime(2021, 2, 4).millis, task.hideUntil)
}
@Test
fun setStartAndDueFromPreferences() = runBlocking {
preferences.setString(R.string.p_default_urgency_key, Task.URGENCY_TODAY.toString())
preferences.setString(R.string.p_default_hideUntil_key, Task.HIDE_UNTIL_DUE.toString())
val task = freezeAt(DateTime(2021, 2, 4, 14, 56, 34, 126)) {
taskCreator.create(null, "test")
}
assertEquals(DateTime(2021, 2, 4).millis, task.hideUntil)
assertEquals(
createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 4).millis),
task.dueDate
)
}
@Test
fun filterStartOverridesDefaultStart() = runBlocking {
preferences.setString(R.string.p_default_urgency_key, Task.URGENCY_TODAY.toString())
preferences.setString(R.string.p_default_hideUntil_key, Task.HIDE_UNTIL_DUE.toString())
val task = freezeAt(DateTime(2021, 2, 4, 14, 56, 34, 126)) {
taskCreator.create(mapOf(
HIDE_UNTIL.name!! to VALUE_EOD_NEXT_WEEK
), null)
}
assertEquals(DateTime(2021, 2, 11).millis, task.hideUntil)
}
@Test
fun filterDueOverridesDefaultDue() = runBlocking {
preferences.setString(R.string.p_default_urgency_key, Task.URGENCY_TODAY.toString())
val task = freezeAt(DateTime(2021, 2, 4, 14, 56, 34, 126)) {
taskCreator.create(mapOf(
DUE_DATE.name!! to VALUE_EOD_TOMORROW
), null)
}
assertEquals(
createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis),
task.dueDate
)
}
}

@ -1,39 +0,0 @@
package com.todoroo.astrid.service
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase
import javax.inject.Inject
@HiltAndroidTest
class TaskDeleterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter
@Test
fun markTaskAsDeleted() = runBlocking {
val task = Task()
taskDao.createNew(task)
taskDeleter.markDeleted(task)
assertTrue(taskDao.fetch(task.id)!!.isDeleted)
}
@Test
fun dontDeleteReadOnlyTasks() = runBlocking {
val task = Task(
readOnly = true
)
taskDao.createNew(task)
taskDeleter.markDeleted(task)
assertFalse(taskDao.fetch(task.id)!!.isDeleted)
}
}

@ -1,329 +0,0 @@
package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.filters.CaldavFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@HiltAndroidTest
class TaskMoverTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskMover: TaskMover
@Before
fun setup() {
runBlocking {
caldavDao.insert(CaldavCalendar(uuid = "1", account = "account1"))
caldavDao.insert(CaldavCalendar(uuid = "2", account = "account2"))
}
}
@Test
fun moveBetweenGoogleTaskLists() = runBlocking {
setAccountType("account1", TYPE_GOOGLE_TASKS)
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1)
assertEquals("2", googleTaskDao.getByTaskId(1)?.calendar)
}
@Test
fun deleteGoogleTaskAfterMove() = runBlocking {
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(1, "account1")
assertEquals(1, deleted.size.toLong())
assertEquals(1, deleted[0].task)
assertTrue(deleted[0].deleted > 0)
}
@Test
fun moveChildrenBetweenGoogleTaskLists() = runBlocking {
setAccountType("account1", TYPE_GOOGLE_TASKS)
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1)
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(2, "account1")
assertEquals(1, deleted.size.toLong())
assertEquals(2, deleted[0].task)
assertTrue(deleted[0].deleted > 0)
assertEquals(1L, taskDao.fetch(2)?.parent)
assertEquals("2", googleTaskDao.getByTaskId(2)?.calendar)
}
@Test
fun moveBetweenCaldavList() = runBlocking {
createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("2", 1)
assertEquals("2", caldavDao.getTask(1)!!.calendar)
}
@Test
fun deleteCaldavTaskAfterMove() = runBlocking {
createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("2", 1)
val deleted = caldavDao.getMoved("1")
assertEquals(1, deleted.size.toLong())
assertEquals(1, deleted[0].task)
assertTrue(deleted[0].deleted > 0)
}
@Test
fun moveRecursiveCaldavChildren() = runBlocking {
createTasks(1)
createSubtask(2, 1)
createSubtask(3, 2)
caldavDao.insert(
listOf(
newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask(
with(TASK, 2L),
with(CALENDAR, "1"),
with(REMOTE_ID, "b"),
with(REMOTE_PARENT, "a")),
newCaldavTask(
with(TASK, 3L),
with(CALENDAR, "1"),
with(REMOTE_PARENT, "b"))))
moveToCaldavList("2", 1)
val deleted = caldavDao.getMoved("1")
assertEquals(3, deleted.size.toLong())
val task = caldavDao.getTask(3)
assertEquals("2", task!!.calendar)
assertEquals(2, taskDao.fetch(3)!!.parent)
}
@Test
fun moveGoogleTaskChildrenToCaldav() = runBlocking {
setAccountType("account1", TYPE_GOOGLE_TASKS)
setAccountType("account2", TYPE_CALDAV)
createTasks(1)
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToCaldavList("2", 1)
val task = caldavDao.getTask(2)
assertEquals("2", task!!.calendar)
assertEquals(1L, taskDao.fetch(2)?.parent)
}
@Test
fun flattenLocalSubtasksWhenMovingToGoogleTasks() = runBlocking {
createTasks(1)
createSubtask(2, 1)
createSubtask(3, 2)
moveToGoogleTasks("1", 1)
assertEquals(1L, taskDao.fetch(3)?.parent)
}
@Test
fun moveLocalChildToGoogleTasks() = runBlocking {
createTasks(1)
createSubtask(2, 1)
moveToGoogleTasks("1", 2)
assertEquals(0L, taskDao.fetch(2)?.parent)
}
@Test
fun moveLocalChildToCaldav() = runBlocking {
createTasks(1)
createSubtask(2, 1)
moveToCaldavList("1", 2)
assertEquals(0, taskDao.fetch(2)!!.parent)
}
@Test
fun flattenCaldavSubtasksWhenMovingToGoogleTasks() = runBlocking {
createTasks(1)
createSubtask(2, 1)
createSubtask(3, 2)
caldavDao.insert(
listOf(
newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask(
with(TASK, 2L),
with(CALENDAR, "1"),
with(REMOTE_ID, "b"),
with(REMOTE_PARENT, "a")),
newCaldavTask(
with(TASK, 3L),
with(CALENDAR, "1"),
with(REMOTE_PARENT, "b"))))
moveToGoogleTasks("2", 1)
val task = taskDao.fetch(3L)
assertEquals(1L, task?.parent)
}
@Test
fun moveGoogleTaskChildWithoutParent() = runBlocking {
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1)
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToGoogleTasks("2", 2)
assertEquals(0L, taskDao.fetch(2)?.parent)
assertEquals("2", googleTaskDao.getByTaskId(2)?.calendar)
}
@Test
fun moveCaldavChildWithoutParent() = runBlocking {
createTasks(1)
createSubtask(2, 1)
caldavDao.insert(
listOf(
newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask(
with(TASK, 2L),
with(CALENDAR, "1"),
with(REMOTE_PARENT, "a"))))
moveToCaldavList("2", 2)
assertEquals("2", caldavDao.getTask(2)!!.calendar)
assertEquals(0, taskDao.fetch(2)!!.parent)
}
@Test
fun moveGoogleTaskToCaldav() = runBlocking {
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
moveToCaldavList("2", 1)
assertEquals("2", caldavDao.getTask(1)!!.calendar)
}
@Test
fun moveCaldavToGoogleTask() = runBlocking {
setAccountType("account1", TYPE_CALDAV)
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1)
assertEquals("2", googleTaskDao.getByTaskId(1L)?.calendar)
}
@Test
fun moveLocalToCaldav() = runBlocking {
createTasks(1)
createSubtask(2, 1)
createSubtask(3, 2)
moveToCaldavList("1", 1)
assertEquals("1", caldavDao.getTask(3)?.calendar)
assertEquals(2L, taskDao.fetch(3)?.parent)
}
@Test
fun moveToSameGoogleTaskListIsNoop() = runBlocking {
setAccountType("account1", TYPE_GOOGLE_TASKS)
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
moveToGoogleTasks("1", 1)
assertTrue(googleTaskDao.getDeletedByTaskId(1, "account1").isEmpty())
assertEquals(1, googleTaskDao.getAllByTaskId(1).size.toLong())
}
@Test
fun moveToSameCaldavListIsNoop() = runBlocking {
createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("1", 1)
assertTrue(caldavDao.getMoved("1").isEmpty())
assertEquals(1, caldavDao.getTasks(1).size.toLong())
}
@Test
fun dontDuplicateWhenParentAndChildGoogleTaskMoved() = runBlocking {
createTasks(1)
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1, 2)
assertEquals(1, googleTaskDao.getAllByTaskId(2).filter { it.deleted == 0L }.size)
}
@Test
fun dontDuplicateWhenParentAndChildCaldavMoved() = runBlocking {
createTasks(1)
createSubtask(2, 1)
caldavDao.insert(
listOf(
newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask(
with(TASK, 2L),
with(CALENDAR, "1"),
with(REMOTE_PARENT, "a"))))
moveToCaldavList("2", 1, 2)
assertEquals(1, caldavDao.getTasks(2).filter { it.deleted == 0L }.size)
}
private suspend fun createTasks(vararg ids: Long) {
for (id in ids) {
taskDao.createNew(newTask(with(ID, id)))
}
}
private suspend fun createSubtask(id: Long, parent: Long) {
taskDao.createNew(newTask(with(ID, id), with(PARENT, parent)))
}
private suspend fun moveToGoogleTasks(list: String, vararg tasks: Long) {
taskMover.move(
tasks.toList(),
CaldavFilter(
calendar = CaldavCalendar(uuid = list),
account = CaldavAccount(accountType = TYPE_GOOGLE_TASKS)
)
)
}
private suspend fun moveToCaldavList(calendar: String, vararg tasks: Long) {
taskMover.move(
tasks.toList(),
CaldavFilter(
CaldavCalendar(name = "", uuid = calendar),
account = CaldavAccount(accountType = TYPE_CALDAV)
)
)
}
private suspend fun setAccountType(account: String, type: Int) {
caldavDao.insert(
CaldavAccount(
uuid = account,
accountType = type,
)
)
}
}

@ -1,442 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service
import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import net.fortuna.ical4j.model.Recur.Frequency.DAILY
import net.fortuna.ical4j.model.Recur.Frequency.MONTHLY
import net.fortuna.ical4j.model.Recur.Frequency.WEEKLY
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.tasks.R
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Task
import org.tasks.data.newLocalAccount
import org.tasks.date.DateTimeUtils
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences
import org.tasks.repeats.RecurrenceUtils.newRecur
import java.util.Calendar
import javax.inject.Inject
@HiltAndroidTest
class TitleParserTest : InjectingTestCase() {
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var caldavDao: CaldavDao
@Before
override fun setUp() {
runBlocking {
super.setUp()
preferences.setStringFromInteger(R.string.p_default_urgency_key, 0)
caldavDao.newLocalAccount()
}
}
/**
* test that completing a task w/ no regular expressions creates a simple task with no date, no
* repeat, no lists
*/
@Test
fun testNoRegexes() = runBlocking {
val task = taskCreator.basicQuickAddTask("Jog")
val nothing = Task()
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
assertEquals(task.recurrence, nothing.recurrence)
}
/** Tests correct date is parsed */
@Test
fun testMonthDate() {
val titleMonthStrings = arrayOf(
"Jan.", "January",
"Feb.", "February",
"Mar.", "March",
"Apr.", "April",
"May", "May",
"Jun.", "June",
"Jul.", "July",
"Aug.", "August",
"Sep.", "September",
"Oct.", "October",
"Nov.", "November",
"Dec.", "December"
)
for (i in 0..22) {
val testTitle = "Jog on " + titleMonthStrings[i] + " 12."
val task = insertTitleAddTask(testTitle)
val date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.monthOfYear, i / 2 + 1)
assertEquals(date.dayOfMonth, 12)
}
}
@Test
fun testMonthSlashDay() {
for (i in 1..12) {
val testTitle = "Jog on $i/12/13"
val task = insertTitleAddTask(testTitle)
val date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.monthOfYear, i)
assertEquals(date.dayOfMonth, 12)
assertEquals(date.year, 2013)
}
}
@Test
fun testArmyTime() {
val testTitle = "Jog on 23:21."
val task = insertTitleAddTask(testTitle)
val date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.hourOfDay, 23)
assertEquals(date.minuteOfHour, 21)
}
@Test
fun test_AM_PM() {
val testTitle = "Jog at 8:33 PM."
val task = insertTitleAddTask(testTitle)
val date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.hourOfDay, 20)
assertEquals(date.minuteOfHour, 33)
}
@Test
fun test_at_hour() {
val testTitle = "Jog at 8 PM."
val task = insertTitleAddTask(testTitle)
val date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.hourOfDay, 20)
assertEquals(date.minuteOfHour, 0)
}
@Test
fun test_oclock_AM() {
val testTitle = "Jog at 8 o'clock AM."
val task = insertTitleAddTask(testTitle)
val date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.hourOfDay, 8)
assertEquals(date.minuteOfHour, 0)
}
@Test
fun test_several_forms_of_eight() {
val testTitles = arrayOf("Jog 8 AM", "Jog 8 o'clock AM", "at 8:00 AM")
for (testTitle in testTitles) {
val task = insertTitleAddTask(testTitle)
val date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.hourOfDay, 8)
assertEquals(date.minuteOfHour, 0)
}
}
@Test
fun test_several_forms_of_1230PM() {
val testTitles = arrayOf(
"Jog 12:30 PM", "at 12:30 PM", "Do something on 12:30 PM", "Jog at 12:30 PM Friday"
)
for (testTitle in testTitles) {
val task = insertTitleAddTask(testTitle)
val date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.hourOfDay, 12)
assertEquals(date.minuteOfHour, 30)
}
}
private fun insertTitleAddTask(title: String): Task = runBlocking {
taskCreator.createWithValues(title)
}
// ----------------Days begin----------------//
@Test
@Ignore("Flaky test")
fun testDays() = runBlocking {
val today = Calendar.getInstance()
var title = "Jog today"
var task = taskCreator.createWithValues(title)
var date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.dayOfWeek, today[Calendar.DAY_OF_WEEK])
// Calendar starts 1-6, date.getDay() starts at 0
title = "Jog tomorrow"
task = taskCreator.createWithValues(title)
date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.dayOfWeek % 7, (today[Calendar.DAY_OF_WEEK] + 1) % 7)
val days = arrayOf(
"sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday")
val abrevDays = arrayOf("sun.", "mon.", "tue.", "wed.", "thu.", "fri.", "sat.")
for (i in 1..6) {
title = "Jog " + days[i]
task = taskCreator.createWithValues(title)
date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.dayOfWeek, i + 1)
title = "Jog " + abrevDays[i]
task = taskCreator.createWithValues(title)
date = DateTimeUtils.newDateTime(task.dueDate)
assertEquals(date.dayOfWeek, i + 1)
}
}
// ----------------Days end----------------//
// ----------------Priority begin----------------//
/** tests all words using priority 0 */
@Test
fun testPriority0() = runBlocking {
val acceptedStrings = arrayOf("priority 0", "least priority", "lowest priority", "bang 0")
for (acceptedString in acceptedStrings) {
val title = "Jog $acceptedString"
val task = taskCreator.createWithValues(title)
assertEquals(task.priority, Task.Priority.NONE)
}
for (acceptedString in acceptedStrings) {
val title = "$acceptedString jog"
val task = taskCreator.createWithValues(title)
assertNotSame(task.priority, Task.Priority.NONE)
}
}
@Test
fun testPriority1() = runBlocking {
val acceptedStringsAtEnd = arrayOf("priority 1", "low priority", "bang", "bang 1")
val acceptedStringsAnywhere = arrayOf("!1", "!")
var task: Task
for (acceptedStringAtEnd in acceptedStringsAtEnd) {
task = taskCreator.basicQuickAddTask(
"Jog $acceptedStringAtEnd") // test at end of task. should set importance.
assertEquals(task.priority, Task.Priority.LOW)
}
for (acceptedStringAtEnd in acceptedStringsAtEnd) {
task = taskCreator.basicQuickAddTask(acceptedStringAtEnd
+ " jog") // test at beginning of task. should not set importance.
assertEquals(task.priority, Task.Priority.LOW)
}
for (acceptedStringAnywhere in acceptedStringsAnywhere) {
task = taskCreator.basicQuickAddTask(
"Jog $acceptedStringAnywhere") // test at end of task. should set importance.
assertEquals(task.priority, Task.Priority.LOW)
task = taskCreator.basicQuickAddTask(
"$acceptedStringAnywhere jog") // test at beginning of task. should set importance.
assertEquals(task.priority, Task.Priority.LOW)
}
}
@Test
fun testPriority2() = runBlocking {
val acceptedStringsAtEnd = arrayOf("priority 2", "high priority", "bang bang", "bang 2")
val acceptedStringsAnywhere = arrayOf("!2", "!!")
for (acceptedStringAtEnd in acceptedStringsAtEnd) {
var title = "Jog $acceptedStringAtEnd"
var task = taskCreator.createWithValues(title)
assertEquals(task.priority, Task.Priority.MEDIUM)
title = "$acceptedStringAtEnd jog"
task = taskCreator.createWithValues(title)
assertNotSame(task.priority, Task.Priority.MEDIUM)
}
for (acceptedStringAnywhere in acceptedStringsAnywhere) {
var title = "Jog $acceptedStringAnywhere"
var task = taskCreator.createWithValues(title)
assertEquals(task.priority, Task.Priority.MEDIUM)
title = "$acceptedStringAnywhere jog"
task = taskCreator.createWithValues(title)
assertEquals(task.priority, Task.Priority.MEDIUM)
}
}
@Test
fun testPriority3() = runBlocking {
val acceptedStringsAtEnd = arrayOf(
"priority 3",
"highest priority",
"bang bang bang",
"bang 3",
"bang bang bang bang bang bang bang"
)
val acceptedStringsAnywhere = arrayOf("!3", "!!!", "!6", "!!!!!!!!!!!!!")
for (acceptedStringAtEnd in acceptedStringsAtEnd) {
var title = "Jog $acceptedStringAtEnd"
var task = taskCreator.createWithValues(title)
assertEquals(task.priority, Task.Priority.HIGH)
title = "$acceptedStringAtEnd jog"
task = taskCreator.createWithValues(title)
assertNotSame(task.priority, Task.Priority.HIGH)
}
for (acceptedStringAnywhere in acceptedStringsAnywhere) {
var title = "Jog $acceptedStringAnywhere"
var task = taskCreator.createWithValues(title)
assertEquals(task.priority, Task.Priority.HIGH)
title = "$acceptedStringAnywhere jog"
task = taskCreator.createWithValues(title)
assertEquals(task.priority, Task.Priority.HIGH)
}
}
// ----------------Priority end----------------//
// ----------------Repeats begin----------------//
/** test daily repeat from due date, but with no due date set */
@Test
fun testDailyWithNoDueDate() = runBlocking {
var title = "Jog daily"
var task = taskCreator.createWithValues(title)
val recur = newRecur()
recur.setFrequency(DAILY.name)
recur.interval = 1
assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
title = "Jog every day"
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
for (i in 1..12) {
title = "Jog every $i days."
recur.interval = i
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
}
}
/** test weekly repeat from due date, with no due date & time set */
@Test
fun testWeeklyWithNoDueDate() = runBlocking {
var title = "Jog weekly"
var task = taskCreator.createWithValues(title)
val recur = newRecur()
recur.setFrequency(WEEKLY.name)
recur.interval = 1
assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
title = "Jog every week"
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
for (i in 1..12) {
title = "Jog every $i weeks"
recur.interval = i
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
}
}
/** test hourly repeat from due date, with no due date but no time */
@Test
fun testMonthlyFromNoDueDate() = runBlocking {
var title = "Jog monthly"
var task = taskCreator.createWithValues(title)
val recur = newRecur()
recur.setFrequency(MONTHLY.name)
recur.interval = 1
assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
title = "Jog every month"
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
for (i in 1..12) {
title = "Jog every $i months"
recur.interval = i
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate())
}
}
@Test
fun testDailyFromDueDate() = runBlocking {
var title = "Jog daily starting from today"
var task = taskCreator.createWithValues(title)
val recur = newRecur()
recur.setFrequency(DAILY.name)
recur.interval = 1
assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate())
title = "Jog every day starting from today"
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate())
for (i in 1..12) {
title = "Jog every $i days starting from today"
recur.interval = i
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate())
}
}
@Test
fun testWeeklyFromDueDate() = runBlocking {
var title = "Jog weekly starting from today"
var task = taskCreator.createWithValues(title)
val recur = newRecur()
recur.setFrequency(WEEKLY.name)
recur.interval = 1
assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate())
title = "Jog every week starting from today"
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate())
for (i in 1..12) {
title = "Jog every $i weeks starting from today"
recur.interval = i
task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString())
assertTrue(task.hasDueDate())
}
}
// ----------------Repeats end----------------//
// ----------------Tags begin----------------//
/** tests all words using priority 0 */
@Test
fun testTagsPound() = runBlocking {
val acceptedStrings = arrayOf("#tag", "#a", "#(a cool tag)", "#(cool)")
var task: Task
for (acceptedString in acceptedStrings) {
task = Task()
task.title = "Jog $acceptedString" // test at end of task. should set importance.
val tags = ArrayList<String>()
TitleParser.listHelper(tagDataDao, task, tags)
val tag = TitleParser.trimParenthesis(acceptedString)
assertTrue(
"test pound at failed for string: $acceptedString for tags: $tags",
tags.contains(tag))
}
}
/** tests all words using priority 0 */
@Test
fun testTagsAt() = runBlocking {
val acceptedStrings = arrayOf("@tag", "@a", "@(a cool tag)", "@(cool)")
var task: Task
for (acceptedString in acceptedStrings) {
task = Task()
task.title = "Jog $acceptedString" // test at end of task. should set importance.
val tags = ArrayList<String>()
TitleParser.listHelper(tagDataDao, task, tags)
val tag = TitleParser.trimParenthesis(acceptedString)
assertTrue(
"testTagsAt failed for string: $acceptedString for tags: $tags",
tags.contains(tag))
}
}
}

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

@ -1,217 +0,0 @@
@file:Suppress("ClassName")
package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.TestUtilities.assertEquals
import org.tasks.caldav.VtodoCache
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.DUE_DATE
import org.tasks.makers.TaskMaker.HIDE_TYPE
import org.tasks.makers.TaskMaker.MODIFICATION_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.opentasks.TestOpenTaskDao
import org.tasks.time.DateTime
import javax.inject.Inject
@HiltAndroidTest
class Upgrade_11_3_Test : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var openTaskDao: TestOpenTaskDao
@Inject lateinit var upgrader: Upgrade_11_3
@Inject lateinit var vtodoCache: VtodoCache
private lateinit var calendar: CaldavCalendar
@Before
override fun setUp() {
super.setUp()
calendar = CaldavCalendar()
runBlocking {
caldavDao.insert(calendar)
}
}
@Test
fun applyRemoteiCalendarStartDate() = runBlocking {
val taskId = taskDao.insert(newTask())
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE)
upgrader.applyiCalendarStartDates()
assertEquals(DateTime(2021, 1, 21), taskDao.fetch(taskId)?.hideUntil)
}
@Test
fun ignoreRemoteiCalendarStartDate() = runBlocking {
val taskId = taskDao.insert(newTask(
with(DUE_DATE, DateTime(2021, 1, 20)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE)
))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE)
upgrader.applyiCalendarStartDates()
assertEquals(DateTime(2021, 1, 20), taskDao.fetch(taskId)?.hideUntil)
}
@Test
fun touchTaskWithLocaliCalendarStartDate() = runBlocking {
val upgradeTime = DateTime(2021, 1, 21, 11, 47, 32, 450)
val taskId = taskDao.insert(newTask(
with(DUE_DATE, DateTime(2021, 1, 20)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE),
with(MODIFICATION_TIME, DateTime(2021, 1, 21, 9, 50, 4, 348))
))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE)
freezeAt(upgradeTime) {
upgrader.applyiCalendarStartDates()
}
assertEquals(upgradeTime, taskDao.fetch(taskId)?.modificationDate)
}
@Test
fun dontTouchWhenNoiCalendarStartDate() = runBlocking {
val modificationTime = DateTime(2021, 1, 21, 9, 50, 4, 348)
val taskId = taskDao.insert(newTask(with(MODIFICATION_TIME, modificationTime)))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_NO_START_DATE)
upgrader.applyiCalendarStartDates()
assertEquals(modificationTime, taskDao.fetch(taskId)?.modificationDate)
}
@Test
fun applyRemoteOpenTaskStartDate() = runBlocking {
val (listId, list) = openTaskDao.insertList()
openTaskDao.insertTask(listId, VTODO_WITH_START_DATE)
val taskId = taskDao.insert(newTask())
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(REMOTE_ID, "4586964443060640060"),
with(TASK, taskId)
))
upgrader.applyOpenTaskStartDates()
assertEquals(DateTime(2021, 1, 21), taskDao.fetch(taskId)?.hideUntil)
}
@Test
fun ignoreRemoteOpenTaskStartDate() = runBlocking {
val (listId, list) = openTaskDao.insertList()
openTaskDao.insertTask(listId, VTODO_WITH_START_DATE)
val taskId = taskDao.insert(newTask(
with(DUE_DATE, DateTime(2021, 1, 20)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE)
))
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(REMOTE_ID, "4586964443060640060"),
with(TASK, taskId)
))
upgrader.applyOpenTaskStartDates()
assertEquals(DateTime(2021, 1, 20), taskDao.fetch(taskId)?.hideUntil)
}
@Test
fun touchWithOpenTaskStartDate() = runBlocking {
val upgradeTime = DateTime(2021, 1, 21, 11, 47, 32, 450)
val (listId, list) = openTaskDao.insertList()
openTaskDao.insertTask(listId, VTODO_WITH_START_DATE)
val taskId = taskDao.insert(newTask(
with(DUE_DATE, DateTime(2021, 1, 20)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE),
with(MODIFICATION_TIME, DateTime(2021, 1, 21, 9, 50, 4, 348))
))
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(REMOTE_ID, "4586964443060640060"),
with(TASK, taskId)
))
freezeAt(upgradeTime) {
upgrader.applyOpenTaskStartDates()
}
assertEquals(upgradeTime, taskDao.fetch(taskId)?.modificationDate)
}
@Test
fun dontTouchNoOpenTaskStartDate() = runBlocking {
val modificationTime = DateTime(2021, 1, 21, 9, 50, 4, 348)
val (listId, list) = openTaskDao.insertList()
openTaskDao.insertTask(listId, VTODO_NO_START_DATE)
val taskId = taskDao.insert(newTask(with(MODIFICATION_TIME, modificationTime)))
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(REMOTE_ID, "4586964443060640060"),
with(TASK, taskId)
))
upgrader.applyOpenTaskStartDates()
assertEquals(modificationTime, taskDao.fetch(taskId)?.modificationDate)
}
companion object {
val VTODO_WITH_START_DATE = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-110301//EN
BEGIN:VTODO
DTSTAMP:20210121T153032Z
UID:4586964443060640060
CREATED:20210121T153000Z
LAST-MODIFIED:20210121T153029Z
SUMMARY:Test
PRIORITY:9
X-APPLE-SORT-ORDER:-27
DTSTART;VALUE=DATE:20210121
END:VTODO
END:VCALENDAR
""".trimIndent()
val VTODO_NO_START_DATE = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-110301//EN
BEGIN:VTODO
DTSTAMP:20210121T153032Z
UID:4586964443060640060
CREATED:20210121T153000Z
LAST-MODIFIED:20210121T153029Z
SUMMARY:Test
PRIORITY:9
X-APPLE-SORT-ORDER:-27
END:VTODO
END:VCALENDAR
""".trimIndent()
}
}

@ -1,62 +0,0 @@
package com.todoroo.astrid.subtasks
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.data.entity.TaskListMetadata
@HiltAndroidTest
class SubtasksHelperTest : SubtasksTestCase() {
@Before
override fun setUp() {
super.setUp()
createTasks()
val m = TaskListMetadata()
m.filter = TaskListMetadata.FILTER_ID_ALL
runBlocking {
updater.initializeFromSerializedTree(
m, filter, SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE))
}
}
private fun createTask(title: String, uuid: String) = runBlocking {
val t = Task()
t.title = title
t.uuid = uuid
taskDao.createNew(t)
}
private fun createTasks() {
createTask("A", "6") // Local id 1
createTask("B", "4") // Local id 2
createTask("C", "3") // Local id 3
createTask("D", "1") // Local id 4
createTask("E", "2") // Local id 5
createTask("F", "5") // Local id 6
}
// Default order: "[-1, [1, 2, [3, 4]], 5, 6]"
@Test
fun testOrderedIdArray() {
val ids = SubtasksHelper.getStringIdArray(DEFAULT_SERIALIZED_TREE)
assertEquals(EXPECTED_ORDER.size, ids.size)
for (i in EXPECTED_ORDER.indices) {
assertEquals(EXPECTED_ORDER[i], ids[i])
}
}
@Test
fun testLocalToRemoteIdMapping() = runBlocking {
val mapped = SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE)
.replace("\\s".toRegex(), "")
assertEquals(EXPECTED_REMOTE, mapped)
}
companion object {
private val EXPECTED_ORDER = arrayOf("-1", "1", "2", "3", "4", "5", "6")
private val EXPECTED_REMOTE = """["-1", ["6", "4", ["3", "1"]], "2", "5"]""".replace("\\s".toRegex(), "")
}
}

@ -1,123 +0,0 @@
package com.todoroo.astrid.subtasks
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.tasks.data.entity.TaskListMetadata
@HiltAndroidTest
class SubtasksMovingTest : SubtasksTestCase() {
private lateinit var A: Task
private lateinit var B: Task
private lateinit var C: Task
private lateinit var D: Task
private lateinit var E: Task
private lateinit var F: Task
@Before
override fun setUp() {
super.setUp()
createTasks()
val m = TaskListMetadata()
m.filter = TaskListMetadata.FILTER_ID_ALL
runBlocking {
updater.initializeFromSerializedTree(
m, filter, SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE))
}
// Assert initial state is correct
expectParentAndPosition(A, null, 0)
expectParentAndPosition(B, A, 0)
expectParentAndPosition(C, A, 1)
expectParentAndPosition(D, C, 0)
expectParentAndPosition(E, null, 1)
expectParentAndPosition(F, null, 2)
}
private fun createTasks() {
A = createTask("A")
B = createTask("B")
C = createTask("C")
D = createTask("D")
E = createTask("E")
F = createTask("F")
}
private fun createTask(title: String): Task = runBlocking {
val task = Task()
task.title = title
taskDao.createNew(task)
task
}
private fun whenTriggerMoveBefore(target: Task?, before: Task?) = runBlocking {
val beforeId = before?.uuid ?: "-1"
updater.moveTo(TaskListMetadata(), filter, target!!.uuid, beforeId)
}
/* Starting State (see SubtasksTestCase):
*
* A
* B
* C
* D
* E
* F
*/
@Test
fun testMoveBeforeIntoSelf() { // Should have no effect
whenTriggerMoveBefore(A, B)
expectParentAndPosition(A, null, 0)
expectParentAndPosition(B, A, 0)
expectParentAndPosition(C, A, 1)
expectParentAndPosition(D, C, 0)
expectParentAndPosition(E, null, 1)
expectParentAndPosition(F, null, 2)
}
@Test
fun testMoveIntoDescendant() { // Should have no effect
whenTriggerMoveBefore(A, C)
expectParentAndPosition(A, null, 0)
expectParentAndPosition(B, A, 0)
expectParentAndPosition(C, A, 1)
expectParentAndPosition(D, C, 0)
expectParentAndPosition(E, null, 1)
expectParentAndPosition(F, null, 2)
}
@Test
fun testMoveToEndOfChildren() { // Should have no effect
whenTriggerMoveBefore(A, E)
expectParentAndPosition(A, null, 0)
expectParentAndPosition(B, A, 0)
expectParentAndPosition(C, A, 1)
expectParentAndPosition(D, C, 0)
expectParentAndPosition(E, null, 1)
expectParentAndPosition(F, null, 2)
}
@Test
fun testStandardMove() {
whenTriggerMoveBefore(A, F)
expectParentAndPosition(A, null, 1)
expectParentAndPosition(B, A, 0)
expectParentAndPosition(C, A, 1)
expectParentAndPosition(D, C, 0)
expectParentAndPosition(E, null, 0)
expectParentAndPosition(F, null, 2)
}
@Test
fun testMoveToEndOfList() {
whenTriggerMoveBefore(A, null)
expectParentAndPosition(A, null, 2)
expectParentAndPosition(B, A, 0)
expectParentAndPosition(C, A, 1)
expectParentAndPosition(D, C, 0)
expectParentAndPosition(E, null, 0)
expectParentAndPosition(F, null, 1)
}
}

@ -1,49 +0,0 @@
package com.todoroo.astrid.subtasks
import com.todoroo.astrid.dao.TaskDao
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.tasks.data.dao.TaskListMetadataDao
import org.tasks.data.entity.Task
import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences
import javax.inject.Inject
abstract class SubtasksTestCase : InjectingTestCase() {
lateinit var updater: SubtasksFilterUpdater
lateinit var filter: AstridOrderingFilter
@Inject lateinit var taskListMetadataDao: TaskListMetadataDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
override fun setUp() {
super.setUp()
filter = runBlocking { MyTasksFilter.create() }
preferences.clear(SubtasksFilterUpdater.ACTIVE_TASKS_ORDER)
updater = SubtasksFilterUpdater(taskListMetadataDao, taskDao)
}
fun expectParentAndPosition(task: Task, parent: Task?, positionInParent: Int) {
val parentId = parent?.uuid ?: "-1"
val n = updater.findNodeForTask(task.uuid)
assertNotNull("No node found for task " + task.title, n)
assertEquals("Parent mismatch", parentId, n!!.parent!!.uuid)
assertEquals("Position mismatch", positionInParent, n.parent!!.children.indexOf(n))
}
companion object {
/* Starting State:
*
* A
* B
* C
* D
* E
* F
*/
val DEFAULT_SERIALIZED_TREE = "[-1, [1, 2, [3, 4]], 5, 6]".replace("\\s".toRegex(), "")
}
}

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

@ -1,23 +0,0 @@
package com.todoroo.astrid.sync
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotEquals
import org.junit.Test
@HiltAndroidTest
class SyncModelTest : NewSyncTestCase() {
@Test
fun testCreateTaskMakesUuid() = runBlocking{
val task = createTask()
assertNotEquals(Task.NO_UUID, task.uuid)
}
@Test
fun testCreateTagMakesUuid() = runBlocking{
val tag = createTagData()
assertNotEquals(Task.NO_UUID, tag.remoteId)
}
}

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

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

@ -1,13 +0,0 @@
package org.tasks
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
@Suppress("unused")
class TestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}

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

@ -1,18 +0,0 @@
package org.tasks.caldav
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.tasks.injection.InjectingTestCase
import javax.inject.Inject
@HiltAndroidTest
class CaldavClientTest : InjectingTestCase() {
@Inject lateinit var clientProvider: CaldavClientProvider
@Test
fun dontCrashOnSpaceInUrl(): Unit = runBlocking {
clientProvider.forUrl("https://example.com/remote.php/a space/", "username", "password")
}
}

@ -1,220 +0,0 @@
package org.tasks.caldav
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.ETAG
import org.tasks.makers.CaldavTaskMaker.OBJECT
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.newTask
@HiltAndroidTest
class CaldavSynchronizerTest : CaldavTest() {
@Before
override fun setUp() = runBlocking {
super.setUp()
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = "username",
password = encryption.encrypt("password"),
url = server.url("/remote.php/dav/calendars/user1/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
}
}
@Test
fun setMessageOnError() = runBlocking {
enqueue()
synchronizer.sync(account)
assertEquals("HTTP 500 Server Error", caldavDao.getAccounts().first().error)
}
@Test
fun dontFetchCalendarIfCtagMatches() = runBlocking {
caldavDao.insert(
CaldavCalendar(
account = this@CaldavSynchronizerTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@CaldavSynchronizerTest.account.url}test-shared/",
)
)
enqueue(OC_SHARE_PROPFIND)
sync()
}
@Test
fun dontFetchTaskIfEtagMatches() = runBlocking {
val calendar = CaldavCalendar(
account = this@CaldavSynchronizerTest.account.uuid,
uuid = UUIDHelper.newUUID(),
url = "${this@CaldavSynchronizerTest.account.url}test-shared/",
)
caldavDao.insert(calendar)
caldavDao.insert(newCaldavTask(
with(TASK, taskDao.insert(newTask())),
with(OBJECT, "3164728546640386952.ics"),
with(ETAG, "43b3ffaac5131880e4dd07a79adba82a"),
with(CALENDAR, calendar.uuid)
))
enqueue(OC_SHARE_PROPFIND, OC_SHARE_REPORT)
sync()
}
@Test
fun syncNewTask() = runBlocking {
enqueue(OC_SHARE_PROPFIND, OC_SHARE_REPORT, OC_SHARE_TASK)
sync()
val calendar = caldavDao.getCalendars().takeIf { it.size == 1 }!!.first()
val caldavTask = caldavDao.getTaskByRemoteId(calendar.uuid!!, "3164728546640386952")!!
assertEquals("Test task", taskDao.fetch(caldavTask.task)!!.title)
}
companion object {
private val OC_SHARE_PROPFIND = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav"
xmlns:cs="http://calendarserver.org/ns/" xmlns:oc="http://owncloud.org/ns">
<d:response>
<d:href>/remote.php/dav/calendars/user1/test-shared/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection />
<cal:calendar />
</d:resourcetype>
<d:displayname>Test shared</d:displayname>
<cal:supported-calendar-component-set>
<cal:comp name="VTODO" />
</cal:supported-calendar-component-set>
<cs:getctag>http://sabre.io/ns/sync/1</cs:getctag>
<x1:calendar-color xmlns:x1="http://apple.com/ns/ical/">#0082c9</x1:calendar-color>
<d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
<oc:owner-principal>principals/users/user1</oc:owner-principal>
<oc:invite>
<oc:user>
<d:href>principal:principals/users/user2</d:href>
<oc:common-name>user2</oc:common-name>
<oc:invite-accepted />
<oc:access>
<oc:read />
</oc:access>
</oc:user>
</oc:invite>
<d:current-user-privilege-set>
<d:privilege>
<d:write />
</d:privilege>
<d:privilege>
<d:write-properties />
</d:privilege>
<d:privilege>
<d:write-content />
</d:privilege>
<d:privilege>
<d:unlock />
</d:privilege>
<d:privilege>
<d:bind />
</d:privilege>
<d:privilege>
<d:unbind />
</d:privilege>
<d:privilege>
<d:write-acl />
</d:privilege>
<d:privilege>
<d:read />
</d:privilege>
<d:privilege>
<d:read-acl />
</d:privilege>
<d:privilege>
<d:read-current-user-privilege-set />
</d:privilege>
<d:privilege>
<cal:read-free-busy />
</d:privilege>
</d:current-user-privilege-set>
<d:current-user-principal>
<d:href>/remote.php/dav/principals/users/user1/</d:href>
</d:current-user-principal>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<d:share-access />
<d:invite />
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""".trimIndent()
private val OC_SHARE_REPORT = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>/remote.php/dav/calendars/user1/test-shared/3164728546640386952.ics</d:href>
<d:propstat>
<d:prop>
<d:getetag>&quot;43b3ffaac5131880e4dd07a79adba82a&quot;</d:getetag>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""".trimIndent()
private val OC_SHARE_TASK = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav">
<d:response>
<d:href>/remote.php/dav/calendars/user1/test-shared/3164728546640386952.ics</d:href>
<d:propstat>
<d:prop>
<d:getcontenttype>text/calendar; charset=utf-8; component=vtodo</d:getcontenttype>
<d:getetag>&quot;43b3ffaac5131880e4dd07a79adba82a&quot;</d:getetag>
<cal:calendar-data>BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-110500//EN
BEGIN:VTODO
DTSTAMP:20210223T154147Z
UID:3164728546640386952
CREATED:20210223T154134Z
LAST-MODIFIED:20210223T154140Z
SUMMARY:Test task
PRIORITY:9
END:VTODO
END:VCALENDAR</cal:calendar-data>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<cal:schedule-tag />
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""".trimIndent()
}
}

@ -1,68 +0,0 @@
package org.tasks.caldav
import com.todoroo.astrid.dao.TaskDao
import junit.framework.Assert.assertFalse
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.rules.Timeout
import org.tasks.R
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.dao.CaldavDao
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import javax.inject.Inject
abstract class CaldavTest : InjectingTestCase() {
@Inject lateinit var synchronizer: CaldavSynchronizer
@Inject lateinit var encryption: KeyStoreEncryption
@Inject lateinit var preferences: Preferences
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDao: TaskDao
protected val server = MockWebServer()
protected lateinit var account: CaldavAccount
@get:Rule
val globalTimeout: Timeout = Timeout.seconds(30)
@Before
override fun setUp() {
super.setUp()
preferences.setBoolean(R.string.p_debug_pro, true)
server.start()
headers.clear()
}
@After
fun after() = server.shutdown()
protected suspend fun sync(account: CaldavAccount = this.account) {
synchronizer.sync(account)
assertFalse(caldavDao.getAccountByUuid(account.uuid!!)!!.hasError)
}
val headers = HashMap<String, String>()
protected fun enqueue(vararg responses: String) {
responses.forEach {
server.enqueue(
MockResponse()
.setResponseCode(207)
.setHeader("Content-Type", "text/xml; charset=\"utf-8\"")
.apply { this@CaldavTest.headers.forEach { (k, v) -> setHeader(k, v) } }
.setBody(it))
}
server.enqueue(MockResponse().setResponseCode(500))
}
companion object {
init {
CaldavSynchronizer.registerFactories()
}
}
}

@ -1,97 +0,0 @@
package org.tasks.caldav
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.data.UUIDHelper
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_NEXTCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS
@HiltAndroidTest
class ServerDetectionTest : CaldavTest() {
@Test
fun detectTasksServer() = runBlocking {
setup(
"DAV" to SABREDAV_COMPLIANCE,
"x-sabre-version" to "4.1.3",
accountType = TYPE_TASKS
)
sync()
assertEquals(SERVER_TASKS, loadAccount().serverType)
}
@Test
fun detectNextcloudServer() = runBlocking {
setup("DAV" to NEXTCLOUD_COMPLIANCE)
sync()
assertEquals(SERVER_NEXTCLOUD, loadAccount().serverType)
}
@Test
fun detectSabredavServer() = runBlocking {
setup(
"DAV" to SABREDAV_COMPLIANCE,
"x-sabre-version" to "4.1.3"
)
sync()
assertEquals(SERVER_SABREDAV, loadAccount().serverType)
}
@Test
fun detectOpenXchangeServer() = runBlocking {
setup("server" to "Openexchange WebDAV")
sync()
assertEquals(SERVER_OPEN_XCHANGE, loadAccount().serverType)
}
@Test
fun unknownServer() = runBlocking {
setup()
sync()
assertEquals(SERVER_UNKNOWN, loadAccount().serverType)
}
private suspend fun loadAccount(): CaldavAccount =
caldavDao.getAccounts().apply { assertEquals(1, size) }.first()
private suspend fun setup(
vararg headers: Pair<String, String>,
accountType: Int = TYPE_CALDAV
) {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = "username",
password = encryption.encrypt("password"),
url = server.url("/remote.php/dav/calendars/user1/").toString(),
accountType = accountType,
).let {
it.copy(id = caldavDao.insert(it))
}
this.headers.putAll(headers)
enqueue(NO_CALENDARS)
}
companion object {
private const val NO_CALENDARS = """<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"/>"""
private const val SABREDAV_COMPLIANCE = "1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendarserver-subscribed, calendar-auto-schedule, calendar-availability, resource-sharing, calendarserver-sharing"
private const val NEXTCLOUD_COMPLIANCE = "1, 3, extended-mkcol, access-control, calendarserver-principal-property-search, calendar-access, calendar-proxy, calendar-auto-schedule, calendar-availability, nc-calendar-webcal-cache, calendarserver-subscribed, oc-resource-sharing, oc-calendar-publishing, calendarserver-sharing, nc-calendar-search, nc-enable-birthday-calendar"
}
}

@ -1,149 +0,0 @@
package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.dao.PrincipalDao
import javax.inject.Inject
@HiltAndroidTest
class SharingMailboxDotOrgTest : CaldavTest() {
@Inject lateinit var principalDao: PrincipalDao
@Test
fun ownerAccess() = runBlocking {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = "3",
password = encryption.encrypt("password"),
url = server.url("/caldav/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
}
val calendar = CaldavCalendar(
account = this@SharingMailboxDotOrgTest.account.uuid,
ctag = "1614876450015",
url = "${this@SharingMailboxDotOrgTest.account.url}MzM/",
)
caldavDao.insert(calendar)
enqueue(SHARE_OWNER)
sync()
// TODO: mailbox.org uses share-access differently, need to figure out how to set owner
assertEquals(ACCESS_READ_WRITE, caldavDao.getCalendar(calendar.uuid!!)!!.access)
}
@Test
fun principalForSharee() = runBlocking {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = "3",
password = encryption.encrypt("password"),
url = server.url("/caldav/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
}
val calendar = CaldavCalendar(
account = this@SharingMailboxDotOrgTest.account.uuid,
ctag = "1614876450015",
url = "${this@SharingMailboxDotOrgTest.account.url}MzM/",
)
caldavDao.insert(calendar)
enqueue(SHARE_OWNER)
sync()
val principal = principalDao.getAll().first()
assertEquals(calendar.id, principal.list)
assertEquals("/principals/users/5", principal.href)
assertNull(principal.displayName)
assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(CaldavCalendar.ACCESS_UNKNOWN, principal.access.access)
}
companion object {
private val SHARE_OWNER = """
<?xml version="1.0" encoding="UTF-8"?>
<D:multistatus xmlns:APPLE="http://apple.com/ns/ical/" xmlns:CAL="urn:ietf:params:xml:ns:caldav"
xmlns:CS="http://calendarserver.org/ns/" xmlns:D="DAV:">
<D:response>
<D:href>/caldav/MzM/</D:href>
<D:propstat>
<D:prop>
<D:current-user-privilege-set>
<D:privilege>
<D:read-acl />
</D:privilege>
<D:privilege>
<D:read-current-user-privilege-set />
</D:privilege>
<D:privilege>
<D:read />
</D:privilege>
<D:privilege>
<D:write />
</D:privilege>
<D:privilege>
<D:write-content />
</D:privilege>
<D:privilege>
<D:write-properties />
</D:privilege>
<D:privilege>
<D:write-acl />
</D:privilege>
<D:privilege>
<D:bind />
</D:privilege>
<D:privilege>
<D:unbind />
</D:privilege>
</D:current-user-privilege-set>
<D:displayname>Tasks</D:displayname>
<D:current-user-principal>
<D:href>/principals/users/3</D:href>
</D:current-user-principal>
<calendar-color symbolic-color="custom" xmlns="http://apple.com/ns/ical/">
#CEE7FFFF
</calendar-color>
<D:invite>
<D:sharee>
<D:href>/principals/users/5</D:href>
<D:invite-accepted />
<D:share-access>read</D:share-access>
</D:sharee>
</D:invite>
<D:sync-token>1614876450015</D:sync-token>
<D:share-access>shared-owner</D:share-access>
<D:resourcetype>
<D:collection />
<CAL:calendar />
</D:resourcetype>
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav">
<CAL:comp name="VTODO" />
</supported-calendar-component-set>
<getctag xmlns="http://calendarserver.org/ns/">33-1614876450015</getctag>
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
<D:propstat>
<D:prop>
<invite xmlns="http://owncloud.org/ns" />
<owner-principal xmlns="http://owncloud.org/ns" />
</D:prop>
<D:status>HTTP/1.1 404 NOT FOUND</D:status>
</D:propstat>
</D:response>
</D:multistatus>
""".trimIndent()
}
}

@ -1,250 +0,0 @@
package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.dao.PrincipalDao
import javax.inject.Inject
@HiltAndroidTest
class SharingOwncloudTest : CaldavTest() {
@Inject lateinit var principalDao: PrincipalDao
private suspend fun setupAccount(user: String) {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = user,
password = encryption.encrypt("password"),
url = server.url("/remote.php/dav/calendars/$user/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
}
}
@Test
fun calendarOwner() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar(
account = this@SharingOwncloudTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingOwncloudTest.account.url}test-shared/",
)
caldavDao.insert(calendar)
enqueue(OC_OWNER)
sync()
assertEquals(ACCESS_OWNER, caldavDao.getCalendarByUuid(calendar.uuid!!)?.access)
}
@Test
fun readOnly() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar(
account = this@SharingOwncloudTest.account.uuid,
ctag = "http://sabre.io/ns/sync/2",
url = "${this@SharingOwncloudTest.account.url}test-shared_shared_by_user1/",
)
caldavDao.insert(calendar)
enqueue(OC_READ_ONLY)
sync()
assertEquals(ACCESS_READ_ONLY, caldavDao.getCalendarByUuid(calendar.uuid!!)?.access)
}
@Test
fun principalForSharee() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar(
account = this@SharingOwncloudTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingOwncloudTest.account.url}test-shared/",
)
caldavDao.insert(calendar)
enqueue(OC_OWNER)
sync()
val principal = principalDao.getAll()
.apply { assertTrue(size == 1) }
.first()
assertEquals(calendar.id, principal.list)
assertEquals("principal:principals/users/user2", principal.href)
assertEquals("user2", principal.name)
assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(ACCESS_READ_ONLY, principal.access.access)
}
@Test
fun principalForOwner() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar(
account = this@SharingOwncloudTest.account.uuid,
ctag = "http://sabre.io/ns/sync/2",
url = "${this@SharingOwncloudTest.account.url}test-shared_shared_by_user1/",
)
caldavDao.insert(calendar)
enqueue(OC_READ_ONLY)
sync()
val principal = principalDao.getAll()
.apply { assertTrue(size == 1) }
.first()
assertEquals(calendar.id, principal.list)
assertEquals("principals/users/user1", principal.href)
assertEquals(null, principal.displayName)
assertEquals(CaldavCalendar.INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(ACCESS_OWNER, principal.access.access)
}
companion object {
private val OC_OWNER = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav"
xmlns:cs="http://calendarserver.org/ns/" xmlns:oc="http://owncloud.org/ns">
<d:response>
<d:href>/remote.php/dav/calendars/user1/test-shared/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection />
<cal:calendar />
</d:resourcetype>
<d:displayname>Test shared</d:displayname>
<cal:supported-calendar-component-set>
<cal:comp name="VTODO" />
</cal:supported-calendar-component-set>
<cs:getctag>http://sabre.io/ns/sync/1</cs:getctag>
<x1:calendar-color xmlns:x1="http://apple.com/ns/ical/">#0082c9</x1:calendar-color>
<d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
<oc:owner-principal>principals/users/user1</oc:owner-principal>
<oc:invite>
<oc:user>
<d:href>principal:principals/users/user2</d:href>
<oc:common-name>user2</oc:common-name>
<oc:invite-accepted />
<oc:access>
<oc:read />
</oc:access>
</oc:user>
</oc:invite>
<d:current-user-privilege-set>
<d:privilege>
<d:write />
</d:privilege>
<d:privilege>
<d:write-properties />
</d:privilege>
<d:privilege>
<d:write-content />
</d:privilege>
<d:privilege>
<d:unlock />
</d:privilege>
<d:privilege>
<d:bind />
</d:privilege>
<d:privilege>
<d:unbind />
</d:privilege>
<d:privilege>
<d:write-acl />
</d:privilege>
<d:privilege>
<d:read />
</d:privilege>
<d:privilege>
<d:read-acl />
</d:privilege>
<d:privilege>
<d:read-current-user-privilege-set />
</d:privilege>
<d:privilege>
<cal:read-free-busy />
</d:privilege>
</d:current-user-privilege-set>
<d:current-user-principal>
<d:href>/remote.php/dav/principals/users/user1/</d:href>
</d:current-user-principal>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<d:share-access />
<d:invite />
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""".trimIndent()
val OC_READ_ONLY = """
<?xml version="1.0"?>
<d:multistatus xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/"
xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns"
xmlns:oc="http://owncloud.org/ns" xmlns:s="http://sabredav.org/ns">
<d:response>
<d:href>/remote.php/dav/calendars/user2/test-shared_shared_by_user1/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection />
<cal:calendar />
</d:resourcetype>
<d:displayname>Test shared (user1)</d:displayname>
<cal:supported-calendar-component-set>
<cal:comp name="VTODO" />
</cal:supported-calendar-component-set>
<cs:getctag>http://sabre.io/ns/sync/2</cs:getctag>
<x1:calendar-color xmlns:x1="http://apple.com/ns/ical/">#0082c9</x1:calendar-color>
<d:sync-token>http://sabre.io/ns/sync/2</d:sync-token>
<oc:owner-principal>principals/users/user1</oc:owner-principal>
<oc:invite />
<d:current-user-privilege-set>
<d:privilege>
<d:write-properties />
</d:privilege>
<d:privilege>
<d:read />
</d:privilege>
<d:privilege>
<d:read-acl />
</d:privilege>
<d:privilege>
<d:read-current-user-privilege-set />
</d:privilege>
<d:privilege>
<cal:read-free-busy />
</d:privilege>
</d:current-user-privilege-set>
<d:current-user-principal>
<d:href>/remote.php/dav/principals/users/user2/</d:href>
</d:current-user-principal>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<d:share-access />
<d:invite />
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""".trimIndent()
}
}

@ -1,318 +0,0 @@
package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.entity.CaldavCalendar.Companion.INVITE_ACCEPTED
import org.tasks.data.dao.PrincipalDao
import javax.inject.Inject
@HiltAndroidTest
class SharingSabredavTest : CaldavTest() {
@Inject lateinit var principalDao: PrincipalDao
private suspend fun setupAccount(user: String) {
account = CaldavAccount(
uuid = UUIDHelper.newUUID(),
username = user,
password = encryption.encrypt("password"),
url = server.url("/calendars/$user/").toString(),
).let {
it.copy(id = caldavDao.insert(it))
}
}
@Test
fun calendarOwner() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar(
account = this@SharingSabredavTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingSabredavTest.account.url}940468858232147861/",
)
caldavDao.insert(calendar)
enqueue(SD_OWNER)
sync()
assertEquals(
ACCESS_OWNER,
caldavDao.getCalendarByUuid(calendar.uuid!!)?.access
)
}
@Test
fun calendarSharee() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar(
account = this@SharingSabredavTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingSabredavTest.account.url}c3853d69-cb7a-476c-a23b-30ffd70f110b/",
)
caldavDao.insert(calendar)
enqueue(SD_SHAREE)
sync()
assertEquals(
ACCESS_READ_WRITE,
caldavDao.getCalendarByUuid(calendar.uuid!!)?.access
)
}
@Test
fun excludeCurrentUserPrincipalFromSharees() = runBlocking {
setupAccount("user1")
caldavDao.insert(
CaldavCalendar(
account = account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${account.url}940468858232147861/",
)
)
enqueue(SD_OWNER)
sync()
assertEquals(1, principalDao.getAll().size)
}
@Test
fun principalForSharee() = runBlocking {
setupAccount("user1")
val calendar = CaldavCalendar(
account = this@SharingSabredavTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingSabredavTest.account.url}940468858232147861/",
)
caldavDao.insert(calendar)
enqueue(SD_OWNER)
sync()
val principal = principalDao.getAll().first()
assertEquals(calendar.id, principal.list)
assertEquals("mailto:user@example.com", principal.href)
assertEquals("Example User", principal.displayName)
assertEquals(INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(ACCESS_READ_WRITE, principal.access.access)
}
@Test
fun principalForOwner() = runBlocking {
setupAccount("user2")
val calendar = CaldavCalendar(
account = this@SharingSabredavTest.account.uuid,
ctag = "http://sabre.io/ns/sync/1",
url = "${this@SharingSabredavTest.account.url}c3853d69-cb7a-476c-a23b-30ffd70f110b/",
)
caldavDao.insert(calendar)
enqueue(SD_SHAREE)
sync()
val principal = principalDao.getAll()
.apply { assertTrue(size == 1) }
.first()
assertEquals(calendar.id, principal.list)
assertEquals("/principals/user1", principal.href)
assertEquals(null, principal.displayName)
assertEquals(INVITE_ACCEPTED, principal.inviteStatus)
assertEquals(ACCESS_OWNER, principal.access.access)
}
companion object {
private val SD_OWNER = """
<?xml version="1.0"?>
<d:multistatus xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/"
xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
<d:response>
<d:href>/calendars/user1/940468858232147861/</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection />
<cal:calendar />
<cs:shared-owner />
</d:resourcetype>
<d:displayname>Shared</d:displayname>
<cal:supported-calendar-component-set>
<cal:comp name="VTODO" />
</cal:supported-calendar-component-set>
<cs:getctag>http://sabre.io/ns/sync/1</cs:getctag>
<d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
<d:share-access>
<d:shared-owner />
</d:share-access>
<d:invite>
<d:sharee>
<d:href>/principals/user1</d:href>
<d:prop />
<d:share-access>
<d:shared-owner />
</d:share-access>
<d:invite-accepted />
</d:sharee>
<d:sharee>
<d:href>mailto:user@example.com</d:href>
<d:prop>
<d:displayname>Example User</d:displayname>
</d:prop>
<d:share-access>
<d:read-write />
</d:share-access>
<d:invite-accepted />
</d:sharee>
</d:invite>
<d:current-user-privilege-set>
<d:privilege>
<cal:read-free-busy />
</d:privilege>
<d:privilege>
<d:read />
</d:privilege>
<d:privilege>
<d:read-acl />
</d:privilege>
<d:privilege>
<d:read-current-user-privilege-set />
</d:privilege>
<d:privilege>
<d:write-properties />
</d:privilege>
<d:privilege>
<d:write />
</d:privilege>
<d:privilege>
<d:write-content />
</d:privilege>
<d:privilege>
<d:unlock />
</d:privilege>
<d:privilege>
<d:bind />
</d:privilege>
<d:privilege>
<d:unbind />
</d:privilege>
<d:privilege>
<d:write-acl />
</d:privilege>
<d:privilege>
<d:share />
</d:privilege>
</d:current-user-privilege-set>
<d:current-user-principal>
<d:href>/principals/user1/</d:href>
</d:current-user-principal>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<x1:calendar-color xmlns:x1="http://apple.com/ns/ical/" />
<x2:owner-principal xmlns:x2="http://owncloud.org/ns" />
<x2:invite xmlns:x2="http://owncloud.org/ns" />
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""".trimIndent()
private val SD_SHAREE = """
<?xml version="1.0"?>
<d:multistatus xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns/"
xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">
<d:response>
<d:href>/calendars/user2/c3853d69-cb7a-476c-a23b-30ffd70f110b/
</d:href>
<d:propstat>
<d:prop>
<d:resourcetype>
<d:collection />
<cal:calendar />
</d:resourcetype>
<d:displayname>Shared</d:displayname>
<cal:supported-calendar-component-set>
<cal:comp name="VTODO" />
</cal:supported-calendar-component-set>
<cs:getctag>http://sabre.io/ns/sync/1</cs:getctag>
<d:sync-token>http://sabre.io/ns/sync/1</d:sync-token>
<d:share-access>
<d:read-write />
</d:share-access>
<d:invite>
<d:sharee>
<d:href>/principals/user1</d:href>
<d:prop />
<d:share-access>
<d:shared-owner />
</d:share-access>
<d:invite-accepted />
</d:sharee>
</d:invite>
<d:current-user-privilege-set>
<d:privilege>
<cal:read-free-busy />
</d:privilege>
<d:privilege>
<d:read />
</d:privilege>
<d:privilege>
<d:read-acl />
</d:privilege>
<d:privilege>
<d:read-current-user-privilege-set />
</d:privilege>
<d:privilege>
<d:write-properties />
</d:privilege>
<d:privilege>
<d:write />
</d:privilege>
<d:privilege>
<d:write-content />
</d:privilege>
<d:privilege>
<d:unlock />
</d:privilege>
<d:privilege>
<d:bind />
</d:privilege>
<d:privilege>
<d:unbind />
</d:privilege>
<d:privilege>
<d:write-acl />
</d:privilege>
</d:current-user-privilege-set>
<d:current-user-principal>
<d:href>/principals/user2/</d:href>
</d:current-user-principal>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
<d:propstat>
<d:prop>
<x1:calendar-color xmlns:x1="http://apple.com/ns/ical/" />
<x2:owner-principal xmlns:x2="http://owncloud.org/ns" />
<x2:invite xmlns:x2="http://owncloud.org/ns" />
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""".trimIndent()
}
}

@ -1,25 +0,0 @@
package org.tasks.data
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase
import javax.inject.Inject
@HiltAndroidTest
class CaldavDaoExtensionsTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Test
fun getLocalListCreatesAccountIfNeeded() = runBlocking {
withTimeout(5000L) {
assertTrue(caldavDao.getAccounts().isEmpty())
caldavDao.getLocalList()
assertTrue(caldavDao.getAccounts(CaldavAccount.TYPE_LOCAL).isNotEmpty())
}
}
}

@ -1,165 +0,0 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.TaskContainerMaker
import org.tasks.makers.TaskContainerMaker.CREATED
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@HiltAndroidTest
class CaldavDaoShiftTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
private val tasks = ArrayList<TaskContainer>()
@Test
fun basicShiftDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created))
addTask(with(CREATED, created.plusSeconds(1)))
addTask(with(CREATED, created.plusSeconds(2)))
caldavDao.shiftDown("calendar", 0, created.plusSeconds(1).toAppleEpoch())
checkOrder(null, tasks[0])
checkOrder(created.plusSeconds(2), tasks[1])
checkOrder(created.plusSeconds(3), tasks[2])
}
@Test
fun shiftDownOnlyWhenNecessary() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created))
addTask(with(CREATED, created.plusSeconds(1)))
addTask(with(CREATED, created.plusSeconds(3)))
addTask(with(CREATED, created.plusSeconds(4)))
caldavDao.shiftDown("calendar", 0, created.plusSeconds(1).toAppleEpoch())
checkOrder(null, tasks[0])
checkOrder(created.plusSeconds(2), tasks[1])
checkOrder(null, tasks[2])
checkOrder(null, tasks[3])
}
@Test
fun ignoreUnnecessaryShiftDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created))
addTask(with(CREATED, created.plusSeconds(2)))
caldavDao.shiftDown("calendar", 0, created.plusSeconds(1).toAppleEpoch())
checkOrder(null, tasks[0])
checkOrder(null, tasks[1])
}
@Test
fun ignoreOtherCalendarWhenShiftingDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask("calendar1", with(CREATED, created))
addTask("calendar2", with(CREATED, created))
caldavDao.shiftDown("calendar1", 0, created.toAppleEpoch())
checkOrder(created.plusSeconds(1), tasks[0])
checkOrder(null, tasks[1])
}
@Test
fun partialShiftDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created))
addTask(with(CREATED, created.plusSeconds(1)))
addTask(with(CREATED, created.plusSeconds(2)))
addTask(with(CREATED, created.plusSeconds(3)))
addTask(with(CREATED, created.plusSeconds(4)))
caldavDao.shiftDown("calendar", 0, created.toAppleEpoch(), created.plusSeconds(3).toAppleEpoch())
checkOrder(created.plusSeconds(1), tasks[0])
checkOrder(created.plusSeconds(2), tasks[1])
checkOrder(created.plusSeconds(3), tasks[2])
checkOrder(null, tasks[3])
checkOrder(null, tasks[4])
}
@Test
fun ignoreMovedTasksWhenShiftingDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created))
caldavDao.update(caldavDao.getTask(tasks[0].id).apply { this?.deleted =
currentTimeMillis()
}!!)
caldavDao.shiftDown("calendar", 0, created.toAppleEpoch())
assertNull(taskDao.fetch(tasks[0].id)!!.order)
}
@Test
fun ignoreDeletedTasksWhenShiftingDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created))
taskDao.update(taskDao.fetch(tasks[0].id).apply { this?.deletionDate = currentTimeMillis() }!!)
caldavDao.shiftDown("calendar", 0, created.toAppleEpoch())
assertNull(taskDao.fetch(tasks[0].id)!!.order)
}
@Test
fun touchShiftedTasks() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created))
addTask(with(CREATED, created.plusSeconds(1)))
freezeAt(created.plusMinutes(1)) {
caldavDao.shiftDown("calendar", 0, created.toAppleEpoch())
}
assertEquals(created.plusMinutes(1).millis, taskDao.fetch(tasks[0].id)!!.modificationDate)
assertEquals(created.plusMinutes(1).millis, taskDao.fetch(tasks[1].id)!!.modificationDate)
}
private suspend fun checkOrder(dateTime: DateTime?, task: TaskContainer) {
val order = taskDao.fetch(task.id)!!.order
if (dateTime == null) {
assertNull(order)
} else {
assertEquals(dateTime.toAppleEpoch(), order)
}
}
private suspend fun addTask(vararg properties: PropertyValue<in TaskContainer?, *>) = addTask("calendar", *properties)
private suspend fun addTask(calendar: String, vararg properties: PropertyValue<in TaskContainer?, *>) {
val t = TaskContainerMaker.newTaskContainer(*properties)
val task = t.task
taskDao.createNew(task)
val caldavTask = CaldavTask(task = t.id, calendar = calendar)
if (task.parent > 0) {
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent)
}
tasks.add(
t.copy(
caldavTask = caldavTask.copy(
id = caldavDao.insert(caldavTask)
)
)
)
}
}

@ -1,106 +0,0 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
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.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import javax.inject.Inject
@HiltAndroidTest
class CaldavDaoTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var tagDao: TagDao
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var caldavDao: CaldavDao
@Test
fun insertNewTaskAtTopOfEmptyList() = runBlocking {
val task = newTask()
taskDao.createNew(task)
caldavDao.insert(task, CaldavTask(task = task.id, calendar = "calendar"), true)
checkOrder(null, task.id)
}
@Test
fun insertNewTaskAboveExistingTask() = runBlocking {
val created = DateTime(2020, 5, 21, 15, 29, 16, 452)
val first = newTask(with(CREATION_TIME, created))
val second = newTask(with(CREATION_TIME, created.plusSeconds(1)))
taskDao.createNew(first)
taskDao.createNew(second)
caldavDao.insert(first, CaldavTask(task = first.id, calendar = "calendar"), true)
caldavDao.insert(second, CaldavTask(task = second.id, calendar = "calendar"), true)
checkOrder(null, first.id)
checkOrder(created.minusSeconds(1), second.id)
}
@Test
fun insertNewTaskBelowExistingTask() = runBlocking {
val created = DateTime(2020, 5, 21, 15, 29, 16, 452)
val first = newTask(with(CREATION_TIME, created))
val second = newTask(with(CREATION_TIME, created.plusSeconds(1)))
taskDao.createNew(first)
taskDao.createNew(second)
caldavDao.insert(first, CaldavTask(task = first.id, calendar = "calendar"), false)
caldavDao.insert(second, CaldavTask(task = second.id, calendar = "calendar"), false)
checkOrder(null, first.id)
checkOrder(null, second.id)
}
@Test
fun insertNewTaskBelowExistingTaskWithSameCreationDate() = runBlocking {
val created = DateTime(2020, 5, 21, 15, 29, 16, 452)
val first = newTask(with(CREATION_TIME, created))
val second = newTask(with(CREATION_TIME, created))
taskDao.createNew(first)
taskDao.createNew(second)
caldavDao.insert(first, CaldavTask(task = first.id, calendar = "calendar"), false)
caldavDao.insert(second, CaldavTask(task = second.id, calendar = "calendar"), false)
checkOrder(null, first.id)
checkOrder(created.plusSeconds(1), second.id)
}
@Test
fun insertNewTaskAtBottomOfEmptyList() = runBlocking {
val task = newTask()
taskDao.createNew(task)
caldavDao.insert(task, CaldavTask(task = task.id, calendar = "calendar"), false)
checkOrder(null, task.id)
}
@Test
fun noResultsForEmptyAccounts() = runBlocking {
val caldavAccount = CaldavAccount(uuid = UUIDHelper.newUUID())
caldavDao.insert(caldavAccount)
assertTrue(caldavDao.getCaldavFilters(caldavAccount.uuid!!).isEmpty())
}
private suspend fun checkOrder(dateTime: DateTime, task: Long) = checkOrder(dateTime.toAppleEpoch(), task)
private suspend fun checkOrder(order: Long?, task: Long) {
val sortOrder = taskDao.fetch(task)!!.order
if (order == null) {
assertNull(sortOrder)
} else {
assertEquals(order, sortOrder)
}
}
}

@ -1,98 +0,0 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.DeletionDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavTask
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@HiltAndroidTest
class DeletionDaoTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var deletionDao: DeletionDao
@Inject lateinit var caldavDao: CaldavDao
@Test
fun deleting1000DoesntCrash() = runBlocking {
deletionDao.delete((1L..1000L).toList(), {})
}
@Test
fun marking998ForDeletionDoesntCrash() = runBlocking {
deletionDao.markDeleted(1L..1000L, {})
}
@Test
fun markDeletedUpdatesModificationTime() = runBlocking {
var task = newTask(with(CREATION_TIME, DateTime().minusMinutes(1)))
taskDao.createNew(task)
deletionDao.markDeleted(listOf(task.id), {})
task = taskDao.fetch(task.id)!!
assertTrue(task.modificationDate > task.creationDate)
assertTrue(task.modificationDate < currentTimeMillis())
}
@Test
fun markDeletedUpdatesDeletionTime() = runBlocking {
var task = newTask(with(CREATION_TIME, DateTime().minusMinutes(1)))
taskDao.createNew(task)
deletionDao.markDeleted(listOf(task.id), {})
task = taskDao.fetch(task.id)!!
assertTrue(task.deletionDate > task.creationDate)
assertTrue(task.deletionDate < currentTimeMillis())
}
@Test
fun purgeDeletedLocalTask() = runBlocking {
val task = newTask(with(DELETION_TIME, newDateTime()))
taskDao.createNew(task)
caldavDao.insert(CaldavAccount(uuid = "abcd", accountType = CaldavAccount.TYPE_LOCAL))
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = "abcd"))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
deletionDao.purgeDeleted()
assertNull(taskDao.fetch(task.id))
}
@Test
fun dontPurgeActiveTasks() = runBlocking {
val task = newTask()
taskDao.createNew(task)
caldavDao.insert(CaldavAccount(uuid = "abcd", accountType = CaldavAccount.TYPE_LOCAL))
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = "abcd"))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
deletionDao.purgeDeleted()
assertNotNull(taskDao.fetch(task.id))
}
@Test
fun dontPurgeDeletedCaldavTask() = runBlocking {
val task = newTask(with(DELETION_TIME, newDateTime()))
taskDao.createNew(task)
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = UUIDHelper.newUUID()))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
deletionDao.purgeDeleted()
assertNotNull(taskDao.fetch(task.id))
}
}

@ -1,283 +0,0 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.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.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@HiltAndroidTest
class GoogleTaskDaoTests : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
@Before
override fun setUp() {
super.setUp()
runBlocking {
caldavDao.insert(CaldavAccount(uuid = "account", accountType = TYPE_GOOGLE_TASKS))
caldavDao.insert(CaldavCalendar(account = "account", uuid = "calendar"))
}
}
@Test
fun insertAtTopOfEmptyList() = runBlocking {
insertTop(newCaldavTask(with(REMOTE_ID, "1234")))
val tasks = googleTaskDao.getByLocalOrder("calendar")
assertEquals(1, tasks.size.toLong())
val task = tasks[0]
assertEquals("1234", googleTaskDao.getByTaskId(task.id)?.remoteId)
assertEquals(0L, task.order)
}
@Test
fun insertAtBottomOfEmptyList() = runBlocking {
insertBottom(newCaldavTask(with(REMOTE_ID, "1234")))
val tasks = googleTaskDao.getByLocalOrder("calendar")
assertEquals(1, tasks.size.toLong())
val task = tasks[0]
assertEquals("1234", googleTaskDao.getByTaskId(task.id)?.remoteId)
assertEquals(0L, task.order)
}
@Test
fun getPreviousIsNullForTopTask() = runBlocking {
insert(newCaldavTask())
assertNull(googleTaskDao.getPrevious("1", 0, 0))
}
@Test
fun getPrevious() = runBlocking {
insertTop(newCaldavTask())
insertTop(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals("1234", googleTaskDao.getPrevious("calendar", 0, 1))
}
@Test
fun insertAtTopOfList() = runBlocking {
insertTop(newCaldavTask(with(REMOTE_ID, "1234")))
insertTop(newCaldavTask(with(REMOTE_ID, "5678")))
val tasks = googleTaskDao.getByLocalOrder("calendar")
assertEquals(2, tasks.size.toLong())
val top = tasks[0]
assertEquals("5678", googleTaskDao.getByTaskId(top.id)?.remoteId)
assertEquals(0L, top.order)
}
@Test
fun insertAtTopOfListShiftsExisting() = runBlocking {
insertTop(newCaldavTask(with(REMOTE_ID, "1234")))
insertTop(newCaldavTask(with(REMOTE_ID, "5678")))
val tasks = googleTaskDao.getByLocalOrder("calendar")
assertEquals(2, tasks.size.toLong())
val bottom = tasks[1]
assertEquals("1234", googleTaskDao.getByTaskId(bottom.id)?.remoteId)
assertEquals(1L, bottom.order)
}
@Test
fun getTaskFromRemoteId() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals(1L, googleTaskDao.getTask("1234", "calendar"))
}
@Test
fun getRemoteIdForTask() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals("1234", googleTaskDao.getRemoteId(1L, "calendar"))
}
@Test
fun moveDownInList() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1")))
insert(newCaldavTask(with(REMOTE_ID, "2")))
insert(newCaldavTask(with(REMOTE_ID, "3")))
val two = getByRemoteId("2")
googleTaskDao.move(taskDao.fetch(two.task)!!, "calendar", 0, 0)
assertEquals(0L, getOrder("2"))
assertEquals(1L, getOrder("1"))
assertEquals(2L, getOrder("3"))
}
@Test
fun moveUpInList() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1")))
insert(newCaldavTask(with(REMOTE_ID, "2")))
insert(newCaldavTask(with(REMOTE_ID, "3")))
val one = getByRemoteId("1")
googleTaskDao.move(taskDao.fetch(one.task)!!, "calendar", 0, 1)
assertEquals(0L, getOrder("2"))
assertEquals(1L, getOrder("1"))
assertEquals(2L, getOrder("3"))
}
@Test
fun moveToTop() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1")))
insert(newCaldavTask(with(REMOTE_ID, "2")))
insert(newCaldavTask(with(REMOTE_ID, "3")))
val three = getByRemoteId("3")
googleTaskDao.move(taskDao.fetch(three.task)!!, "calendar", 0, 0)
assertEquals(0L, getOrder("3"))
assertEquals(1L, getOrder("1"))
assertEquals(2L, getOrder("2"))
}
@Test
fun moveToBottom() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1")))
insert(newCaldavTask(with(REMOTE_ID, "2")))
insert(newCaldavTask(with(REMOTE_ID, "3")))
val one = getByRemoteId("1")
googleTaskDao.move(taskDao.fetch(one.task)!!, "calendar", 0, 2)
assertEquals(0L, getOrder("2"))
assertEquals(1L, getOrder("3"))
assertEquals(2L, getOrder("1"))
}
@Test
fun dontAllowEmptyParent() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "1234")))
googleTaskDao.updatePosition("1234", "", "0")
assertNull(googleTaskDao.getByTaskId(1)!!.remoteParent)
}
@Test
fun updatePositionWithNullParent() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "1234")))
googleTaskDao.updatePosition("1234", null, "0")
assertNull(googleTaskDao.getByTaskId(1)!!.remoteParent)
}
@Test
fun updatePosition() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "1234")))
googleTaskDao.updatePosition("1234", "abcd", "0")
assertEquals("abcd", googleTaskDao.getByTaskId(1)!!.remoteParent)
}
@Test
fun ignoreSelfParent() = runBlocking {
insert(
newCaldavTask(
with(TASK, 1),
with(REMOTE_ID, "123"),
with(REMOTE_PARENT, "123")
)
)
caldavDao.updateParents()
assertEquals(0, taskDao.fetch(1)!!.parent)
}
@Test
fun updateParents() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_PARENT, "123")))
caldavDao.updateParents()
assertEquals(1, taskDao.fetch(2)!!.parent)
}
@Test
fun updateParentsByList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_PARENT, "123")))
caldavDao.updateParents("calendar")
assertEquals(1, taskDao.fetch(2)!!.parent)
}
@Test
fun updateParentsMustMatchList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "2"), with(REMOTE_PARENT, "123")))
caldavDao.updateParents()
assertEquals(0, taskDao.fetch(2)!!.parent)
}
@Test
fun updateParentsByListMustMatchList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))
insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "2"), with(REMOTE_PARENT, "123")))
caldavDao.updateParents("2")
assertEquals(0, taskDao.fetch(2)!!.parent)
}
@Test
fun ignoreEmptyStringWhenUpdatingParents() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_ID, ""), with(REMOTE_PARENT, "")))
caldavDao.updateParents()
assertEquals(0, taskDao.fetch(2)!!.parent)
}
@Test
fun ignoreEmptyStringWhenUpdatingParentsForList() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "")))
insert(newCaldavTask(with(TASK, 2), with(REMOTE_ID, ""), with(REMOTE_PARENT, "")))
caldavDao.updateParents("1")
assertEquals(0, taskDao.fetch(2)!!.parent)
}
private suspend fun getOrder(remoteId: String): Long? {
return taskDao.fetch(googleTaskDao.getByRemoteId(remoteId, "calendar")!!.task)?.order
}
private suspend fun insertTop(googleTask: CaldavTask) {
insert(googleTask, true)
}
private suspend fun insertBottom(googleTask: CaldavTask) {
insert(googleTask, false)
}
private suspend fun insert(googleTask: CaldavTask, top: Boolean = false) {
val task = newTask()
taskDao.createNew(task)
googleTaskDao.insertAndShift(
task,
googleTask.copy(task = task.id),
top
)
}
private suspend fun getByRemoteId(remoteId: String): CaldavTask {
return googleTaskDao.getByRemoteId(remoteId, "calendar")!!
}
}

@ -1,26 +0,0 @@
package org.tasks.data
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase
import javax.inject.Inject
@HiltAndroidTest
class GoogleTaskListDaoTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Test
fun noResultsForEmptyAccount() = runBlocking {
val account = CaldavAccount(
uuid = "user@gmail.com",
username = "user@gmail.com",
)
caldavDao.insert(account)
assertTrue(caldavDao.getCaldavFilters(account.username!!).isEmpty())
}
}

@ -1,269 +0,0 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.LocationDao
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.Geofence
import org.tasks.data.entity.Place
import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.HIDE_TYPE
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@HiltAndroidTest
class LocationDaoTest : InjectingTestCase() {
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var alarmDao: AlarmDao
@Test
fun getExistingPlace() = runBlocking {
locationDao.insert(Place(latitude = 48.067222, longitude = 12.863611))
val place = locationDao.findPlace(48.067222.toLikeString(), 12.863611.toLikeString())
assertEquals(48.067222, place?.latitude)
assertEquals(12.863611, place?.longitude)
}
@Test
fun getPlaceWithLessPrecision() = runBlocking {
locationDao.insert(Place(latitude = 50.7547, longitude = -2.2279))
val place = locationDao.findPlace(50.754712.toLikeString(), (-2.227945).toLikeString())
assertEquals(50.7547, place?.latitude)
assertEquals(-2.2279, place?.longitude)
}
@Test
fun getPlaceWithMorePrecision() = runBlocking {
locationDao.insert(Place(latitude = 36.246944, longitude = -116.816944))
locationDao.getPlaces().forEach { println(it) }
val place = locationDao.findPlace(36.2469.toLikeString(), (-116.8169).toLikeString())
assertEquals(36.246944, place?.latitude)
assertEquals(-116.816944, place?.longitude)
}
@Test
fun noActiveGeofences() = runBlocking {
val place = Place()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1)))
locationDao.insert(Geofence(task = 1, place = place.uid))
assertNull(locationDao.getGeofencesByPlace(place.uid!!))
}
@Test
fun activeArrivalGeofence() = runBlocking {
val place = Place()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1)))
locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true))
val geofence = locationDao.getGeofencesByPlace(place.uid!!)
assertTrue(geofence!!.arrival)
assertFalse(geofence.departure)
}
@Test
fun activeDepartureGeofence() = runBlocking {
val place = Place()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1)))
locationDao.insert(Geofence(task = 1, place = place.uid, isDeparture = true))
val geofence = locationDao.getGeofencesByPlace(place.uid!!)
assertFalse(geofence!!.arrival)
assertTrue(geofence.departure)
}
@Test
fun geofenceInactiveForCompletedTask() = runBlocking {
val place = Place()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(COMPLETION_TIME, newDateTime())))
locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true))
assertNull(locationDao.getGeofencesByPlace(place.uid!!))
}
@Test
fun geofenceInactiveForDeletedTask() = runBlocking {
val place = Place()
locationDao.insert(place)
taskDao.createNew(newTask(with(ID, 1), with(DELETION_TIME, newDateTime())))
locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true))
assertNull(locationDao.getGeofencesByPlace(place.uid!!))
}
@Test
fun ignoreArrivalForSnoozedTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
locationDao.insert(place)
val task = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = task,
time = newDateTime().plusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
locationDao.insert(Geofence(task = task, place = place.uid, isArrival = true))
assertTrue(locationDao.getArrivalGeofences(place.uid!!, currentTimeMillis()).isEmpty())
}
}
@Test
fun ignoreDepartureForSnoozedTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
locationDao.insert(place)
val task = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = task,
time = newDateTime().plusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
locationDao.insert(Geofence(task = task, place = place.uid, isDeparture = true))
assertTrue(locationDao.getDepartureGeofences(place.uid!!, currentTimeMillis()).isEmpty())
}
}
@Test
fun getArrivalWithElapsedSnooze() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
locationDao.insert(place)
val task = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = task,
time = newDateTime().minusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
val geofence = Geofence(task = task, place = place.uid, isArrival = true)
.let { it.copy(id = locationDao.insert(it)) }
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!,
currentTimeMillis()
))
}
}
@Test
fun getDepartureWithElapsedSnooze() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
locationDao.insert(place)
val task = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = task,
time = newDateTime().minusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
val geofence = Geofence(task = task, place = place.uid, isDeparture = true)
.let { it.copy(id = locationDao.insert(it)) }
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!,
currentTimeMillis()
))
}
}
@Test
fun ignoreArrivalForHiddenTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
locationDao.insert(place)
taskDao.createNew(newTask(
with(ID, 1),
with(DUE_TIME, newDateTime().plusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true))
assertTrue(locationDao.getArrivalGeofences(place.uid!!, currentTimeMillis()).isEmpty())
}
}
@Test
fun ignoreDepartureForHiddenTask() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
locationDao.insert(place)
taskDao.createNew(newTask(
with(ID, 1),
with(DUE_TIME, newDateTime().plusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
locationDao.insert(Geofence(task = 1, place = place.uid, isDeparture = true))
assertTrue(locationDao.getDepartureGeofences(place.uid!!, currentTimeMillis()).isEmpty())
}
}
@Test
fun getArrivalWithElapsedHideUntil() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
locationDao.insert(place)
taskDao.createNew(newTask(
with(ID, 1),
with(DUE_TIME, newDateTime().minusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
val geofence = Geofence(task = 1, place = place.uid, isArrival = true)
.let {
it.copy(id = locationDao.insert(it))
}
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!,
currentTimeMillis()
))
}
}
@Test
fun getDepartureWithElapsedHideUntil() = runBlocking {
freezeAt(currentTimeMillis()).thawAfter {
val place = Place()
locationDao.insert(place)
taskDao.createNew(newTask(
with(ID, 1),
with(DUE_TIME, newDateTime().minusMinutes(15)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
val geofence = Geofence(task = 1, place = place.uid, isDeparture = true)
.let { it.copy(id = locationDao.insert(it)) }
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!,
currentTimeMillis()
))
}
}
}

@ -1,108 +0,0 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.R
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.filters.CaldavFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.ORDER
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.preferences.Preferences
import javax.inject.Inject
@HiltAndroidTest
class ManualGoogleTaskQueryTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
private lateinit var filter: CaldavFilter
@Before
override fun setUp() {
super.setUp()
preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true)
val calendar = CaldavCalendar(uuid = "1234")
runBlocking {
caldavDao.insert(CaldavAccount())
caldavDao.insert(calendar)
}
filter = CaldavFilter(calendar, account = CaldavAccount(accountType = TYPE_GOOGLE_TASKS))
}
@Test
fun setIndentOnSubtask() = runBlocking {
newTask(1, 0, 0)
newTask(2, 0, 1)
val subtask = query()[1]
assertEquals(1, subtask.indent)
}
@Test
fun setParentOnSubtask() = runBlocking {
newTask(2, 0, 0)
newTask(1, 0, 2)
val subtask = query()[1]
assertEquals(2, subtask.parent)
}
@Test
fun querySetsPrimarySort() = runBlocking {
newTask(1, 0, 0)
newTask(2, 1, 0)
newTask(3, 0, 2)
val subtasks = query()
assertEquals(0, subtasks[0].primarySort)
assertEquals(1, subtasks[1].primarySort)
assertEquals(1, subtasks[2].primarySort)
}
@Test
fun querySetsSecondarySortOnSubtasks() = runBlocking {
newTask(1, 0, 0)
newTask(2, 0, 1)
newTask(3, 1, 1)
val subtasks = query()
assertEquals(0, subtasks[0].secondarySort)
assertEquals(0, subtasks[1].secondarySort)
assertEquals(1, subtasks[2].secondarySort)
}
private suspend fun newTask(id: Long, order: Long, parent: Long = 0) {
taskDao.insert(TaskMaker.newTask(
with(ID, id),
with(TaskMaker.UUID, UUIDHelper.newUUID()),
with(ORDER, order),
with(PARENT, parent),
))
googleTaskDao.insert(newCaldavTask(with(CALENDAR, filter.uuid), with(TASK, id)))
}
private suspend fun query(): List<TaskContainer> = taskDao.fetchTasks(
TaskListQuery.getQuery(preferences, filter)
)
}

@ -1,133 +0,0 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.TagDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@HiltAndroidTest
class TagDataDaoTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var tagDao: TagDao
@Inject lateinit var tagDataDao: TagDataDao
@Test
fun tagDataOrderedByNameIgnoresNullNames() = runBlocking {
tagDataDao.insert(TagData(name = null))
assertTrue(tagDataDao.tagDataOrderedByName().isEmpty())
}
@Test
fun tagDataOrderedByNameIgnoresEmptyNames() = runBlocking {
tagDataDao.insert(TagData(name = ""))
assertTrue(tagDataDao.tagDataOrderedByName().isEmpty())
}
@Test
fun getTagWithCaseForMissingTag() = runBlocking {
assertEquals("derp", tagDataDao.getTagWithCase("derp"))
}
@Test
fun getTagWithCaseFixesCase() = runBlocking {
tagDataDao.insert(TagData(name = "Derp"))
assertEquals("Derp", tagDataDao.getTagWithCase("derp"))
}
@Test
fun getTagsByName() = runBlocking {
val tagData = TagData(name = "Derp").let { it.copy(id = tagDataDao.insert(it)) }
assertEquals(listOf(tagData), tagDataDao.getTags(listOf("Derp")))
}
@Test
fun getTagsByNameCaseSensitive() = runBlocking {
tagDataDao.insert(TagData(name = "Derp"))
assertTrue(tagDataDao.getTags(listOf("derp")).isEmpty())
}
@Test
fun getTagDataForTask() = runBlocking {
val taskOne = newTask()
val taskTwo = newTask()
taskDao.createNew(taskOne)
taskDao.createNew(taskTwo)
val tagOne = TagData(name = "one").let { it.copy(id = tagDataDao.insert(it)) }
val tagTwo = TagData(name = "two").let { it.copy(id = tagDataDao.insert(it)) }
tagDao.insert(Tag(task = taskOne.id, taskUid = taskOne.uuid, tagUid = tagOne.remoteId))
tagDao.insert(Tag(task = taskTwo.id, taskUid = taskTwo.uuid, tagUid = tagTwo.remoteId))
assertEquals(listOf(tagOne), tagDataDao.getTagDataForTask(taskOne.id))
}
@Test
fun getEmptyTagSelections() = runBlocking {
val selections = tagDataDao.getTagSelections(listOf(1L))
assertTrue(selections.first.isEmpty())
assertTrue(selections.second.isEmpty())
}
@Test
fun getPartialTagSelections() = runBlocking {
newTag(1, "tag1", "tag2")
newTag(2, "tag2", "tag3")
assertEquals(
setOf("tag1", "tag3"), tagDataDao.getTagSelections(listOf(1L, 2L)).first)
}
@Test
fun getEmptyPartialSelections() = runBlocking {
newTag(1, "tag1")
newTag(2, "tag1")
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).first.isEmpty())
}
@Test
fun getCommonTagSelections() = runBlocking {
newTag(1, "tag1", "tag2")
newTag(2, "tag2", "tag3")
assertEquals(setOf("tag2"), tagDataDao.getTagSelections(listOf(1L, 2L)).second)
}
@Test
fun getEmptyCommonSelections() = runBlocking {
newTag(1, "tag1")
newTag(2, "tag2")
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).second.isEmpty())
}
@Test
fun getSelectionsWithNoTags() = runBlocking {
newTag(1)
val selections = tagDataDao.getTagSelections(listOf(1L))
assertTrue(selections.first.isEmpty())
assertTrue(selections.second.isEmpty())
}
@Test
fun noCommonSelectionsWhenOneTaskHasNoTags() = runBlocking {
newTag(1, "tag1")
newTag(2)
val selections = tagDataDao.getTagSelections(listOf(1L, 2L))
assertEquals(setOf("tag1"), selections.first)
assertTrue(selections.second.isEmpty())
}
private suspend fun newTag(taskId: Long, vararg tags: String) {
val task = newTask(with(ID, taskId))
taskDao.createNew(task)
for (tag in tags) {
tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = tag))
}
}
}

@ -1,133 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@HiltAndroidTest
class TaskDaoTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter
/** Test various task fetch conditions */
@Test
fun testTaskConditions() = runBlocking {
// create normal task
var task = Task()
task.title = "normal"
taskDao.createNew(task)
// create blank task
task = Task()
task.title = ""
taskDao.createNew(task)
// create hidden task
task = Task()
task.title = "hidden"
task.hideUntil = currentTimeMillis() + 10000
taskDao.createNew(task)
// create task with deadlines
task = Task()
task.title = "deadlineInFuture"
task.dueDate = currentTimeMillis() + 10000
taskDao.createNew(task)
task = Task()
task.title = "deadlineInPast"
task.dueDate = currentTimeMillis() - 10000
taskDao.createNew(task)
// create completed task
task = Task()
task.title = "completed"
task.completionDate = currentTimeMillis() - 10000
taskDao.createNew(task)
// check is active
assertEquals(5, taskDao.getActiveTasks().size)
// check is visible
assertEquals(5, taskDao.getActiveTasks().size)
}
/** Test task deletion */
@Test
fun testTDeletion() = runBlocking {
assertEquals(0, taskDao.getAll().size)
// create task "happy"
val task = Task()
task.title = "happy"
taskDao.createNew(task)
assertEquals(1, taskDao.getAll().size)
// delete
taskDeleter.delete(task)
assertEquals(0, taskDao.getAll().size)
}
/** Test passing invalid task indices to various things */
@Test
fun testInvalidIndex() = runBlocking {
assertEquals(0, taskDao.getAll().size)
assertNull(taskDao.fetch(1))
taskDeleter.delete(listOf(1L))
// make sure db still works
assertEquals(0, taskDao.getAll().size)
}
@Test
fun findChildrenInList() = runBlocking {
val parent = taskDao.createNew(newTask())
val child = taskDao.createNew(newTask(with(PARENT, parent)))
assertEquals(listOf(child), taskDao.getChildren(listOf(parent, child)))
}
@Test
fun findRecursiveChildrenInList() = runBlocking {
val parent = taskDao.createNew(newTask())
val child = taskDao.createNew(newTask(with(PARENT, parent)))
val grandchild = taskDao.createNew(newTask(with(PARENT, child)))
assertEquals(
listOf(child, grandchild, grandchild),
taskDao.getChildren(listOf(parent, child, grandchild)))
}
@Test
fun findRecursiveChildrenInListAfterSkippingParent() = runBlocking {
val parent = taskDao.createNew(newTask())
val child = taskDao.createNew(newTask(with(PARENT, parent)))
val grandchild = taskDao.createNew(newTask(with(PARENT, child)))
assertEquals(listOf(child, grandchild), taskDao.getChildren(listOf(parent, grandchild)))
}
@Test
fun dontSetParentToSelf() = runBlocking {
val parent = taskDao.createNew(newTask())
val child = taskDao.createNew(newTask())
taskDao.setParent(parent, listOf(parent, child))
assertEquals(0, taskDao.fetch(parent)!!.parent)
assertEquals(parent, taskDao.fetch(child)!!.parent)
}
}

@ -1,62 +0,0 @@
package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
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.dao.UpgraderDao
import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData
import org.tasks.injection.InjectingTestCase
import org.tasks.makers.TaskMaker
import javax.inject.Inject
@HiltAndroidTest
class UpgraderDaoTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var tagDao: TagDao
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var upgraderDao: UpgraderDao
@Test
fun getCaldavTasksWithTags() = runBlocking {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.ID, 1L))
taskDao.createNew(task)
val one = TagData()
val two = TagData()
tagDataDao.insert(one)
tagDataDao.insert(two)
tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = one.remoteId))
tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = two.remoteId))
caldavDao.insert(CaldavTask(task = task.id, calendar = "calendar"))
assertEquals(listOf(task.id), upgraderDao.tasksWithTags())
}
@Test
fun ignoreNonCaldavTaskWithTags() = runBlocking {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.ID, 1L))
taskDao.createNew(task)
val tag = TagData()
tagDataDao.insert(tag)
tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = tag.remoteId))
assertTrue(upgraderDao.tasksWithTags().isEmpty())
}
@Test
fun ignoreCaldavTaskWithoutTags() = runBlocking {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.ID, 1L))
taskDao.createNew(task)
tagDataDao.insert(TagData())
caldavDao.insert(CaldavTask(task = task.id, calendar = "calendar"))
assertTrue(upgraderDao.tasksWithTags().isEmpty())
}
}

@ -1,121 +0,0 @@
package org.tasks.gtasks
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.entity.Task
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
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.newTask
import org.tasks.time.DateTime
@RunWith(AndroidJUnit4::class)
class GoogleTaskSynchronizerTest {
@Test
fun testMergeDate() {
val local = newTask(with(DUE_DATE, DateTime(2016, 3, 12)))
GoogleTaskSynchronizer.mergeDates(newTask(with(DUE_DATE, DateTime(2016, 3, 11))).dueDate, local)
assertEquals(DateTime(2016, 3, 11, 12, 0).millis, local.dueDate)
}
@Test
fun testMergeTime() {
val local = newTask(with(DUE_TIME, DateTime(2016, 3, 11, 13, 30)))
GoogleTaskSynchronizer.mergeDates(newTask(with(DUE_DATE, DateTime(2016, 3, 11))).dueDate, local)
assertEquals(DateTime(2016, 3, 11, 13, 30, 1).millis, local.dueDate)
}
@Test
fun testDueDateAdjustHideBackwards() {
val local = newTask(with(DUE_DATE, DateTime(2016, 3, 12)), with(HIDE_TYPE, Task.HIDE_UNTIL_DUE))
GoogleTaskSynchronizer.mergeDates(newTask(with(DUE_DATE, DateTime(2016, 3, 11))).dueDate, local)
assertEquals(DateTime(2016, 3, 11).millis, local.hideUntil)
}
@Test
fun testDueDateAdjustHideForwards() {
val local = newTask(with(DUE_DATE, DateTime(2016, 3, 12)), with(HIDE_TYPE, Task.HIDE_UNTIL_DUE))
GoogleTaskSynchronizer.mergeDates(newTask(with(DUE_DATE, DateTime(2016, 3, 14))).dueDate, local)
assertEquals(DateTime(2016, 3, 14).millis, local.hideUntil)
}
@Test
fun testDueTimeAdjustHideBackwards() {
val local = newTask(
with(DUE_TIME, DateTime(2016, 3, 12, 13, 30)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME))
GoogleTaskSynchronizer.mergeDates(newTask(with(DUE_DATE, DateTime(2016, 3, 11))).dueDate, local)
assertEquals(
DateTime(2016, 3, 11, 13, 30, 1).millis, local.hideUntil)
}
@Test
fun testDueTimeAdjustTimeForwards() {
val local = newTask(
with(DUE_TIME, DateTime(2016, 3, 12, 13, 30)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME))
GoogleTaskSynchronizer.mergeDates(newTask(with(DUE_DATE, DateTime(2016, 3, 14))).dueDate, local)
assertEquals(
DateTime(2016, 3, 14, 13, 30, 1).millis, local.hideUntil)
}
@Test
fun testDueDateClearHide() {
val local = newTask(with(DUE_DATE, DateTime(2016, 3, 12)), with(HIDE_TYPE, Task.HIDE_UNTIL_DUE))
GoogleTaskSynchronizer.mergeDates(newTask().dueDate, local)
assertEquals(0L, local.hideUntil)
}
@Test
fun testDueTimeClearHide() {
val local = newTask(
with(DUE_TIME, DateTime(2016, 3, 12, 13, 30)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME))
GoogleTaskSynchronizer.mergeDates(newTask().dueDate, local)
assertEquals(0L, local.hideUntil)
}
@Test
fun truncateValue() {
assertEquals("1234567", GoogleTaskSynchronizer.truncate("12345678", 7))
}
@Test
fun dontTruncateMax() {
assertEquals("1234567", GoogleTaskSynchronizer.truncate("1234567", 7))
}
@Test
fun dontTruncateShortValue() {
assertEquals("12345", GoogleTaskSynchronizer.truncate("12345", 7))
}
@Test
fun dontTruncateNull() {
assertNull(GoogleTaskSynchronizer.truncate(null, 7))
}
@Test
fun dontOverwriteTruncatedValue() {
assertEquals("123456789", GoogleTaskSynchronizer.getTruncatedValue("123456789", "1234567", 7))
}
@Test
fun overwriteTruncatedValueWithShortenedValue() {
assertEquals("12345", GoogleTaskSynchronizer.getTruncatedValue("123456789", "12345", 7))
}
@Test
fun overwriteTruncatedValueWithNullValue() {
assertNull(GoogleTaskSynchronizer.getTruncatedValue("123456789", null, 7))
}
@Test
fun overwriteNullValueWithTruncatedValue() {
assertEquals("1234567", GoogleTaskSynchronizer.getTruncatedValue(null, "1234567", 7))
}
}

@ -1,24 +0,0 @@
package org.tasks.injection
import android.content.Context
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.testing.HiltAndroidRule
import org.junit.Before
import org.junit.Rule
abstract class InjectingTestCase {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Before
open fun setUp() {
hiltRule.inject()
}
protected fun runOnMainSync(runnable: Runnable) =
InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable)
protected val context: Context
get() = getApplicationContext()
}

@ -1,55 +0,0 @@
package org.tasks.injection
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import org.mockito.Mockito.mock
import org.tasks.TestUtilities
import org.tasks.data.db.Database
import org.tasks.jobs.WorkManager
import org.tasks.location.LocationManager
import org.tasks.location.MockLocationManager
import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.PermissivePermissionChecker
import org.tasks.preferences.Preferences
import javax.inject.Singleton
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [ProductionModule::class]
)
class TestModule {
@Provides
@Singleton
fun getDatabase(@ApplicationContext context: Context): Database =
Room
.inMemoryDatabaseBuilder(context, Database::class.java)
.fallbackToDestructiveMigration(dropAllTables = true)
.setDriver()
.build()
@Provides
fun getPermissionChecker(@ApplicationContext context: Context): PermissionChecker {
return PermissivePermissionChecker(context)
}
@Provides
fun getPreferences(@ApplicationContext context: Context): Preferences {
return TestUtilities.newPreferences(context)
}
@Provides
@Singleton
fun getMockLocationManager(): MockLocationManager = MockLocationManager()
@Provides
fun getLocationManager(locationManager: MockLocationManager): LocationManager = locationManager
@Provides
fun getWorkManager(): WorkManager = mock(WorkManager::class.java)
}

@ -1,80 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package org.tasks.jobs
import android.net.Uri
import androidx.test.InstrumentationRegistry
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.R
import org.tasks.backup.BackupConstants.BACKUP_CLEANUP_MATCHER
import org.tasks.backup.TasksJsonExporter
import org.tasks.backup.TasksJsonExporter.ExportType
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences
import java.io.File
import java.io.IOException
import javax.inject.Inject
@HiltAndroidTest
class BackupServiceTests : InjectingTestCase() {
@Inject lateinit var jsonExporter: TasksJsonExporter
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
private lateinit var temporaryDirectory: File
@Before
override fun setUp() {
runBlocking {
super.setUp()
temporaryDirectory = try {
File.createTempFile("backup", System.nanoTime().toString())
} catch (e: IOException) {
throw RuntimeException(e)
}
if (!temporaryDirectory.delete()) {
throw RuntimeException(
"Could not delete temp file: " + temporaryDirectory.absolutePath)
}
if (!temporaryDirectory.mkdir()) {
throw RuntimeException(
"Could not create temp directory: " + temporaryDirectory.absolutePath)
}
preferences.setUri(R.string.p_backup_dir, Uri.fromFile(temporaryDirectory))
// make a temporary task
val task = Task()
task.title = "helicopter"
taskDao.createNew(task)
}
}
@After
fun tearDown() {
for (file in temporaryDirectory.listFiles()!!) {
file.delete()
}
temporaryDirectory.delete()
}
@Test
fun testBackup() = runBlocking {
assertEquals(0, temporaryDirectory.list()!!.size)
jsonExporter.exportTasks(InstrumentationRegistry.getTargetContext(), ExportType.EXPORT_TYPE_SERVICE, null)
// assert file created
val files = temporaryDirectory.listFiles()
assertEquals(1, files!!.size)
assertTrue(files[0].name.matches(BACKUP_CLEANUP_MATCHER))
}
}

@ -1,73 +0,0 @@
package org.tasks.location
import android.location.Location
import android.location.LocationManager.GPS_PROVIDER
import android.location.LocationManager.NETWORK_PROVIDER
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTime
import javax.inject.Inject
@HiltAndroidTest
class LocationServiceAndroidTest : InjectingTestCase() {
@Inject lateinit var service: LocationServiceAndroid
@Inject lateinit var locationManager: MockLocationManager
@Test
fun sortByAccuracy() = runBlocking {
newLocation(NETWORK_PROVIDER, 45.0, 46.0, 50f, DateTime(2021, 2, 4, 13, 35, 45, 121))
newLocation(GPS_PROVIDER, 45.1, 46.1, 30f, DateTime(2021, 2, 4, 13, 33, 45, 121))
assertEquals(MapPosition(45.1, 46.1), service.currentLocation())
}
@Test
fun sortWithStaleLocation() = runBlocking {
newLocation(GPS_PROVIDER, 45.1, 46.1, 30f, DateTime(2021, 2, 4, 13, 33, 44, 121))
newLocation(NETWORK_PROVIDER, 45.0, 46.0, 50f, DateTime(2021, 2, 4, 13, 35, 45, 121))
assertEquals(MapPosition(45.0, 46.0), service.currentLocation())
}
@Test
fun useNewerUpdateWhenAccuracySame() = runBlocking {
newLocation(GPS_PROVIDER, 45.1, 46.1, 50f, DateTime(2021, 2, 4, 13, 35, 45, 100))
newLocation(NETWORK_PROVIDER, 45.0, 46.0, 50f, DateTime(2021, 2, 4, 13, 35, 45, 121))
assertEquals(MapPosition(45.0, 46.0), service.currentLocation())
}
@Test
fun returnCachedLocation() = runBlocking {
newLocation(GPS_PROVIDER, 45.1, 46.1, 50f, DateTime(2021, 2, 4, 13, 35, 45, 100))
service.currentLocation()
locationManager.clearLocations()
assertEquals(MapPosition(45.1, 46.1), service.currentLocation())
}
@Test
fun nullWhenNoPosition() = runBlocking {
assertNull(service.currentLocation())
}
private fun newLocation(
provider: String,
latitude: Double,
longitude: Double,
accuracy: Float,
time: DateTime) {
locationManager.addLocations(Location(provider).apply {
this.latitude = latitude
this.longitude = longitude
this.accuracy = accuracy
this.time = time.millis
})
}
}

@ -1,26 +0,0 @@
package org.tasks.location
import android.app.PendingIntent
import android.location.Location
class MockLocationManager : LocationManager {
private val mockLocations = ArrayList<Location>()
fun addLocations(vararg locations: Location) {
mockLocations.addAll(locations)
}
fun clearLocations() = mockLocations.clear()
override val lastKnownLocations: List<Location>
get() = mockLocations
override fun addProximityAlert(
latitude: Double,
longitude: Double,
radius: Float,
intent: PendingIntent
) {}
override fun removeProximityAlert(intent: PendingIntent) {}
}

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

@ -1,237 +0,0 @@
package org.tasks.opentasks
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.TestUtilities.withTZ
import org.tasks.makers.CaldavTaskMaker
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import java.util.*
@HiltAndroidTest
class OpenTasksDueDateTests : OpenTasksTest() {
@Test
fun readDueDatePositiveOffset() = runBlocking {
val (_, list) = withVtodo(ALL_DAY_DUE)
withTZ(BERLIN) {
synchronizer.sync()
}
val caldavTask = caldavDao.getTaskByRemoteId(list.uuid!!, "3863299529704302692")
val task = taskDao.fetch(caldavTask!!.task)
assertEquals(
DateTime(2021, 2, 1, 12, 0, 0, 0, BERLIN).millis,
task?.dueDate
)
}
@Test
fun writeDueDatePositiveOffset() = withTZ(BERLIN) {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask(
with(TaskMaker.DUE_DATE, DateTime(2021, 2, 1))
))
caldavDao.insert(newCaldavTask(
with(CaldavTaskMaker.CALENDAR, list.uuid),
with(CaldavTaskMaker.REMOTE_ID, "1234"),
with(CaldavTaskMaker.TASK, taskId)
))
synchronizer.sync()
assertEquals(
1612137600000,
openTaskDao.getTask(listId, "1234")?.task?.due?.date?.time
)
}
@Test
fun readDueDateNoOffset() = runBlocking {
val (_, list) = withVtodo(ALL_DAY_DUE)
withTZ(LONDON) {
synchronizer.sync()
}
val caldavTask = caldavDao.getTaskByRemoteId(list.uuid!!, "3863299529704302692")
val task = taskDao.fetch(caldavTask!!.task)
assertEquals(
DateTime(2021, 2, 1, 12, 0, 0, 0, LONDON).millis,
task?.dueDate
)
}
@Test
fun writeDueDateNoOffset() = withTZ(LONDON) {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask(
with(TaskMaker.DUE_DATE, DateTime(2021, 2, 1))
))
caldavDao.insert(newCaldavTask(
with(CaldavTaskMaker.CALENDAR, list.uuid),
with(CaldavTaskMaker.REMOTE_ID, "1234"),
with(CaldavTaskMaker.TASK, taskId)
))
synchronizer.sync()
assertEquals(
1612137600000,
openTaskDao.getTask(listId, "1234")?.task?.due?.date?.time
)
}
@Test
fun readDueDateNegativeOffset() = runBlocking {
val (_, list) = withVtodo(ALL_DAY_DUE)
withTZ(NEW_YORK) {
synchronizer.sync()
}
val caldavTask = caldavDao.getTaskByRemoteId(list.uuid!!, "3863299529704302692")
val task = taskDao.fetch(caldavTask!!.task)
assertEquals(
DateTime(2021, 2, 1, 12, 0, 0, 0, NEW_YORK).millis,
task?.dueDate
)
}
@Test
fun writeDueDateNegativeOffset() = withTZ(NEW_YORK) {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask(
with(TaskMaker.DUE_DATE, DateTime(2021, 2, 1))
))
caldavDao.insert(newCaldavTask(
with(CaldavTaskMaker.CALENDAR, list.uuid),
with(CaldavTaskMaker.REMOTE_ID, "1234"),
with(CaldavTaskMaker.TASK, taskId)
))
synchronizer.sync()
assertEquals(
1612137600000,
openTaskDao.getTask(listId, "1234")?.task?.due?.date?.time
)
}
@Test
fun pushStartTimeBeforeDueTime() = withTZ(CHICAGO) {
val (listId, list) = openTaskDao.insertList()
val task = newTask(
with(TaskMaker.HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME),
with(TaskMaker.DUE_TIME, DateTime(2021, 2, 1, 16, 0))
)
taskDao.createNew(task)
caldavDao.insert(newCaldavTask(
with(CaldavTaskMaker.CALENDAR, list.uuid),
with(CaldavTaskMaker.REMOTE_ID, "1234"),
with(CaldavTaskMaker.TASK, task.id)
))
synchronizer.sync()
assertEquals(
1612216800000,
openTaskDao.getTask(listId, "1234")?.task?.dtStart?.date?.time
)
assertEquals(
1612216801000,
openTaskDao.getTask(listId, "1234")?.task?.due?.date?.time
)
}
@Test
fun startTimeEqualDueTime() = runBlocking {
val (_, list) = withVtodo(START_TIME_DUE_TIME)
withTZ(CHICAGO) {
synchronizer.sync()
}
val caldavTask = caldavDao.getTaskByRemoteId(list.uuid!!, "2009955511573185442")
val task = taskDao.fetch(caldavTask!!.task)!!
assertEquals(DateTime(2021, 2, 4, 8, 0, 1, 0, CHICAGO).millis, task.dueDate)
assertEquals(task.dueDate, task.hideUntil)
}
@Test
fun startTimeEqualDueTimeNoOffset() = runBlocking {
val (_, list) = withVtodo(START_TIME_DUE_TIME_NO_OFFSET)
withTZ(CHICAGO) {
synchronizer.sync()
}
val caldavTask = caldavDao.getTaskByRemoteId(list.uuid!!, "2009955511573185442")
val task = taskDao.fetch(caldavTask!!.task)!!
assertEquals(DateTime(2021, 2, 4, 8, 0, 1, 0, CHICAGO).millis, task.dueDate)
assertEquals(task.dueDate, task.hideUntil)
}
companion object {
private val BERLIN = TimeZone.getTimeZone("Europe/Berlin")
private val LONDON = TimeZone.getTimeZone("Europe/London")
private val NEW_YORK = TimeZone.getTimeZone("America/New_York")
private val CHICAGO = TimeZone.getTimeZone("America/Chicago")
private val ALL_DAY_DUE = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-110304//EN
BEGIN:VTODO
DTSTAMP:20210129T155402Z
UID:3863299529704302692
CREATED:20210129T155318Z
LAST-MODIFIED:20210129T155329Z
SUMMARY:Due date
DUE;VALUE=DATE:20210201
END:VTODO
END:VCALENDAR
""".trimIndent()
private val START_TIME_DUE_TIME = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-110305//EN
BEGIN:VTODO
DTSTAMP:20210203T164753Z
UID:2009955511573185442
CREATED:20210203T164728Z
LAST-MODIFIED:20210203T164750Z
SUMMARY:Start time
X-APPLE-SORT-ORDER:-5
DUE;TZID=America/Chicago:20210204T080001
DTSTART;TZID=America/Chicago:20210204T080000
END:VTODO
END:VCALENDAR
""".trimIndent()
private val START_TIME_DUE_TIME_NO_OFFSET = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-110305//EN
BEGIN:VTODO
DTSTAMP:20210203T164753Z
UID:2009955511573185442
CREATED:20210203T164728Z
LAST-MODIFIED:20210203T164750Z
SUMMARY:Start time
X-APPLE-SORT-ORDER:-5
DUE;TZID=America/Chicago:20210204T080000
DTSTART;TZID=America/Chicago:20210204T080000
END:VTODO
END:VCALENDAR
""".trimIndent()
}
}

@ -1,389 +0,0 @@
package org.tasks.opentasks
import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.TestUtilities.withTZ
import org.tasks.caldav.iCalendar.Companion.collapsed
import org.tasks.caldav.iCalendar.Companion.order
import org.tasks.caldav.iCalendar.Companion.parent
import org.tasks.caldav.iCalendar.Companion.snooze
import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.TagDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData
import org.tasks.data.entity.Task
import org.tasks.makers.CaldavTaskMaker
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.COLLAPSED
import org.tasks.makers.TaskMaker.ORDER
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import java.util.TimeZone
import javax.inject.Inject
@HiltAndroidTest
class OpenTasksPropertiesTests : OpenTasksTest() {
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var tagDao: TagDao
@Inject lateinit var alarmDao: AlarmDao
@Test
fun loadRemoteParentInfo() = runBlocking {
val (_, list) = withVtodo(SUBTASK)
synchronizer.sync()
val task = caldavDao.getTaskByRemoteId(list.uuid!!, "dfede1b0-435b-4bba-9708-2422e781747c")
assertEquals("7daa4a5c-cc76-4ddf-b4f8-b9d3a9cb00e7", task?.remoteParent)
}
@Test
fun pushParentInfo() = runBlocking {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask(with(TaskMaker.PARENT, 594)))
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(CaldavTaskMaker.TASK, taskId),
with(REMOTE_ID, "abcd"),
with(CaldavTaskMaker.REMOTE_PARENT, "1234")
))
synchronizer.sync()
assertEquals("1234", openTaskDao.getTask(listId, "abcd")?.task?.parent)
}
@Test
fun createNewTags() = runBlocking {
val (_, list) = withVtodo(TWO_TAGS)
synchronizer.sync()
assertEquals(
setOf("Tag1", "Tag2"),
caldavDao.getTaskByRemoteId(list.uuid!!, "3076145036806467726")
?.task
?.let { tagDao.getTagsForTask(it) }
?.map { it.name }
?.toSet()
)
}
@Test
fun matchExistingTag() = runBlocking {
val (_, list) = withVtodo(ONE_TAG)
val tag = TagData(name = "Tag1").let { it.copy(id = tagDataDao.insert(it)) }
synchronizer.sync()
assertEquals(
listOf(tag),
caldavDao.getTaskByRemoteId(list.uuid!!, "3076145036806467726")
?.task
?.let { tagDataDao.getTagDataForTask(it)}
)
}
@Test
fun uploadTags() = runBlocking {
val (listId, list) = openTaskDao.insertList()
val task = newTask().apply { taskDao.createNew(this) }
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(REMOTE_ID, "1234"),
with(CaldavTaskMaker.TASK, task.id)
))
insertTag(task, "Tag1")
insertTag(task, "Tag2")
synchronizer.sync()
assertEquals(
setOf("Tag1", "Tag2"),
openTaskDao.getTask(listId, "1234")?.task?.categories?.toSet()
)
}
@Test
fun loadOrder() = runBlocking {
val (_, list) = withVtodo(ONE_TAG)
synchronizer.sync()
val task = caldavDao.getTaskByRemoteId(list.uuid!!, "3076145036806467726")!!.task
assertEquals(633734058L, taskDao.fetch(task)?.order)
}
@Test
fun pushOrder() = runBlocking {
val (listId, list) = openTaskDao.insertList()
val task = newTask(with(ORDER, 5678L))
taskDao.createNew(task)
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(REMOTE_ID, "1234"),
with(CaldavTaskMaker.TASK, task.id)
))
synchronizer.sync()
assertEquals(
5678L,
openTaskDao.getTask(listId, "1234")?.task?.order
)
}
@Test
fun readCollapsedState() = runBlocking {
val (_, list) = withVtodo(HIDE_SUBTASKS)
synchronizer.sync()
val task = caldavDao
.getTaskByRemoteId(list.uuid!!, "2822976a-b71e-4962-92e4-db7297789c20")
?.let { taskDao.fetch(it.task) }
assertTrue(task!!.isCollapsed)
}
@Test
fun pushCollapsedState() = runBlocking {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask(with(COLLAPSED, true)))
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(CaldavTaskMaker.TASK, taskId),
with(REMOTE_ID, "abcd")
))
synchronizer.sync()
assertTrue(openTaskDao.getTask(listId, "abcd")?.task!!.collapsed)
}
@Test
fun removeCollapsedState() = runBlocking {
val (listId, list) = withVtodo(HIDE_SUBTASKS)
synchronizer.sync()
val task = caldavDao.getTaskByRemoteId(list.uuid!!, "2822976a-b71e-4962-92e4-db7297789c20")
taskDao.setCollapsed(task!!.task, false)
synchronizer.sync()
assertFalse(
openTaskDao
.getTask(listId, "2822976a-b71e-4962-92e4-db7297789c20")
?.task
!!.collapsed
)
}
@Test
fun readSnoozeTime() = runBlocking {
val (_, list) = withVtodo(SNOOZED)
withTZ(CHICAGO) {
synchronizer.sync()
}
val task = caldavDao
.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5")
?.let { taskDao.fetch(it.task) }
assertEquals(
listOf(
Alarm(
id = 1,
task = task!!.id,
time = 1612972355000,
type = TYPE_SNOOZE
)
),
alarmDao.getAlarms(task.id)
)
}
@Test
fun pushSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = taskId,
time = DateTime(2021, 2, 4, 13, 30).millis,
type = TYPE_SNOOZE
)
)
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(CaldavTaskMaker.TASK, taskId),
with(REMOTE_ID, "abcd")
))
freezeAt(DateTime(2021, 2, 4, 12, 30, 45, 125)) {
synchronizer.sync()
}
assertEquals(1612467000000, openTaskDao.getTask(listId, "abcd")?.task!!.snooze)
}
@Test
fun dontPushLapsedSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask())
alarmDao.insert(
Alarm(
task = taskId,
time = DateTime(2021, 2, 4, 13, 30).millis,
type = TYPE_SNOOZE
)
)
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(CaldavTaskMaker.TASK, taskId),
with(REMOTE_ID, "abcd")
))
freezeAt(DateTime(2021, 2, 4, 13, 30, 45, 125)) {
synchronizer.sync()
}
assertNull(openTaskDao.getTask(listId, "abcd")?.task!!.snooze)
}
@Test
fun removeSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = withVtodo(SNOOZED)
synchronizer.sync()
val task = caldavDao.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5")
?: throw IllegalStateException("Missing task")
assertEquals(
listOf(Alarm(1, task.id, DateTime(2021, 2, 10, 9, 52, 35).millis, TYPE_SNOOZE)),
alarmDao.getAlarms(1)
)
alarmDao.deleteSnoozed(listOf(1))
taskDao.touch(task.task)
synchronizer.sync()
assertNull(
openTaskDao
.getTask(listId, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5")
?.task
!!.snooze
)
}
private suspend fun insertTag(task: Task, name: String) =
TagData(name = name)
.apply { tagDataDao.insert(this) }
.let { tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = it.remoteId)) }
companion object {
private val CHICAGO = TimeZone.getTimeZone("America/Chicago")
private val SUBTASK = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud Tasks v0.13.6
BEGIN:VTODO
UID:dfede1b0-435b-4bba-9708-2422e781747c
CREATED:20210128T150333
LAST-MODIFIED:20210128T150338
DTSTAMP:20210128T150338
SUMMARY:Child
RELATED-TO:7daa4a5c-cc76-4ddf-b4f8-b9d3a9cb00e7
END:VTODO
END:VCALENDAR
""".trimIndent()
private val ONE_TAG = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-110304//EN
BEGIN:VTODO
DTSTAMP:20210201T204211Z
UID:3076145036806467726
CREATED:20210201T204143Z
LAST-MODIFIED:20210201T204209Z
SUMMARY:Tags
CATEGORIES:Tag1
X-APPLE-SORT-ORDER:633734058
END:VTODO
END:VCALENDAR
""".trimIndent()
private val TWO_TAGS = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:+//IDN tasks.org//android-110304//EN
BEGIN:VTODO
DTSTAMP:20210201T204211Z
UID:3076145036806467726
CREATED:20210201T204143Z
LAST-MODIFIED:20210201T204209Z
SUMMARY:Tags
CATEGORIES:Tag1,Tag2
X-APPLE-SORT-ORDER:633734058
END:VTODO
END:VCALENDAR
""".trimIndent()
private val HIDE_SUBTASKS = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Nextcloud Tasks v0.13.6
BEGIN:VTODO
UID:2822976a-b71e-4962-92e4-db7297789c20
CREATED:20210209T104536
LAST-MODIFIED:20210209T104548
DTSTAMP:20210209T104548
SUMMARY:Parent
X-OC-HIDESUBTASKS:1
END:VTODO
END:VCALENDAR
""".trimIndent()
private val SNOOZED = """
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTODO
CREATED:20210210T151826Z
LAST-MODIFIED:20210210T152235Z
DTSTAMP:20210210T152235Z
UID:4CBBC669-70E3-474D-A0A3-0FC42A14A5A5
SUMMARY:Test snooze
STATUS:NEEDS-ACTION
X-MOZ-LASTACK:20210210T152235Z
DTSTART;TZID=America/Chicago:20210210T091900
DUE;TZID=America/Chicago:20210210T091900
X-MOZ-SNOOZE-TIME:20210210T155235Z
X-MOZ-GENERATION:1
END:VTODO
END:VCALENDAR
""".trimIndent()
}
}

@ -1,109 +0,0 @@
package org.tasks.opentasks
import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.RECUR
import org.tasks.makers.TaskMaker.newTask
@HiltAndroidTest
class OpenTasksSynchronizerTest : OpenTasksTest() {
@Test
fun createNewAccounts() = runBlocking {
openTaskDao.insertList()
synchronizer.sync()
val accounts = caldavDao.getAccounts()
assertEquals(1, accounts.size)
with(accounts[0]) {
assertEquals("bitfire.at.davdroid:test_account", uuid)
assertEquals("test_account", name)
assertEquals(TYPE_OPENTASKS, accountType)
}
}
@Test
fun deleteRemovedAccounts() = runBlocking {
caldavDao.insert(
CaldavAccount(
uuid = "bitfire.at.davdroid:test_account",
accountType = TYPE_OPENTASKS,
)
)
synchronizer.sync()
assertTrue(caldavDao.getAccounts().isEmpty())
}
@Test
fun createNewLists() = runBlocking {
openTaskDao.insertList()
synchronizer.sync()
val lists = caldavDao.getCalendarsByAccount("bitfire.at.davdroid:test_account")
assertEquals(1, lists.size)
with(lists[0]) {
assertEquals(name, "default_list")
}
}
@Test
fun removeMissingLists() = runBlocking {
val (_, list) = openTaskDao.insertList(url = "url1")
caldavDao.insert(
CaldavCalendar(
account = list.account,
url = "url2",
)
)
synchronizer.sync()
assertEquals(listOf(list), caldavDao.getCalendars())
}
@Test
fun simplePushNewTask() = runBlocking {
val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask())
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(REMOTE_ID, "1234"),
with(TASK, taskId)
))
synchronizer.sync()
assertNotNull(openTaskDao.getTask(listId.toLong(), "1234"))
}
@Test
fun sanitizeRecurrenceRule() = runBlocking {
val (_, list) = openTaskDao.insertList()
val taskId = taskDao.insert(newTask(with(RECUR, "RRULE:FREQ=WEEKLY;COUNT=-1")))
caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid),
with(TASK, taskId)
))
synchronizer.sync()
val task = openTaskDao.getTasks().first()
assertEquals("FREQ=WEEKLY", task.rRule?.value)
}
}

@ -1,34 +0,0 @@
package org.tasks.opentasks
import com.todoroo.astrid.dao.TaskDao
import org.junit.Before
import org.tasks.R
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.dao.CaldavDao
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences
import javax.inject.Inject
abstract class OpenTasksTest : InjectingTestCase() {
@Inject lateinit var openTaskDao: TestOpenTaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var synchronizer: OpenTasksSynchronizer
@Inject lateinit var taskDao: TaskDao
@Before
override fun setUp() {
super.setUp()
openTaskDao.reset()
preferences.setBoolean(R.string.p_debug_pro, true)
}
protected suspend fun withVtodo(vtodo: String): Pair<Long, CaldavCalendar> =
openTaskDao
.insertList()
.let { (listId, list) ->
openTaskDao.insertTask(listId, vtodo)
Pair(listId, list)
}
}

@ -1,98 +0,0 @@
package org.tasks.opentasks
import android.content.ContentProviderResult
import android.content.Context
import at.bitfire.ical4android.BatchOperation
import at.bitfire.ical4android.Task
import dagger.hilt.android.qualifiers.ApplicationContext
import org.dmfs.tasks.contract.TaskContract
import org.dmfs.tasks.contract.TaskContract.TaskListColumns.ACCESS_LEVEL_OWNER
import org.tasks.caldav.iCalendar
import org.tasks.data.MyAndroidTask
import org.tasks.data.OpenTaskDao
import org.tasks.data.UUIDHelper
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavCalendar
import javax.inject.Inject
class TestOpenTaskDao @Inject constructor(
@ApplicationContext context: Context,
private val caldavDao: CaldavDao
) : OpenTaskDao(context, caldavDao) {
suspend fun insertList(
name: String = DEFAULT_LIST,
type: String = DEFAULT_TYPE,
account: String = DEFAULT_ACCOUNT,
url: String = UUIDHelper.newUUID(),
accessLevel: Int = ACCESS_LEVEL_OWNER,
): Pair<Long, CaldavCalendar> {
val uri = taskLists.buildUpon()
.appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(TaskContract.TaskLists.ACCOUNT_NAME, account)
.appendQueryParameter(TaskContract.TaskLists.ACCOUNT_TYPE, type)
.build()
val result = applyOperation(
BatchOperation.CpoBuilder.newInsert(uri)
.withValue(TaskContract.CommonSyncColumns._SYNC_ID, url)
.withValue(TaskContract.TaskListColumns.LIST_NAME, name)
.withValue(TaskContract.TaskLists.SYNC_ENABLED, "1")
.withValue(TaskContract.TaskLists.ACCESS_LEVEL, accessLevel)
)
val calendar = CaldavCalendar(
uuid = UUIDHelper.newUUID(),
name = name,
account = "$type:$account",
url = url,
)
caldavDao.insert(calendar)
return Pair(result.uri!!.lastPathSegment!!.toLong(), calendar)
}
fun insertTask(listId: Long, vtodo: String) {
val ops = ArrayList<BatchOperation.CpoBuilder>()
val task = MyAndroidTask(iCalendar.fromVtodo(vtodo)!!)
ops.add(task.toBuilder(tasks).withValue(TaskContract.TaskColumns.LIST_ID, listId))
task.enqueueProperties(properties, ops, 0)
applyOperation(*ops.toTypedArray())
}
fun getTasks(): List<Task> {
val result = ArrayList<Task>()
cr.query(
tasks.buildUpon().appendQueryParameter(TaskContract.LOAD_PROPERTIES, "1").build(),
null,
null,
null,
null)?.use {
while (it.moveToNext()) {
MyAndroidTask(it).task?.let { task -> result.add(task) }
}
}
return result
}
fun reset(
type: String = DEFAULT_TYPE,
account: String = DEFAULT_ACCOUNT
) {
cr.delete(
taskLists.buildUpon()
.appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(TaskContract.TaskLists.ACCOUNT_NAME, account)
.appendQueryParameter(TaskContract.TaskLists.ACCOUNT_TYPE, type)
.build(),
null,
null
)
cr.delete(tasks, null, null)
}
private fun applyOperation(vararg builders: BatchOperation.CpoBuilder): ContentProviderResult =
cr.applyBatch(authority, ArrayList(builders.asList().map { it.build() }))[0]
companion object {
const val DEFAULT_ACCOUNT = "test_account"
const val DEFAULT_TYPE = ACCOUNT_TYPE_DAVX5
const val DEFAULT_LIST = "default_list"
}
}

@ -1,12 +0,0 @@
package org.tasks.preferences
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
class PermissivePermissionChecker(@ApplicationContext context: Context) : PermissionChecker(context) {
override fun canAccessCalendars() = true
override fun canAccessForegroundLocation() = true
override fun canAccessBackgroundLocation() = true
}

@ -1,152 +0,0 @@
package org.tasks.preferences
import android.annotation.SuppressLint
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.tasks.data.entity.Task.Companion.NOTIFY_AFTER_DEADLINE
import org.tasks.data.entity.Task.Companion.NOTIFY_AT_DEADLINE
import org.tasks.data.entity.Task.Companion.NOTIFY_AT_START
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.R
import org.tasks.TestUtilities.newPreferences
import org.tasks.time.DateTime
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class PreferenceTests {
private lateinit var preferences: Preferences
@Before
fun setUp() {
preferences = newPreferences(ApplicationProvider.getApplicationContext())
preferences.clear()
preferences.setBoolean(R.string.p_rmd_enable_quiet, true)
}
@Test
fun testNotQuietWhenQuietHoursDisabled() {
preferences.setBoolean(R.string.p_rmd_enable_quiet, false)
setQuietHoursStart(22)
setQuietHoursEnd(10)
val dueDate = DateTime(2015, 12, 29, 8, 0, 1).millis
assertEquals(dueDate, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testIsQuietAtStartOfQuietHoursNoWrap() {
setQuietHoursStart(18)
setQuietHoursEnd(19)
val dueDate = DateTime(2015, 12, 29, 18, 0, 1).millis
assertEquals(
DateTime(2015, 12, 29, 19, 0).millis, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testIsQuietAtStartOfQuietHoursWrap() {
setQuietHoursStart(22)
setQuietHoursEnd(10)
val dueDate = DateTime(2015, 12, 29, 22, 0, 1).millis
assertEquals(
DateTime(2015, 12, 30, 10, 0).millis, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testAdjustForQuietHoursNightWrap() {
setQuietHoursStart(22)
setQuietHoursEnd(10)
val dueDate = DateTime(2015, 12, 29, 23, 30).millis
assertEquals(
DateTime(2015, 12, 30, 10, 0).millis, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testAdjustForQuietHoursMorningWrap() {
setQuietHoursStart(22)
setQuietHoursEnd(10)
val dueDate = DateTime(2015, 12, 30, 7, 15).millis
assertEquals(
DateTime(2015, 12, 30, 10, 0).millis, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testAdjustForQuietHoursWhenStartAndEndAreSame() {
setQuietHoursStart(18)
setQuietHoursEnd(18)
val dueDate = DateTime(2015, 12, 29, 18, 0, 0).millis
assertEquals(dueDate, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testIsNotQuietAtEndOfQuietHoursNoWrap() {
setQuietHoursStart(17)
setQuietHoursEnd(18)
val dueDate = DateTime(2015, 12, 29, 18, 0).millis
assertEquals(dueDate, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testIsNotQuietAtEndOfQuietHoursWrap() {
setQuietHoursStart(22)
setQuietHoursEnd(10)
val dueDate = DateTime(2015, 12, 29, 10, 0).millis
assertEquals(dueDate, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testIsNotQuietBeforeNoWrap() {
setQuietHoursStart(17)
setQuietHoursEnd(18)
val dueDate = DateTime(2015, 12, 29, 11, 30).millis
assertEquals(dueDate, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testIsNotQuietAfterNoWrap() {
setQuietHoursStart(17)
setQuietHoursEnd(18)
val dueDate = DateTime(2015, 12, 29, 22, 15).millis
assertEquals(dueDate, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testIsNotQuietWrap() {
setQuietHoursStart(22)
setQuietHoursEnd(10)
val dueDate = DateTime(2015, 12, 29, 13, 45).millis
assertEquals(dueDate, preferences.adjustForQuietHours(dueDate))
}
@Test
fun testDefaultReminders() {
assertEquals(0, defaultReminders())
assertEquals(2, defaultReminders(NOTIFY_AT_DEADLINE))
assertEquals(4, defaultReminders(NOTIFY_AFTER_DEADLINE))
assertEquals(6, defaultReminders(NOTIFY_AT_DEADLINE, NOTIFY_AFTER_DEADLINE))
assertEquals(32, defaultReminders(NOTIFY_AT_START))
assertEquals(38, defaultReminders(NOTIFY_AT_START, NOTIFY_AT_DEADLINE, NOTIFY_AFTER_DEADLINE))
}
private fun setQuietHoursStart(hour: Int) {
preferences.setInt(R.string.p_rmd_quietStart, hour * MILLIS_PER_HOUR)
}
private fun setQuietHoursEnd(hour: Int) {
preferences.setInt(R.string.p_rmd_quietEnd, hour * MILLIS_PER_HOUR)
}
private fun defaultReminders(vararg values: Int): Int {
preferences.setStringSet(
R.string.p_default_reminders_key,
values.map { it.toString() }.toSet()
)
return preferences.defaultReminders
}
companion object {
@SuppressLint("NewApi")
private val MILLIS_PER_HOUR = TimeUnit.HOURS.toMillis(1).toInt()
}
}

@ -1,138 +0,0 @@
package org.tasks.repeats
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.Freeze
import org.tasks.TestUtilities.withTZ
import org.tasks.analytics.Firebase
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTime
import java.text.ParseException
import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject
@HiltAndroidTest
class RepeatRuleToStringTest : InjectingTestCase() {
@Inject lateinit var firebase: Firebase
@Test
fun daily() {
assertEquals("Repeats daily", toString("RRULE:FREQ=DAILY"))
}
@Test
fun weekly() {
assertEquals("Repeats weekly", toString("RRULE:FREQ=WEEKLY;INTERVAL=1"))
}
@Test
fun weeklyPlural() {
assertEquals("Repeats every 2 weeks", toString("RRULE:FREQ=WEEKLY;INTERVAL=2"))
}
@Test
fun weeklyByDay() {
assertEquals(
"Repeats weekly on Mon, Tue, Wed, Thu, Fri",
toString("RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR"))
}
@Test
fun printDaysInRepeatRuleOrder() {
assertEquals(
"Repeats weekly on Fri, Thu, Wed, Tue, Mon",
toString("RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=FR,TH,WE,TU,MO"))
}
@Test
fun useLocaleForDays() {
assertEquals(
"Wiederholt sich wöchentlich Sa., So.",
toString("de", "RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=SA,SU"))
}
@Test
fun everyFifthTuesday() {
assertEquals(
"Repeats monthly on every fifth Tuesday",
toString("RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=5TU")
)
}
@Test
fun everyLastWednesday() {
assertEquals(
"Repeats monthly on every last Wednesday",
toString("RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=-1WE")
)
}
@Test
fun everyFirstThursday() {
assertEquals(
"Repeats every 2 months on every first Thursday",
toString("RRULE:FREQ=MONTHLY;INTERVAL=2;BYDAY=1TH")
)
}
@Test
fun repeatUntilPositiveOffset() {
Freeze.freezeAt(DateTime(2021, 1, 4)) {
withTZ(BERLIN) {
assertEquals(
"Repeats daily, ends on February 23",
toString("RRULE:FREQ=DAILY;UNTIL=20210223;INTERVAL=1")
)
}
}
}
@Test
fun repeatUntilNoOffset() {
Freeze.freezeAt(DateTime(2021, 1, 4)) {
withTZ(LONDON) {
assertEquals(
"Repeats daily, ends on February 23",
toString("RRULE:FREQ=DAILY;UNTIL=20210223;INTERVAL=1")
)
}
}
}
@Test
fun repeatUntilNegativeOffset() {
Freeze.freezeAt(DateTime(2021, 1, 4)) {
withTZ(NEW_YORK) {
assertEquals(
"Repeats daily, ends on February 23",
toString("RRULE:FREQ=DAILY;UNTIL=20210223;INTERVAL=1")
)
}
}
}
private fun toString(rrule: String): String? {
return toString(null, rrule)
}
private fun toString(language: String?, rrule: String): String? {
return try {
val locale = language?.let { Locale.forLanguageTag(it) } ?: Locale.getDefault()
val configuration = context.resources.configuration.apply {
setLocale(locale)
}
RepeatRuleToString(context.createConfigurationContext(configuration), locale, firebase)
.toString(rrule)
} catch (e: ParseException) {
throw RuntimeException(e)
}
}
companion object {
private val BERLIN = TimeZone.getTimeZone("Europe/Berlin")
private val LONDON = TimeZone.getTimeZone("Europe/London")
private val NEW_YORK = TimeZone.getTimeZone("America/New_York")
}
}

@ -1,95 +0,0 @@
package org.tasks.ui.editviewmodel
import androidx.lifecycle.SavedStateHandle
import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.timers.TimerPlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.tasks.calendars.CalendarEventProvider
import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.LocationDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.dao.UserActivityDao
import org.tasks.data.db.Database
import org.tasks.data.entity.Task
import org.tasks.data.newLocalAccount
import org.tasks.injection.InjectingTestCase
import org.tasks.location.GeofenceApi
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.PermissivePermissionChecker
import org.tasks.preferences.Preferences
import org.tasks.ui.TaskEditViewModel
import javax.inject.Inject
open class BaseTaskEditViewModelTest : InjectingTestCase() {
@Inject lateinit var db: Database
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var timerPlugin: TimerPlugin
@Inject lateinit var calendarEventProvider: CalendarEventProvider
@Inject lateinit var gCalHelper: GCalHelper
@Inject lateinit var taskMover: TaskMover
@Inject lateinit var geofenceApi: GeofenceApi
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var alarmService: AlarmService
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var userActivityDao: UserActivityDao
@Inject lateinit var caldavDao: CaldavDao
protected lateinit var viewModel: TaskEditViewModel
@Before
override fun setUp() {
runBlocking {
super.setUp()
caldavDao.newLocalAccount()
}
}
protected fun setup(task: Task) = runBlocking {
viewModel = TaskEditViewModel(
context,
SavedStateHandle().apply {
set(TaskEditFragment.EXTRA_TASK, task)
},
taskDao,
taskDeleter,
timerPlugin,
PermissivePermissionChecker(context),
calendarEventProvider,
gCalHelper,
taskMover,
db.locationDao(),
geofenceApi,
db.tagDao(),
db.tagDataDao(),
preferences,
db.googleTaskDao(),
db.caldavDao(),
taskCompleter,
alarmService,
MutableSharedFlow(),
userActivityDao = userActivityDao,
taskAttachmentDao = db.taskAttachmentDao(),
alarmDao = db.alarmDao(),
defaultFilterProvider = defaultFilterProvider,
)
}
protected fun save(): Boolean = runBlocking(Dispatchers.Main) {
viewModel.save()
}
}

@ -1,41 +0,0 @@
package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert
import org.junit.Test
import org.tasks.data.entity.Task
import org.tasks.makers.TaskMaker
@HiltAndroidTest
class PriorityTests : BaseTaskEditViewModelTest() {
@Test
fun changePriorityCausesChange() {
setup(TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH)))
viewModel.setPriority(Task.Priority.MEDIUM)
Assert.assertTrue(viewModel.hasChanges())
}
@Test
fun applyPriorityChange() {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH))
setup(task)
viewModel.setPriority(Task.Priority.MEDIUM)
save()
Assert.assertEquals(Task.Priority.MEDIUM, task.priority)
}
@Test
fun noChangeWhenRevertingPriority() {
setup(TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH)))
viewModel.setPriority(Task.Priority.MEDIUM)
viewModel.setPriority(Task.Priority.HIGH)
Assert.assertFalse(viewModel.hasChanges())
}
}

@ -1,192 +0,0 @@
package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.service.TaskCreator.Companion.setDefaultReminders
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.R
import org.tasks.data.createDueDate
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Alarm.Companion.whenDue
import org.tasks.data.entity.Alarm.Companion.whenOverdue
import org.tasks.data.entity.Alarm.Companion.whenStarted
import org.tasks.data.entity.Task
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.START_DATE
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
@HiltAndroidTest
class ReminderTests : BaseTaskEditViewModelTest() {
@Test
fun whenStartReminder() = runBlocking {
preferences.setStringSet(
R.string.p_default_reminders_key,
hashSetOf(Task.NOTIFY_AT_START.toString())
)
val task = newTask(with(START_DATE, DateTime()))
task.setDefaultReminders(preferences)
setup(task)
assertEquals(
persistentSetOf(Alarm(type = Alarm.TYPE_REL_START)),
viewModel.viewState.value.alarms
)
}
@Test
fun whenDueReminder() = runBlocking {
preferences.setStringSet(
R.string.p_default_reminders_key,
hashSetOf(Task.NOTIFY_AT_DEADLINE.toString())
)
val task = newTask(with(DUE_TIME, DateTime()))
task.setDefaultReminders(preferences)
setup(task)
assertEquals(
persistentSetOf(Alarm(type = Alarm.TYPE_REL_END)),
viewModel.viewState.value.alarms
)
}
@Test
fun whenOverDueReminder() = runBlocking {
preferences.setStringSet(
R.string.p_default_reminders_key,
hashSetOf(Task.NOTIFY_AFTER_DEADLINE.toString())
)
val task = newTask(with(DUE_TIME, DateTime()))
task.setDefaultReminders(preferences)
setup(task)
assertEquals(
persistentSetOf(whenOverdue(0)),
viewModel.viewState.value.alarms
)
}
@Test
fun ringFiveTimes() = runBlocking {
val task = newTask()
setup(task)
viewModel.ringFiveTimes = true
save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeFive)
}
@Test
fun ringNonstop() = runBlocking {
val task = newTask()
setup(task)
viewModel.ringNonstop = true
save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
}
@Test
fun ringFiveTimesCantRingNonstop() = runBlocking {
val task = newTask()
setup(task)
viewModel.ringNonstop = true
viewModel.ringFiveTimes = true
save()
assertFalse(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeFive)
}
@Test
fun ringNonStopCantRingFiveTimes() = runBlocking {
val task = newTask()
setup(task)
viewModel.ringFiveTimes = true
viewModel.ringNonstop = true
save()
assertFalse(taskDao.fetch(task.id)!!.isNotifyModeFive)
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
}
@Test
fun noDefaultRemindersWithNoDates() = runBlocking {
val task = newTask()
task.setDefaultReminders(preferences)
setup(task)
save()
assertTrue(alarmDao.getAlarms(task.id).isEmpty())
}
@Test
fun addDefaultRemindersWhenAddingDueDate() = runBlocking {
preferences.setStringSet(
R.string.p_default_reminders_key,
hashSetOf(
Task.NOTIFY_AT_DEADLINE.toString(),
Task.NOTIFY_AFTER_DEADLINE.toString(),
)
)
val task = newTask()
setup(task)
viewModel.setDueDate(
createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME,
currentTimeMillis()
)
)
save()
assertEquals(
listOf(whenDue(1).copy(id = 1), whenOverdue(1).copy(id = 2)),
alarmDao.getAlarms(task.id)
)
}
@Test
fun addDefaultRemindersWhenAddingStartDate() = runBlocking {
preferences.setStringSet(
R.string.p_default_reminders_key,
hashSetOf(Task.NOTIFY_AT_START.toString())
)
val task = newTask()
setup(task)
viewModel.setStartDate(
createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME,
currentTimeMillis()
)
)
save()
assertEquals(
listOf(whenStarted(1).copy(id = 1)),
alarmDao.getAlarms(task.id)
)
}
}

@ -1,39 +0,0 @@
package org.tasks.ui.editviewmodel
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.entity.Task
import org.tasks.makers.TaskMaker.newTask
@HiltAndroidTest
class TaskEditViewModelTest : BaseTaskEditViewModelTest() {
@Test
fun noChangesForNewTask() {
setup(newTask())
assertFalse(viewModel.hasChanges())
}
@Test
fun dontSaveTaskWithoutChanges() = runBlocking {
setup(newTask())
assertFalse(save())
assertTrue(taskDao.getAll().isEmpty())
}
@Test
fun dontSaveTaskTwice() = runBlocking {
setup(newTask())
viewModel.setPriority(Task.Priority.HIGH)
assertTrue(save())
assertFalse(viewModel.save())
}
}

@ -1,163 +0,0 @@
package org.tasks.ui.editviewmodel
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.DeletionDao
import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.Task
import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.PermissivePermissionChecker
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.ui.TaskListViewModel
import javax.inject.Inject
@HiltAndroidTest
class TaskListViewModelTest : InjectingTestCase() {
private lateinit var viewModel: TaskListViewModel
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var deletionDao: DeletionDao
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var inventory: Inventory
@Inject lateinit var firebase: Firebase
@Inject lateinit var caldavDao: CaldavDao
@Before
override fun setUp() {
super.setUp()
viewModel = TaskListViewModel(
applicationContext = context,
preferences = preferences,
taskDao = taskDao,
deletionDao = deletionDao,
taskDeleter = taskDeleter,
localBroadcastManager = localBroadcastManager,
inventory = inventory,
firebase = firebase,
permissionChecker = PermissivePermissionChecker(context),
caldavDao = caldavDao,
)
viewModel.setFilter(runBlocking { MyTasksFilter.create() })
}
@Test
fun clearCompletedTask() = runBlocking {
val task = taskDao.createNew(
Task(completionDate = currentTimeMillis())
)
clearCompleted()
assertTrue(taskDao.fetch(task)!!.isDeleted)
}
@Test
fun dontDeleteTaskWithRecurringParent() = runBlocking {
val parent = taskDao.createNew(
Task(
recurrence = "RRULE:FREQ=DAILY;INTERVAL=1"
)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertFalse(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun dontDeleteTaskWithRecurringGrandparent() = runBlocking {
val grandparent = taskDao.createNew(
Task(recurrence = "RRULE:FREQ=DAILY;INTERVAL=1")
)
val parent = taskDao.createNew(
Task(parent = grandparent)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertFalse(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearGrandchildWithNoRecurringAncestors() = runBlocking {
val grandparent = taskDao.createNew(Task())
val parent = taskDao.createNew(
Task(parent = grandparent)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearGrandchildWithCompletedRecurringAncestor() = runBlocking {
val grandparent = taskDao.createNew(
Task(
recurrence = "RRULE:FREQ=DAILY;INTERVAL=1",
completionDate = currentTimeMillis(),
)
)
val parent = taskDao.createNew(
Task(parent = grandparent)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearHiddenSubtask() = runBlocking {
preferences.showCompleted = false
val parent = taskDao.createNew(Task())
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
private suspend fun clearCompleted() = viewModel.markDeleted(viewModel.getTasksToClear())
}

@ -1,42 +0,0 @@
package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.entity.Task.Priority.Companion.HIGH
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.newTask
@HiltAndroidTest
class TitleTests : BaseTaskEditViewModelTest() {
@Test
fun changeTitleCausesChange() {
setup(newTask())
viewModel.setTitle("Test")
assertTrue(viewModel.hasChanges())
}
@Test
fun saveWithEmptyTitle() = runBlocking {
val task = newTask()
setup(task)
viewModel.setPriority(HIGH)
save()
assertEquals("(No title)", taskDao.fetch(task.id)!!.title)
}
@Test
fun newTaskPrepopulatedWithTitleHasChanges() {
setup(newTask(with(TaskMaker.TITLE, "some title")))
assertTrue(viewModel.hasChanges())
}
}

@ -1,113 +0,0 @@
package org.tasks.billing
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences
import javax.inject.Inject
@HiltAndroidTest
class InventoryTest : InjectingTestCase() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var signatureVerifier: SignatureVerifier
@Inject lateinit var caldavDao: CaldavDao
lateinit var inventory: Inventory
@Test
fun hasTasksAccount() = runBlocking {
caldavDao.insert(CaldavAccount(accountType = CaldavAccount.TYPE_TASKS, url = "https://caldav.tasks.org/calendars/"))
initInventory()
inventory.updateTasksAccount()
assertTrue(inventory.hasTasksAccount)
}
@Test
fun hasTasksAccountWithCaldav() = runBlocking {
caldavDao.insert(CaldavAccount(accountType = CaldavAccount.TYPE_CALDAV, url = "https://caldav.tasks.org/calendars/"))
initInventory()
inventory.updateTasksAccount()
assertTrue(inventory.hasTasksAccount)
}
@Test
fun monthlyIsPro() {
withPurchases(monthly01)
assertTrue(inventory.hasPro)
assertEquals(1, inventory.subscription.value?.subscriptionPrice)
}
@Test
fun testMonthlyUpgrade() {
withPurchases(monthly01, monthly03)
assertEquals(3, inventory.subscription.value?.subscriptionPrice)
}
@Test
fun testMonthlyOverAnnual() {
withPurchases(monthly01, annual03)
assertTrue(inventory.subscription.value!!.isMonthly)
}
@Test
fun isCancelled() {
withPurchases(annual03Cancelled)
assertTrue(inventory.subscription.value!!.isCanceled)
}
@Test
fun cancelledIsStillPro() {
withPurchases(annual03Cancelled)
assertTrue(inventory.subscription.value!!.isProSubscription)
}
private fun withPurchases(vararg purchases: String) {
val asPurchases =
purchases
.map(::JSONObject)
.map {
com.android.billingclient.api.Purchase(
it.getString("zza"),
it.getString("zzb")
)
}
.map(::Purchase)
preferences.setPurchases(asPurchases)
initInventory()
}
private fun initInventory() {
runOnMainSync {
inventory = Inventory(
context,
preferences,
signatureVerifier,
localBroadcastManager,
caldavDao
)
}
}
companion object {
private const val annual03 = """{"zza":"{\"orderId\":\"GPA.3372-3222-8630-38485\",\"packageName\":\"org.tasks\",\"productId\":\"annual_03\",\"purchaseTime\":1603917413542,\"purchaseState\":0,\"purchaseToken\":\"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg\",\"autoRenewing\":true}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3372-3222-8630-38485","packageName":"org.tasks","productId":"annual_03","purchaseTime":1603917413542,"purchaseState":0,"purchaseToken":"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg","autoRenewing":true}},"zzb":"Od2ulMjFethYNdA1rTm7AvNMyfFgefCaZhtBeYuTHlMB/XbEd/m9noRlKWMnShFthnQyw97CfrB86aaB52OSWm9pGkPzaRtOJPyL8BJHP9LEjXHOQIQ2Nx9zRF30+EWgV4O0IyeL/o5eUvTQRNnfyUXFdJQRLiKTblQojO6mTCX2fA6lTAntjJpbTbYGuYZjg782gX5HvmwQN5CJu7ZVZCH9AmsnAqZgb7h+MXhquQjv0L4pDVDp3dyDDwgpCAvSRy3550ZANPfNGsQpPr9Iv9IGoK0/INZRrq63VEEAz2mBGkzJgyQUYVtT6AylvNrqdo0w17hs0MLfsj6dwvSlYw\u003d\u003d"}"""
private const val monthly01 = """{"zza":"{\"orderId\":\"GPA.3369-0544-4429-52590\",\"packageName\":\"org.tasks\",\"productId\":\"monthly_01\",\"purchaseTime\":1603912474316,\"purchaseState\":0,\"purchaseToken\":\"iibbhlkglfjcgdebphiklajb.AO-J1OyJd2kCytLMfT8Vszibf_E99ffLha5cHgOM8o3gYPKy1kD8nIZh0hcEEyOPe7fsdFJrR1-gtvg8WKLFNJoCdqrerJ2Z6Q\",\"autoRenewing\":true}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3369-0544-4429-52590","packageName":"org.tasks","productId":"monthly_01","purchaseTime":1603912474316,"purchaseState":0,"purchaseToken":"iibbhlkglfjcgdebphiklajb.AO-J1OyJd2kCytLMfT8Vszibf_E99ffLha5cHgOM8o3gYPKy1kD8nIZh0hcEEyOPe7fsdFJrR1-gtvg8WKLFNJoCdqrerJ2Z6Q","autoRenewing":true}},"zzb":"UK7fdCY61QownZW8jDLB1myUKf1llFh9rj5I7P8V03AgdA6LGpEUiCvMvCqHfMGpY3VewmawezqiCUdGWGr+UgS+6QHEuFjpO8L+E36JUDqlU9uoGrTsXLI1gXQNQElGJ71DrKlFBbyyBHSeGWnzijcq4DyyHQzpmsqijxfs0KGjkta2TiOCtyxS+YA569xaGi6lcLGTyMEe7wS5bcjdfwFir0uVtCP+iqjoEd3kt4/03l9BEJYgf8eBxI0vrm4O+jYDJu8gGMTSQZiSqb0wN4sq8D9ksV+BcI4az6LVa1d6nuD+ob0Woe0/P2uoXG8nTEZJnrAZjkG6q8736HP6rw\u003d\u003d"}"""
private const val monthly03 = """{"zza":"{\"orderId\":\"GPA.3348-6247-8527-38213\",\"packageName\":\"org.tasks\",\"productId\":\"monthly_03\",\"purchaseTime\":1603912730414,\"purchaseState\":0,\"purchaseToken\":\"cmomnojdllomadpoinoabbkd.AO-J1OypdY4iXbMrF21L6Evn3wZSccwiBq-d55G1BVcrkwuH69zOuqb35yZnVynEb9KEvnQvgYQsUpv1AD5749iU-eDo4TRV5A\",\"autoRenewing\":true}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3348-6247-8527-38213","packageName":"org.tasks","productId":"monthly_03","purchaseTime":1603912730414,"purchaseState":0,"purchaseToken":"cmomnojdllomadpoinoabbkd.AO-J1OypdY4iXbMrF21L6Evn3wZSccwiBq-d55G1BVcrkwuH69zOuqb35yZnVynEb9KEvnQvgYQsUpv1AD5749iU-eDo4TRV5A","autoRenewing":true}},"zzb":"FkkW5FPw2elWnenIoQT7U5BnL2prcuK0GJEaHKtObPujSGRfJWFfThe3yuQ0w9AuTO0EDbm7LbJI44AiVJmpva3Iz3U2np2eNBuUAJIw9eECvQjEvuYk6Vq7LIgJwEsTyA8xRwjLJm+R1mmMWOxURmvDVBgDTHCOJsdUI9s52CSTQf2Ek+XABHugrMJudO43LzDuV2sP9mCqXUnSLbBXe3zZKyhhuz7gD+/5yavkRsPOVcZnsJetdxEmnrip8JEvgtHAvciPkvSD/fYeXdAlY2HiQWK/S0/I+yRaCEK8V+Um78ibbYc4Ng5NcXDm44nTv3F6jQEzYy4qRv/ohmwEQg\u003d\u003d"}"""
private const val annual03Cancelled = """{"zza":"{\"orderId\":\"GPA.3372-3222-8630-38485\",\"packageName\":\"org.tasks\",\"productId\":\"annual_03\",\"purchaseTime\":1603917413542,\"purchaseState\":0,\"purchaseToken\":\"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg\",\"autoRenewing\":false}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3372-3222-8630-38485","packageName":"org.tasks","productId":"annual_03","purchaseTime":1603917413542,"purchaseState":0,"purchaseToken":"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg","autoRenewing":false}},"zzb":"jL+2qRv0LtCutoJ86NWaInbx/9/kIWbxXRKYkou74TBjwu9KZ89EpJY632ImEy2xfLd8DHuVuWOcZY646I29Ny2E4HYNAsQEg2du4NRXEHZvu+py4Mi212KF8S2EPNdZCor1wiOJ0zRVBiRAtiCfqxHjQdfKn7FpDiHFrUhMu1huEAxJ0Xrnvxcmkouizw3wzKnAvI+O75LIWWZHCy+1o7s285cSKtQoztVY/nHInJLxV6dk93lAivOlEox+VCLU978lUvv45Rue50fMzS2CRsVFmRt9/yTP8RCiQKzGC/pyHtqNj/ceCrDi4VV8JPhsPd4NUaKk82Oq1xmGXtzEcQ\u003d\u003d"}"""
}
}

@ -1,35 +0,0 @@
package org.tasks.caldav
import androidx.test.annotation.UiThreadTest
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.R
import org.tasks.billing.Inventory
import org.tasks.data.entity.CaldavAccount
import javax.inject.Inject
@HiltAndroidTest
class CaldavSubscriptionTest : CaldavTest() {
@Inject lateinit var inventory: Inventory
@Test
@UiThreadTest
fun cantSyncWithoutPro() = runBlocking {
preferences.setBoolean(R.string.p_debug_pro, false)
inventory.clear()
inventory.add(emptyList())
account = CaldavAccount(uuid = UUIDHelper.newUUID())
.let { it.copy(id = caldavDao.insert(it)) }
synchronizer.sync(account)
assertEquals(
context.getString(R.string.requires_pro_subscription),
caldavDao.getAccountByUuid(account.uuid!!)?.error
)
}
}

@ -1,23 +0,0 @@
package org.tasks.opentasks
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.R
@HiltAndroidTest
class OpenTasksSubscriptionTest : OpenTasksTest() {
@Test
fun cantSyncWithoutPro() = runBlocking {
preferences.setBoolean(R.string.p_debug_pro, false)
openTaskDao.insertList()
synchronizer.sync()
assertEquals(
context.getString(R.string.requires_pro_subscription),
caldavDao.getAccounts()[0].error
)
}
}

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="GoogleAppIndexingWarning">
</application>
</manifest>

@ -1,40 +0,0 @@
{
"project_info": {
"project_number": "448612483090",
"firebase_url": "https://tasks-debug.firebaseio.com",
"project_id": "tasks-debug",
"storage_bucket": "tasks-debug.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:448612483090:android:c31434c55745e54c",
"android_client_info": {
"package_name": "org.tasks"
}
},
"oauth_client": [
{
"client_id": "448612483090-ns27d5rn3nm5nh4fjrkc22ag9qslkuho.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBLqXwszuCxCXdYRz7FCgpJa9Kufo4cs8E"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "448612483090-ns27d5rn3nm5nh4fjrkc22ag9qslkuho.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}

@ -1,48 +0,0 @@
package org.tasks
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import com.todoroo.andlib.utility.AndroidUtilities.atLeastQ
import leakcanary.AppWatcher
import org.tasks.logging.FileLogger
import org.tasks.preferences.Preferences
import timber.log.Timber
import javax.inject.Inject
class BuildSetup @Inject constructor(
private val context: Application,
private val preferences: Preferences,
private val fileLogger: FileLogger,
) {
fun setup() {
Timber.plant(Timber.DebugTree())
Timber.plant(fileLogger)
if (preferences.getBoolean(R.string.p_leakcanary, false)) {
AppWatcher.manualInstall(context)
}
if (preferences.getBoolean(R.string.p_strict_mode_thread, false)) {
val builder = StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog()
if (preferences.getBoolean(R.string.p_crash_main_queries, false)) {
builder.penaltyDeath()
}
StrictMode.setThreadPolicy(builder.build())
}
if (preferences.getBoolean(R.string.p_strict_mode_vm, false)) {
val builder = VmPolicy.Builder()
.detectActivityLeaks()
.detectLeakedSqlLiteObjects()
.detectLeakedRegistrationObjects()
.detectLeakedClosableObjects()
.detectFileUriExposure()
.penaltyLog()
.detectContentUriWithoutPermission()
if (atLeastQ()) {
builder
.detectCredentialProtectedWhileLocked()
.detectImplicitDirectBoot()
}
StrictMode.setVmPolicy(builder.build())
}
}
}

@ -1,115 +0,0 @@
package org.tasks.preferences.fragments
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import at.bitfire.cert4android.CustomCertManager.Companion.resetCertificates
import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.billing.BillingClient
import org.tasks.billing.Inventory
import org.tasks.data.createDueDate
import org.tasks.data.entity.Task
import org.tasks.extensions.Context.toast
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.min
@AndroidEntryPoint
class Debug : InjectingPreferenceFragment() {
@Inject lateinit var inventory: Inventory
@Inject lateinit var billingClient: BillingClient
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var taskDao: com.todoroo.astrid.dao.TaskDao
override fun getPreferenceXml() = R.xml.preferences_debug
override suspend fun setupPreferences(savedInstanceState: Bundle?) {
for (pref in listOf(
R.string.p_leakcanary,
R.string.p_strict_mode_vm,
R.string.p_strict_mode_thread,
R.string.p_crash_main_queries
)) {
findPreference(pref)
.setOnPreferenceChangeListener { _: Preference?, _: Any? ->
showRestartDialog()
true
}
}
findPreference(R.string.debug_reset_ssl).setOnPreferenceClickListener {
resetCertificates(requireContext())
context?.toast("SSL certificates reset")
false
}
findPreference(R.string.debug_force_restart).setOnPreferenceClickListener {
restart()
false
}
setupIap(R.string.debug_themes, Inventory.SKU_THEMES)
findPreference(R.string.debug_crash_app).setOnPreferenceClickListener {
throw RuntimeException("Crashed app from debug preferences")
}
findPreference(R.string.debug_clear_hints).setOnPreferenceClickListener {
preferences.installDate =
min(preferences.installDate, currentTimeMillis() - TimeUnit.DAYS.toMillis(14))
preferences.lastSubscribeRequest = 0L
preferences.lastReviewRequest = 0L
preferences.shownBeastModeHint = false
preferences.warnMicrosoft = true
preferences.warnGoogleTasks = true
preferences.warnQuietHoursDisabled = true
preferences.setBoolean(R.string.p_just_updated, true)
true
}
findPreference(R.string.debug_create_tasks).setOnPreferenceClickListener {
lifecycleScope.launch {
val count = 5000
for (i in 1..count) {
val task = taskCreator.createWithValues("")
taskDao.createNew(task)
task.title = "Task ${task.id}"
task.dueDate = createDueDate(Task.URGENCY_SPECIFIC_DAY, currentTimeMillis())
taskDao.save(task)
}
Toast.makeText(context, "Created $count tasks", Toast.LENGTH_SHORT).show()
}
false
}
}
private fun setupIap(@StringRes prefId: Int, sku: String) {
val preference: Preference = findPreference(prefId)
if (inventory.getPurchase(sku) == null) {
preference.title = getString(R.string.debug_purchase, sku)
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
lifecycleScope.launch {
billingClient.initiatePurchaseFlow(requireActivity().parent, "inapp" /*SkuType.INAPP*/, sku)
}
false
}
} else {
preference.title = getString(R.string.debug_consume, sku)
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
lifecycleScope.launch {
billingClient.consume(sku)
}
false
}
}
}
}

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

Loading…
Cancel
Save