mirror of https://github.com/tasks/tasks
Compare commits
No commits in common. 'main' and '4.8.12' have entirely different histories.
@ -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
|
||||
@ -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.
|
||||
[](https://travis-ci.org/tasks/tasks) [](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)
|
||||
[](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> [](https://f-droid.org/repository/browse/?fdid=org.tasks) [](https://www.amazon.com/gp/product/B00QHGTL7O/ref=mas_pm_tasks_astrid_to_do_list_clone)
|
||||
|
||||
---
|
||||
### Screenshots
|
||||
|
||||
[](https://tasks.org/docs/donate)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=alex@tasks.org)
|
||||
[](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"/>
|
||||
|
||||
[](https://github.com/tasks/tasks/actions/workflows/bundle.yml) [](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>
|
||||
@ -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,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>"43b3ffaac5131880e4dd07a79adba82a"</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>"43b3ffaac5131880e4dd07a79adba82a"</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…
Reference in New Issue