Compare commits

..

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

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

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

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

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

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

9
.gitignore vendored

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

@ -0,0 +1,22 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ControlFlowStatementWithoutBraces" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="Convert2streamapi" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="DoubleBraceInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="Guava" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LoggerInitializedWithForeignClass" enabled="false" level="WARNING" enabled_by_default="false">
<option name="loggerClassName" value="org.apache.log4j.Logger,org.slf4j.LoggerFactory,org.apache.commons.logging.LogFactory,java.util.logging.Logger" />
<option name="loggerFactoryMethodName" value="getLogger,getLogger,getLog,getLogger" />
</inspection_tool>
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
<inspection_tool class="StaticPseudoFunctionalStyleMethod" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SuspiciousIndentAfterControlStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TryFinallyCanBeTryWithResources" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="TryWithIdenticalCatches" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

@ -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,28 @@
language: android
sudo: required
jdk: oraclejdk8
env:
matrix:
- ANDROID_TARGET=android-21 ANDROID_ABI=armeabi-v7a
android:
components:
- tools # https://github.com/travis-ci/travis-ci/issues/5049
- tools # https://github.com/travis-ci/travis-ci/issues/6040
- android-26
- sys-img-armeabi-v7a-android-21
- platform-tools
- build-tools-26.0.1
- 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 :app:lintGoogleplayDebug
- ./gradlew :app:connectedGoogleplayDebugAndroidTest

@ -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,12 @@
Astrid was a popular cross-platform productivity service that was [acquired](https://web.archive.org/web/20130811052500/http://blog.astrid.com/blog/2013/05/01/yahoo-acquires-astrid/) and [discontinued](https://techcrunch.com/2013/07/06/astrid-goes-dark-august-5-goodnight-sweet-squid/) in 2013. The source code from Astrid's open source Android app serves as the basis of Tasks.
[![Build Status](https://travis-ci.org/tasks/tasks.svg?branch=master)](https://travis-ci.org/tasks/tasks)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
alt="Get it on Google Play"
height="80">](https://play.google.com/store/apps/details?id=org.tasks)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="80">](https://f-droid.org/packages/org.tasks)
[![Make a contribution](https://pledgie.com/campaigns/24281.png?skin_name=chrome)](https://pledgie.com/campaigns/24281)
Please visit [tasks.org](https://tasks.org) for end user documentation and support
<a href='https://play.google.com/store/apps/details?id=org.tasks&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' width="210" height="80"/></a>
<a href='https://f-droid.org/repository/browse/?fdid=org.tasks'><img src='https://f-droid.org/wiki/images/f/ff/F-Droid-button_available-on_bigger.png' /></a>
[![Get it on Amazon App Store](https://images-na.ssl-images-amazon.com/images/G/01/mobile-apps/devportal2/res/images/amazon-underground-app-us-black.png)](https://www.amazon.com/gp/product/B00QHGTL7O/ref=mas_pm_tasks_astrid_to_do_list_clone)
---
[![Donate with Bitcoin](https://img.shields.io/badge/bitcoin-donate-yellow.svg?logo=bitcoin)](https://tasks.org/docs/donate)
[![PayPal donate button](https://img.shields.io/badge/paypal-donate-yellow.svg?logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=alex@tasks.org)
[![Liberapay donate button](https://img.shields.io/liberapay/receives/tasks.svg?logo=liberapay)](https://liberapay.com/tasks/donate)
[![build](https://github.com/tasks/tasks/actions/workflows/bundle.yml/badge.svg)](https://github.com/tasks/tasks/actions/workflows/bundle.yml) [![weblate](https://hosted.weblate.org/widgets/tasks/-/android/svg-badge.svg)](https://hosted.weblate.org/engage/tasks/?utm_source=widget)
### Contributing
Contributions are always welcome! Whether translations, code changes, bug reports, feature requests, or otherwise, your help is appreciated. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md).
### Communication
You can submit questions to [GitHub Discussions](https://github.com/tasks/tasks/discussions).
If you have a suggestion or want to report a bug, please see [CONTRIBUTING.md](CONTRIBUTING.md).
Visit the wiki to
* [help with translations](https://github.com/tasks/tasks/wiki/Translations)
* [become a beta tester](https://github.com/tasks/tasks/wiki/Beta-Testing)
* [get started with development](https://github.com/tasks/tasks/wiki/Getting-Started-with-Development)

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

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

@ -0,0 +1,168 @@
apply plugin: 'com.android.application'
task wrapper(type: Wrapper) {
gradleVersion = '4.1'
}
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0-beta5'
}
}
repositories {
jcenter()
google()
}
android {
lintOptions {
lintConfig file("lint.xml")
textOutput 'stdout'
textReport true
}
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "org.tasks"
versionCode 469
versionName "5.0.0"
targetSdkVersion 26
minSdkVersion 15
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
signingConfigs {
release
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes {
debug {
multiDexEnabled true
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.pro'
signingConfig signingConfigs.release
}
}
flavorDimensions 'store'
productFlavors {
generic {
dimension 'store'
}
googleplay {
dimension 'store'
}
amazon {
dimension 'store'
}
}
if (project.hasProperty('keyAlias') &&
project.hasProperty('storeFile') &&
project.hasProperty('storePassword') &&
project.hasProperty('keyPassword')) {
android.signingConfigs.release.keyAlias = keyAlias
android.signingConfigs.release.storeFile = file(storeFile)
android.signingConfigs.release.storePassword = storePassword
android.signingConfigs.release.keyPassword = keyPassword
} else {
buildTypes.release.signingConfig = null
}
}
configurations {
all*.exclude group: 'com.google.guava', module: 'guava-jdk5'
all*.exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}
final DAGGER_VERSION = '2.9'
final BUTTERKNIFE_VERSION = '8.8.1'
final GPS_VERSION = '11.2.2'
final SUPPORT_VERSION = '26.0.2'
final STETHO_VERSION = '1.5.0'
final ROOM_VERSION = '1.0.0-alpha9'
final TESTING_SUPPORT_VERSION = '1.0.0'
dependencies {
annotationProcessor "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
compile "com.google.dagger:dagger:${DAGGER_VERSION}"
compile "android.arch.persistence.room:rxjava2:${ROOM_VERSION}"
annotationProcessor "android.arch.persistence.room:compiler:${ROOM_VERSION}"
compile "io.reactivex.rxjava2:rxandroid:2.0.1"
annotationProcessor "com.jakewharton:butterknife-compiler:${BUTTERKNIFE_VERSION}"
compile "com.jakewharton:butterknife:${BUTTERKNIFE_VERSION}"
debugCompile ("com.facebook.stetho:stetho:${STETHO_VERSION}") {
exclude group: 'com.google.code.findbugs', module: 'jsr305'
}
debugCompile "com.facebook.stetho:stetho-timber:${STETHO_VERSION}@aar"
//noinspection GradleDependency
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
debugCompile 'com.android.support:multidex:1.0.2'
compile 'com.github.rey5137:material:1.2.4'
compile 'com.nononsenseapps:filepicker:4.1.0'
compile "com.android.support:design:${SUPPORT_VERSION}"
compile "com.android.support:support-annotations:${SUPPORT_VERSION}"
compile "com.android.support:support-v13:${SUPPORT_VERSION}"
compile "com.android.support:cardview-v7:${SUPPORT_VERSION}"
compile 'com.jakewharton.timber:timber:4.5.1'
compile 'com.jakewharton.threetenabp:threetenabp:1.0.5'
//noinspection GradleDependency
compile 'com.google.guava:guava:20.0'
compile 'com.jakewharton:process-phoenix:2.0.0'
compile 'com.google.android.apps.dashclock:dashclock-api:2.0.0'
compile 'com.twofortyfouram:android-plugin-api-for-locale:1.0.2'
compile 'com.bignerdranch.android:recyclerview-multiselect:0.2'
compile ('com.rubiconproject.oss:jchronic:0.2.6') {
transitive = false
}
compile ('org.scala-saddle:google-rfc-2445:20110304') {
transitive = false
}
compile ('com.wdullaer:materialdatetimepicker:3.2.3') {
exclude group: 'com.android.support', module: 'support-v4'
}
compile "me.leolin:ShortcutBadger:1.1.18@aar"
googleplayCompile "com.google.android.gms:play-services-location:${GPS_VERSION}"
googleplayCompile "com.google.android.gms:play-services-analytics:${GPS_VERSION}"
googleplayCompile "com.google.android.gms:play-services-auth:${GPS_VERSION}"
googleplayCompile "com.google.android.gms:play-services-places:${GPS_VERSION}"
googleplayCompile 'com.google.apis:google-api-services-tasks:v1-rev47-1.22.0'
googleplayCompile 'com.google.api-client:google-api-client-android:1.22.0'
amazonCompile "com.google.android.gms:play-services-analytics:${GPS_VERSION}"
androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
androidTestAnnotationProcessor "com.jakewharton:butterknife-compiler:${BUTTERKNIFE_VERSION}"
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'com.natpryce:make-it-easy:4.0.1'
androidTestCompile "com.android.support.test:runner:${TESTING_SUPPORT_VERSION}"
androidTestCompile "com.android.support.test:rules:${TESTING_SUPPORT_VERSION}"
androidTestCompile "com.android.support:support-annotations:${SUPPORT_VERSION}"
}

@ -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 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<lint>
<issue id="MissingTranslation" severity="ignore"/>
<issue id="InconsistentArrays" severity="error"/>
<issue id="MissingTranslation" severity="ignore"/>
<issue id="VectorRaster" severity="ignore" />
<issue id="MissingQuantity" severity="ignore"/>
<issue id="ImpliedQuantity" severity="ignore"/>
<issue id="InvalidPackage">
<ignore regexp="net.fortuna.ical4j.util.JCacheTimeZoneCache"/>
</issue>
<issue id="MissingQuantity" severity="ignore"/>
<issue id="ImpliedQuantity" severity="ignore"/>
</lint>

73
app/proguard.pro vendored

@ -1,6 +1,21 @@
-dontobfuscate
-keep class org.tasks.** { *; }
# https://code.google.com/p/android/issues/detail?id=78293
-keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; }
-keep public class android.support.v7.internal.view.menu.** { *; }
-keep public class * extends android.support.v4.view.ActionProvider {
public <init>(android.content.Context);
}
# google-rfc-2445-20110304
-dontwarn com.google.ical.compat.jodatime.**
# https://github.com/JakeWharton/butterknife/blob/581666a28022796fdd62caaf3420e621215abfda/butterknife/proguard-rules.txt
-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
-keep class butterknife.*
-keepclasseswithmembernames class * { @butterknife.* <methods>; }
-keepclasseswithmembernames class * { @butterknife.* <fields>; }
# guava
-dontwarn sun.misc.Unsafe
@ -9,56 +24,6 @@
-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>; }
-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
-dontwarn com.google.errorprone.annotations.concurrent.LazyInit
-dontwarn com.google.errorprone.annotations.ForOverride

@ -1,12 +1,12 @@
{
"formatVersion": 1,
"database": {
"version": 39,
"identityHash": "7e082aef9f43061a29426ac5c9489db0",
"version": 1,
"identityHash": "eab7679fcfaa5fd45ac7da7a4b205348",
"entities": [
{
"tableName": "notification",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `type` INTEGER NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `task` INTEGER, `timestamp` INTEGER NOT NULL, `type` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uid",
@ -18,7 +18,7 @@
"fieldPath": "taskId",
"columnName": "task",
"affinity": "INTEGER",
"notNull": true
"notNull": false
},
{
"fieldPath": "timestamp",
@ -54,7 +54,7 @@
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"7e082aef9f43061a29426ac5c9489db0\")"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"eab7679fcfaa5fd45ac7da7a4b205348\")"
]
}
}

@ -0,0 +1,95 @@
require 'rexml/document'
STRINGS = {}
STRING_ARRAYS = {}
PLURALS = {}
def load(path)
file = File.new(path)
doc = REXML::Document.new(file)
doc.context[:attribute_quote] = :quote
doc
end
def get_items(elem)
items = []
elem.each_element('item') do |item|
items << item.text
end
items
end
def get_plurals(elem)
plurals = {}
elem.each_element('item') do |item|
plurals[item.attributes['quantity']] = item.text
end
plurals
end
def index_elements(doc)
doc.elements['resources'].each_element('string') { |elem| STRINGS[elem.attributes['name']] = elem.text }
doc.elements['resources'].each_element('string-array') { |elem| STRING_ARRAYS[elem.attributes['name']] = get_items(elem) }
doc.elements['resources'].each_element('plurals') { |elem| PLURALS[elem.attributes['name']] = get_plurals(elem) }
end
def find_duplicate_plurals(doc)
dups = []
doc.elements['resources'].each_element('plurals') do |elem|
string_name = elem.attributes['name']
plurals = get_plurals(elem)
dups << string_name if plurals.eql? PLURALS[string_name]
end
dups
end
def find_duplicate_string_arrays(doc)
dups = []
doc.elements['resources'].each_element('string-array') do |elem|
string_name = elem.attributes['name']
items = get_items(elem)
dups << string_name if items.eql? STRING_ARRAYS[string_name]
end
dups
end
def find_duplicate_strings(doc)
dups = []
doc.elements['resources'].each_element('string') do |elem|
string_name = elem.attributes['name']
dups << string_name if elem.text.eql? STRINGS[string_name]
end
dups
end
def remove_items(doc, type, names)
names.each { |name| doc.elements.delete("resources/#{type}[@name='#{name}']") }
end
def clean(path)
doc = load(path)
remove_items(doc, 'string', find_duplicate_strings(doc))
remove_items(doc, 'string-array', find_duplicate_string_arrays(doc))
remove_items(doc, 'plurals', find_duplicate_plurals(doc))
prolog, *tail = doc.to_s.split("\n").reject { |x| x.strip.eql? "" }
File.open(path, 'w') do |f|
f.puts prolog
f.puts "<!-- ************************************************************** -->"
f.puts "<!-- ********* THIS FILE IS GENERATED BY GETLOCALIZATION ********** -->"
f.puts "<!-- ******** http://www.getlocalization.com/tasks_android ******** -->"
f.puts "<!-- ******************* DO NOT MODIFY MANUALLY ******************* -->"
f.puts "<!-- ************************************************************** -->"
f.puts "<!--suppress AndroidLintTypographyEllipsis,AndroidLintTypographyDashes-->"
f.print tail.join("\n")
end
end
def remove_untranslated_strings(*string_files)
Dir.glob("src/main/res/values/strings*.xml").each { |path| index_elements(load(path)) }
string_files.each { |path| clean path }
end
if __FILE__ == $0
lang = ARGV[0]
remove_untranslated_strings("src/main/res/values-#{lang}/strings.xml")
end

@ -0,0 +1,153 @@
#!/usr/bin/env ruby
require 'fileutils'
$:.unshift File.dirname(__FILE__)
require 'clean_translations'
# Script for invoking the GetLocalization tools
# IMPORTANT: Right now, must be invoked from the project's root directory.
# Usage: ./bin/getloc.rb [cmd] [lang]
# cmd: 'export' or 'import'
# lang: Language code or 'master'
PROJECT_NAME='tasks_android'
LANGUAGE_MAP = {
"el" => "grk",
"sk" => "sk-SK",
"hu" => "hu-HU",
"fa" => "pes-IR"
}
# Converts astrid language codes to GetLocalization language codes (which don't use -r)
def astrid_code_to_getloc_code(lang)
(LANGUAGE_MAP[lang] || lang).sub("-r", "-")
end
# Inverse of the above function
def getloc_code_to_astrid_code(lang)
(LANGUAGE_MAP.invert[lang] || lang).sub("-", "-r")
end
# Uploads files for the specified language to GetLocalization
# tmp_files (Array): temporary strings files to use
# lang (String): language code
# src_files_block (lambda): Block for computing the source file list from the language code
def export(tmp_files, lang, src_files_block)
src_files = src_files_block.call(lang)
for i in 0...tmp_files.length
%x(cp #{src_files[i]} #{tmp_files[i]}) if src_files[i] != tmp_files[i]
end
tmp_files.each do |f|
%x(gsed -i "s/\\\\\\'/'/g" #{f})
end
if lang == "master"
tmp_files.each do |f|
puts "Updating master file #{f}"
%x(curl --form file=@#{f} --user "#{@user}:#{@password}" https://api.getlocalization.com/#{PROJECT_NAME}/api/update-master/)
end
else
raise "dont do this if you already exported your translations"
lang_tmp = astrid_code_to_getloc_code(lang)
tmp_files.each do |f|
puts "Updating language file #{f}"
name = File.basename(f)
%x(curl --form file=@#{f} --user "#{@user}:#{@password}" https://api.getlocalization.com/#{PROJECT_NAME}/api/translations/file/#{name}/#{lang_tmp}/)
end
end
end
# Downloads and imports files for the specified language
# tmp_files (Array): temporary strings files to use
# lang (String): language code
# dst_files_block (lambda): Block for computing the destination files list from the language code
def import(tmp_files, lang, dst_files_block)
if lang == "master"
tmp_dir = File.dirname(tmp_files[0])
tmp_all = File.join(tmp_dir, "all.zip")
tmp_all_dir = File.join(tmp_dir, "all")
%x(curl --user "#{@user}:#{@password}" https://api.getlocalization.com/#{PROJECT_NAME}/api/translations/zip/ -o #{tmp_all})
%x(mkdir #{tmp_all_dir})
%x(tar xzf #{tmp_all} -C #{tmp_all_dir})
# Get all translations
Dir.foreach(tmp_all_dir) do |l|
if (l != "." && l != "..")
lang_local = getloc_code_to_astrid_code(l)
dst_files = dst_files_block.call(lang_local)
for i in 0...tmp_files.length
file = File.join(tmp_all_dir, l, File.basename(tmp_files[i]))
%x(gsed -i "s/\\([^\\\\\\]\\)'/\\1\\\\\\'/g" #{file})
puts "Moving #{file} to #{dst_files[i]}"
%x(mv #{file} #{dst_files[i]})
end
end
end
%x(rm -rf #{tmp_all_dir})
%x(rm #{tmp_all})
else
lang_tmp = astrid_code_to_getloc_code(lang)
dst_files = dst_files_block.call(lang)
for i in 0...tmp_files.length
name = File.basename(tmp_files[i])
%x(curl --user "#{@user}:#{@password}" https://api.getlocalization.com/#{PROJECT_NAME}/api/translations/file/#{name}/#{lang_tmp}/ -o #{tmp_files[i]})
%x(gsed -i "s/\\([^\\\\\\]\\)'/\\1\\\\\\'/g" #{tmp_files[i]})
`gsed -i '/\s*<!--.*-->\s*$/d' #{tmp_files[i]}` # strip comments
puts "Moving #{tmp_files[i]} to #{dst_files[i]}"
%x(mv #{tmp_files[i]} #{dst_files[i]})
end
remove_untranslated_strings(*dst_files)
end
end
class Android
def self.tmp_files
FileUtils.mkdir_p "translations"
["translations/strings.xml"]
end
def self.src_files(cmd, type)
if cmd == :export && type == "master"
lambda { |l| ["src/main/res/values/strings.xml"] }
else
lambda { |l| ["src/main/res/values-#{l}/strings.xml"] }
end
end
end
# Main function for invoking the GetLocalization tools
# cmd (String): Command to invoke. Must be 'import' or 'export'
# lang (String): Language code. Can also be 'master' to specify master files for export or all languages for import.
def getloc(cmd, languages)
cmd = cmd.to_sym
raise "must set GETLOC_USER and GETLOC_PASS environment variables" if ENV['GETLOC_USER'].nil? or ENV['GETLOC_PASS'].nil?
@user = ENV['GETLOC_USER']
@password = ENV['GETLOC_PASS']
platform_class = Android
languages.split(',').each do |lang|
case cmd
when :export
puts "Exporting #{lang} files"
export(platform_class.tmp_files, lang, platform_class.src_files(cmd, lang))
when :import
puts "Importing #{lang} files"
import(platform_class.tmp_files, lang, platform_class.src_files(cmd, lang))
else
puts "Command #{cmd} not recognized. Should be one of 'export' or 'import'."
return
end
platform_class.tmp_files.each do |f|
%x(rm -f #{f})
end
end
end
getloc(*ARGV)

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.tasks">
<application>
<!-- Google Analytics -->
<receiver
android:name="com.google.android.gms.analytics.AnalyticsReceiver"
android:enabled="true">
<intent-filter>
<action android:name="com.google.android.gms.analytics.ANALYTICS_DISPATCH" />
</intent-filter>
</receiver>
<service
android:name="com.google.android.gms.analytics.AnalyticsService"
android:enabled="true"
android:exported="false" />
<receiver
android:name="com.google.android.gms.analytics.CampaignTrackingReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.android.vending.INSTALL_REFERRER" />
</intent-filter>
</receiver>
<service android:name="com.google.android.gms.analytics.CampaignTrackingService" />
</application>
</manifest>

@ -0,0 +1,23 @@
package com.todoroo.astrid.gtasks;
import com.todoroo.astrid.api.Filter;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
public class GtasksFilterExposer {
@Inject
public GtasksFilterExposer() {
}
public List<Filter> getFilters() {
return Collections.emptyList();
}
public Filter getFilter(long aLong) {
return null;
}
}

@ -0,0 +1,26 @@
package com.todoroo.astrid.gtasks;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
public class GtasksListService {
@Inject
public GtasksListService() {
}
public List<GtasksList> getLists() {
return Collections.emptyList();
}
public GtasksList getList(long storeId) {
return null;
}
public List<GtasksList> getSortedGtasksList() {
return Collections.emptyList();
}
}

@ -0,0 +1,12 @@
package com.todoroo.astrid.gtasks;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.api.GtasksFilter;
import org.tasks.tasklist.GtasksListFragment;
public class GtasksSubtaskListFragment extends GtasksListFragment {
public static TaskListFragment newGtasksSubtaskListFragment(GtasksFilter gtasksFilter, GtasksList list) {
return null;
}
}

@ -0,0 +1,23 @@
package org.tasks;
import android.accounts.Account;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
public class AccountManager {
@Inject
public AccountManager() {
}
public List<String> getAccounts() {
return Collections.emptyList();
}
public Account getAccount(String userName) {
return null;
}
}

@ -0,0 +1,14 @@
package org.tasks;
import javax.inject.Inject;
public class FlavorSetup {
@Inject
public FlavorSetup() {
}
public void setup() {
}
}

@ -0,0 +1,4 @@
package org.tasks.activities;
public class GoogleTaskListSettingsActivity {
}

@ -0,0 +1,97 @@
package org.tasks.analytics;
import android.content.Context;
import com.google.android.gms.analytics.ExceptionParser;
import com.google.android.gms.analytics.ExceptionReporter;
import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.StandardExceptionParser;
import com.google.common.base.Strings;
import org.tasks.BuildConfig;
import org.tasks.R;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication;
import javax.inject.Inject;
import timber.log.Timber;
@ApplicationScope
public class Tracker {
private final GoogleAnalytics analytics;
private final com.google.android.gms.analytics.Tracker tracker;
private final ExceptionParser exceptionParser;
private final Context context;
@Inject
public Tracker(@ForApplication Context context) {
this.context = context;
analytics = GoogleAnalytics.getInstance(context);
tracker = analytics.newTracker(R.xml.google_analytics);
tracker.setAppVersion(Integer.toString(BuildConfig.VERSION_CODE));
final StandardExceptionParser standardExceptionParser = new StandardExceptionParser(context, null);
exceptionParser = (thread, throwable) -> {
StringBuilder stack = new StringBuilder()
.append(standardExceptionParser.getDescription(thread, throwable))
.append("\n")
.append(throwable.getClass().getName())
.append("\n");
for (StackTraceElement element : throwable.getStackTrace()) {
stack.append(element.toString())
.append("\n");
}
return stack.toString();
};
ExceptionReporter reporter = new ExceptionReporter(
tracker,
Thread.getDefaultUncaughtExceptionHandler(),
context);
reporter.setExceptionParser(exceptionParser);
Thread.setDefaultUncaughtExceptionHandler(reporter);
}
public void setTrackingEnabled(boolean enabled) {
analytics.setAppOptOut(!enabled);
}
public void reportException(Throwable t) {
reportException(Thread.currentThread(), t);
}
public void reportException(Thread thread, Throwable t) {
Timber.e(t, t.getMessage());
tracker.send(new HitBuilders.ExceptionBuilder()
.setDescription(exceptionParser.getDescription(thread.getName(), t))
.setFatal(false)
.build());
}
public void reportEvent(Tracking.Events event) {
reportEvent(event, null);
}
public void reportEvent(Tracking.Events event, String label) {
reportEvent(event, event.action, label);
}
public void reportEvent(Tracking.Events event, int action, String label) {
reportEvent(event, context.getString(action), label);
}
public void reportEvent(Tracking.Events event, String action, String label) {
reportEvent(event.category, action, label);
}
private void reportEvent(int category, String action, String label) {
HitBuilders.EventBuilder eventBuilder = new HitBuilders.EventBuilder()
.setCategory(context.getString(category))
.setAction(action);
if (!Strings.isNullOrEmpty(label)) {
eventBuilder.setLabel(label);
}
tracker.send(eventBuilder.build());
}
}

@ -0,0 +1,32 @@
package org.tasks.billing;
import android.app.Activity;
import android.content.Intent;
import javax.inject.Inject;
public class PurchaseHelper {
@Inject
public PurchaseHelper() {
}
public boolean purchase(final Activity activity,
final String sku, final String pref,
final int requestCode, final PurchaseHelperCallback callback) {
callback.purchaseCompleted(false, sku);
return false;
}
public void handleActivityResult(PurchaseHelperCallback callback, int requestCode, int resultCode, Intent data) {
}
public void disposeIabHelper() {
}
public void consumePurchases() {
}
}

@ -0,0 +1,32 @@
package org.tasks.gtasks;
import com.todoroo.astrid.activity.TaskListFragment;
import javax.inject.Inject;
public class SyncAdapterHelper {
@Inject
public SyncAdapterHelper() {
}
public boolean shouldShowBackgroundSyncWarning() {
return false;
}
public void checkPlayServices(TaskListFragment taskListFragment) {
}
public boolean initiateManualSync() {
return false;
}
public boolean isEnabled() {
return false;
}
public void requestSynchronization() {
}
}

@ -0,0 +1,123 @@
package org.tasks.injection;
import com.todoroo.astrid.activity.BeastModePreferences;
import com.todoroo.astrid.activity.ShareLinkActivity;
import com.todoroo.astrid.activity.TaskListActivity;
import com.todoroo.astrid.core.CustomFilterActivity;
import com.todoroo.astrid.core.DefaultsPreferences;
import com.todoroo.astrid.core.OldTaskPreferences;
import com.todoroo.astrid.files.AACRecordingActivity;
import com.todoroo.astrid.gcal.CalendarReminderActivity;
import com.todoroo.astrid.reminders.ReminderPreferences;
import org.tasks.activities.AddAttachmentActivity;
import org.tasks.activities.CalendarSelectionActivity;
import org.tasks.activities.CameraActivity;
import org.tasks.activities.ColorPickerActivity;
import org.tasks.activities.DateAndTimePickerActivity;
import org.tasks.activities.DatePickerActivity;
import org.tasks.activities.FilterSelectionActivity;
import org.tasks.activities.FilterSettingsActivity;
import org.tasks.activities.GoogleTaskListSettingsActivity;
import org.tasks.activities.TagSettingsActivity;
import org.tasks.activities.TimePickerActivity;
import org.tasks.dashclock.DashClockSettings;
import org.tasks.files.FileExplore;
import org.tasks.files.MyFilePickerActivity;
import org.tasks.locale.ui.activity.TaskerSettingsActivity;
import org.tasks.preferences.AppearancePreferences;
import org.tasks.preferences.BasicPreferences;
import org.tasks.preferences.DateTimePreferences;
import org.tasks.preferences.HelpAndFeedbackActivity;
import org.tasks.preferences.MiscellaneousPreferences;
import org.tasks.reminders.MissedCallActivity;
import org.tasks.reminders.NotificationActivity;
import org.tasks.reminders.SnoozeActivity;
import org.tasks.themes.Theme;
import org.tasks.voice.VoiceCommandActivity;
import org.tasks.widget.ShortcutConfigActivity;
import org.tasks.widget.WidgetConfigActivity;
import dagger.Subcomponent;
@ActivityScope
@Subcomponent(modules = ActivityModule.class)
public interface ActivityComponent {
Theme getTheme();
FragmentComponent plus(FragmentModule module);
DialogFragmentComponent plus(DialogFragmentModule dialogFragmentModule);
NativeDialogFragmentComponent plus(NativeDialogFragmentModule nativeDialogFragmentModule);
void inject(TaskerSettingsActivity taskerSettingsActivity);
void inject(DashClockSettings dashClockSettings);
void inject(AACRecordingActivity aacRecordingActivity);
void inject(CustomFilterActivity customFilterActivity);
void inject(CalendarReminderActivity calendarReminderActivity);
void inject(FilterSettingsActivity filterSettingsActivity);
void inject(TagSettingsActivity tagSettingsActivity);
void inject(ShareLinkActivity shareLinkActivity);
void inject(TaskListActivity taskListActivity);
void inject(BeastModePreferences beastModePreferences);
void inject(NotificationActivity notificationActivity);
void inject(SnoozeActivity snoozeActivity);
void inject(MissedCallActivity missedCallActivity);
void inject(FileExplore fileExplore);
void inject(CalendarSelectionActivity calendarSelectionActivity);
void inject(FilterSelectionActivity filterSelectionActivity);
void inject(DateAndTimePickerActivity dateAndTimePickerActivity);
void inject(AddAttachmentActivity addAttachmentActivity);
void inject(DatePickerActivity datePickerActivity);
void inject(CameraActivity cameraActivity);
void inject(TimePickerActivity timePickerActivity);
void inject(VoiceCommandActivity voiceCommandActivity);
void inject(ReminderPreferences reminderPreferences);
void inject(WidgetConfigActivity widgetConfigActivity);
void inject(OldTaskPreferences oldTaskPreferences);
void inject(DefaultsPreferences defaultsPreferences);
void inject(ShortcutConfigActivity shortcutConfigActivity);
void inject(MiscellaneousPreferences miscellaneousPreferences);
void inject(HelpAndFeedbackActivity helpAndFeedbackActivity);
void inject(DateTimePreferences dateTimePreferences);
void inject(AppearancePreferences appearancePreferences);
void inject(MyFilePickerActivity myFilePickerActivity);
void inject(ColorPickerActivity colorPickerActivity);
void inject(BasicPreferences basicPreferences);
void inject(GoogleTaskListSettingsActivity googleTaskListSettingsActivity);
}

@ -0,0 +1,23 @@
package org.tasks.injection;
import org.tasks.Tasks;
import org.tasks.dashclock.DashClockExtension;
import org.tasks.widget.ScrollableWidgetUpdateService;
import dagger.Component;
@ApplicationScope
@Component(modules = ApplicationModule.class)
public interface ApplicationComponent {
void inject(DashClockExtension dashClockExtension);
void inject(Tasks tasks);
void inject(ScrollableWidgetUpdateService scrollableWidgetUpdateService);
ActivityComponent plus(ActivityModule module);
BroadcastComponent plus(BroadcastModule module);
IntentServiceComponent plus(IntentServiceModule module);
}

@ -0,0 +1,31 @@
package org.tasks.injection;
import org.tasks.activities.CalendarSelectionDialog;
import org.tasks.dialogs.AddAttachmentDialog;
import org.tasks.dialogs.ColorPickerDialog;
import org.tasks.dialogs.RecordAudioDialog;
import org.tasks.dialogs.SortDialog;
import org.tasks.reminders.MissedCallDialog;
import org.tasks.reminders.NotificationDialog;
import org.tasks.reminders.SnoozeDialog;
import dagger.Subcomponent;
@Subcomponent(modules = DialogFragmentModule.class)
public interface DialogFragmentComponent {
void inject(NotificationDialog notificationDialog);
void inject(MissedCallDialog missedCallDialog);
void inject(CalendarSelectionDialog calendarSelectionDialog);
void inject(AddAttachmentDialog addAttachmentDialog);
void inject(SnoozeDialog snoozeDialog);
void inject(SortDialog sortDialog);
void inject(ColorPickerDialog colorPickerDialog);
void inject(RecordAudioDialog recordAudioDialog);
}

@ -0,0 +1,28 @@
package org.tasks.injection;
import org.tasks.dialogs.DonationDialog;
import org.tasks.dialogs.ExportTasksDialog;
import org.tasks.dialogs.ImportTasksDialog;
import org.tasks.dialogs.NativeDatePickerDialog;
import org.tasks.dialogs.NativeTimePickerDialog;
import org.tasks.dialogs.SeekBarDialog;
import org.tasks.locale.LocalePickerDialog;
import dagger.Subcomponent;
@Subcomponent(modules = NativeDialogFragmentModule.class)
public interface NativeDialogFragmentComponent {
void inject(LocalePickerDialog localePickerDialog);
void inject(NativeDatePickerDialog nativeDatePickerDialog);
void inject(NativeTimePickerDialog nativeTimePickerDialog);
void inject(SeekBarDialog seekBarDialog);
void inject(ExportTasksDialog exportTasksDialog);
void inject(ImportTasksDialog importTasksDialog);
void inject(DonationDialog donationDialog);
}

@ -0,0 +1,26 @@
package org.tasks.location;
import java.util.List;
import javax.inject.Inject;
@SuppressWarnings("EmptyMethod")
public class GeofenceApi {
@Inject
public GeofenceApi() {
}
public void register(List<Geofence> activeGeofences) {
}
public void cancel(Geofence geofence) {
}
public void cancel(List<Geofence> geofences) {
}
}

@ -0,0 +1,5 @@
package org.tasks.location;
public class GeofenceTransitionsIntentService {
}

@ -0,0 +1,17 @@
package org.tasks.location;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import org.tasks.preferences.Preferences;
public class PlacePicker {
public static Intent getIntent(Activity activity) {
return null;
}
public static Geofence getPlace(Context context, Intent data, Preferences preferences) {
return null;
}
}

@ -0,0 +1,27 @@
package org.tasks.receivers;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.data.Task;
import org.tasks.injection.BroadcastComponent;
import org.tasks.injection.InjectingBroadcastReceiver;
public class PushReceiver extends InjectingBroadcastReceiver {
public static void broadcast(Context context, Task task, ContentValues values) {
}
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
}
@Override
protected void inject(BroadcastComponent component) {
component.inject(this);
}
}

@ -0,0 +1,11 @@
package org.tasks.tasklist;
import com.todoroo.astrid.activity.TaskListFragment;
import com.todoroo.astrid.api.GtasksFilter;
import com.todoroo.astrid.gtasks.GtasksList;
public class GtasksListFragment extends TaskListFragment {
public static TaskListFragment newGtasksListFragment(GtasksFilter gtasksFilter, GtasksList list) {
return null;
}
}

@ -0,0 +1,45 @@
package org.tasks.ui;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gtasks.GtasksList;
import org.tasks.R;
import org.tasks.injection.FragmentComponent;
public class GoogleTaskListFragment extends TaskEditControlFragment {
public static final int TAG = R.string.TEA_ctrl_google_task_list;
@Override
protected int getLayout() {
return 0;
}
@Override
protected int getIcon() {
return 0;
}
@Override
public int controlId() {
return 0;
}
@Override
public void initialize(boolean isNewTask, Task task) {
}
@Override
public void apply(Task task) {
}
@Override
protected void inject(FragmentComponent component) {
}
public void setList(GtasksList list) {
}
}

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="sku_themes">themes</string>
</resources>

@ -0,0 +1,262 @@
/**
* 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 android.support.test.runner.AndroidJUnit4;
import com.todoroo.andlib.data.Callback;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.R;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
/**
* 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@todoroo.com>
*
*/
@RunWith(AndroidJUnit4.class)
public class TranslationTests {
/**
* Loop through each locale and call runnable
*/
private void forEachLocale(Callback<Resources> callback) {
Locale[] locales = Locale.getAvailableLocales();
for(Locale locale : locales) {
callback.apply(getResourcesForLocale(locale));
}
}
private Resources getResourcesForLocale(Locale locale) {
Resources resources = getTargetContext().getResources();
Configuration configuration = new Configuration(resources.getConfiguration());
configuration.locale = locale;
return new Resources(resources.getAssets(), resources.getDisplayMetrics(), configuration);
}
private static final class FormatStringData {
private static final char[] scratch = new char[10];
/** format characters */
public final char[] characters;
/** the original string */
public final String string;
public FormatStringData(String string) {
this.string = string;
int pos = -1;
int count = 0;
while(true) {
pos = string.indexOf('%', ++pos);
if(pos++ == -1)
break;
if(pos >= string.length())
scratch[count++] = '\0';
else
scratch[count++] = string.charAt(pos);
}
characters = new char[count];
for(int i = 0; i < count; i++) {
characters[i] = scratch[i];
}
}
/** test that the characters match */
public boolean matches(FormatStringData other) {
if(characters.length != other.characters.length)
return false;
outer: for(int i = 0; i < characters.length; i++) {
if(Character.isDigit(characters[i])) {
for(int j = 0; j < other.characters.length; j++)
if(characters[i] == other.characters[j])
break outer;
return false;
} else if(characters[i] != other.characters[i])
return false;
}
return true;
}
@Override
public String toString() {
StringBuilder value = new StringBuilder("[");
for(int i = 0; i < characters.length; i++) {
value.append(characters[i]);
if(i < characters.length - 1)
value.append(',');
}
value.append("]: '").append(string).append('\'');
return value.toString();
}
}
/**
* Internal test of format string parser
*/
@Test
public void testFormatStringParser() {
String s = "abc";
FormatStringData data = new FormatStringData(s);
assertEquals(s, data.string);
assertEquals(0, data.characters.length);
s = "abc %s def";
data = new FormatStringData(s);
assertEquals(1, data.characters.length);
assertEquals('s', data.characters[0]);
s = "abc %%s def %d";
data = new FormatStringData(s);
assertEquals(2, data.characters.length);
assertEquals('%', data.characters[0]);
assertEquals('d', data.characters[1]);
assertTrue(data.toString(), data.toString().contains("[%"));
assertTrue(data.toString(), data.toString().contains("d]"));
assertTrue(data.toString(), data.toString().contains(s));
assertTrue(data.matches(new FormatStringData("espanol %% und %d si")));
assertFalse(data.matches(new FormatStringData("ingles %d ja %% pon")));
s = "% abc %";
data = new FormatStringData(s);
assertEquals(2, data.characters.length);
assertEquals(' ', data.characters[0]);
assertEquals('\0', data.characters[1]);
}
/**
* Test that the format specifiers in translations match exactly the
* translations in the default text
*/
@Test
public void testFormatStringsMatch() throws Exception {
final Resources resources = getTargetContext().getResources();
final int[] strings = getResourceIds(R.string.class);
final FormatStringData[] formatStrings = new FormatStringData[strings.length];
final StringBuilder failures = new StringBuilder();
for(int i = 0; i < strings.length; i++) {
try {
String string = resources.getString(strings[i]);
formatStrings[i] = new FormatStringData(string);
} catch (Exception e) {
String name = resources.getResourceName(strings[i]);
failures.append(String.format("error opening %s: %s\n",
name, e.getMessage()));
}
}
forEachLocale(r -> {
Locale locale = r.getConfiguration().locale;
for(int i = 0; i < strings.length; i++) {
try {
switch(strings[i]) {
case R.string.abc_shareactionprovider_share_with_application:
continue;
}
String string = r.getString(strings[i]);
FormatStringData newFS = new FormatStringData(string);
if(!newFS.matches(formatStrings[i])) {
String name = r.getResourceName(strings[i]);
failures.append(String.format("%s (%s): %s != %s\n",
name, locale.toString(), newFS, formatStrings[i]));
}
} catch (Exception e) {
String name = r.getResourceName(strings[i]);
failures.append(String.format("%s: error opening %s: %s\n",
locale.toString(), name, e.getMessage()));
}
}
});
assertTrue(failures.toString(), errorCount(failures) == 0);
}
/**
* check if string contains contains substrings
*/
private void contains(Resources r, int resource, StringBuilder failures, String expected) {
String translation = r.getString(resource);
if(!translation.contains(expected)) {
Locale locale = r.getConfiguration().locale;
String name = r.getResourceName(resource);
failures.append(String.format("%s: %s did not contain: %s\n",
locale.toString(), name, expected));
}
}
/**
* Test dollar sign resources
*/
@Test
public void testSpecialStringsMatch() throws Exception {
final StringBuilder failures = new StringBuilder();
forEachLocale(r -> {
contains(r, R.string.CFC_tag_text, failures, "?");
contains(r, R.string.CFC_title_contains_text, failures, "?");
contains(r, R.string.CFC_dueBefore_text, failures, "?");
contains(r, R.string.CFC_tag_contains_text, failures, "?");
contains(r, R.string.CFC_gtasks_list_text, failures, "?");
});
assertEquals(failures.toString(), 0,
failures.toString().replaceAll("[^\n]", "").length());
}
/**
* Count newlines
*/
private int errorCount(StringBuilder failures) {
int count = 0;
int pos = -1;
while(true) {
pos = failures.indexOf("\n", pos + 1);
if(pos == -1)
return count;
count++;
}
}
/**
* @return an array of all string resource id's
*/
private int[] getResourceIds(Class<?> resources) {
Field[] fields = resources.getDeclaredFields();
List<Integer> ids = new ArrayList<>(fields.length);
for (Field field : fields) {
try {
ids.add(field.getInt(null));
} catch (Exception e) {
// not a field we care about
}
}
int[] idsAsIntArray = new int[ids.size()];
for(int i = 0; i < ids.size(); i++)
idsAsIntArray[i] = ids.get(i);
return idsAsIntArray;
}
}

@ -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)
}
}

@ -0,0 +1,233 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.andlib.utility;
import android.content.res.Configuration;
import android.support.test.runner.AndroidJUnit4;
import android.util.DisplayMetrics;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.Snippet;
import org.tasks.time.DateTime;
import java.util.Locale;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static com.todoroo.andlib.utility.DateUtilities.addCalendarMonthsToUnixtime;
import static com.todoroo.andlib.utility.DateUtilities.getDateString;
import static com.todoroo.andlib.utility.DateUtilities.getStartOfDay;
import static com.todoroo.andlib.utility.DateUtilities.getTimeString;
import static com.todoroo.andlib.utility.DateUtilities.getWeekday;
import static com.todoroo.andlib.utility.DateUtilities.getWeekdayShort;
import static com.todoroo.andlib.utility.DateUtilities.oneMonthFromNow;
import static junit.framework.Assert.assertEquals;
import static org.tasks.Freeze.freezeAt;
import static org.tasks.date.DateTimeUtils.newDate;
import static org.tasks.date.DateTimeUtils.newDateTime;
@RunWith(AndroidJUnit4.class)
public class DateUtilitiesTest {
private static Locale defaultLocale;
@Before
public void setUp() {
defaultLocale = Locale.getDefault();
Locale.setDefault(Locale.US);
}
@After
public void tearDown() {
DateUtilities.is24HourOverride = null;
Locale.setDefault(defaultLocale);
}
private void setLocale(Locale locale) {
Locale.setDefault(locale);
Configuration config = new Configuration();
config.locale = locale;
DisplayMetrics metrics = getTargetContext().getResources().getDisplayMetrics();
getTargetContext().getResources().updateConfiguration(config, metrics);
}
public void forEachLocale(Runnable r) {
Locale[] locales = Locale.getAvailableLocales();
for(Locale locale : locales) {
setLocale(locale);
r.run();
}
}
@Test
public void testTimeString() {
forEachLocale(() -> {
DateTime d = newDateTime();
DateUtilities.is24HourOverride = false;
for (int i = 0; i < 24; i++) {
d = d.withHourOfDay(i);
getTimeString(getTargetContext(), d);
}
DateUtilities.is24HourOverride = true;
for (int i = 0; i < 24; i++) {
d = d.withHourOfDay(i);
getTimeString(getTargetContext(), d);
}
});
}
@Test
public void testDateString() {
forEachLocale(() -> {
DateTime d = newDateTime();
for (int i = 0; i < 12; i++) {
d = d.withMonthOfYear(i);
getDateString(d);
}
});
}
@Test
public void testGet24HourTime() {
DateUtilities.is24HourOverride = true;
assertEquals("09:05", getTimeString(null, new DateTime(2014, 1, 4, 9, 5, 36)));
assertEquals("13:00", getTimeString(null, new DateTime(2014, 1, 4, 13, 0, 1)));
}
@Test
public void testGetTime() {
DateUtilities.is24HourOverride = false;
assertEquals("9:05 AM", getTimeString(null, new DateTime(2014, 1, 4, 9, 5, 36)));
assertEquals("1:05 PM", getTimeString(null, new DateTime(2014, 1, 4, 13, 5, 36)));
}
@Test
public void testGetTimeWithNoMinutes() {
DateUtilities.is24HourOverride = false;
assertEquals("1 PM", getTimeString(null, new DateTime(2014, 1, 4, 13, 0, 59))); // derp?
}
@Test
public void testGetDateStringWithYear() {
assertEquals("Jan 4 '14", getDateString(new DateTime(2014, 1, 4, 0, 0, 0)));
}
@Test
public void testGetDateStringHidingYear() {
freezeAt(newDate(2014, 1, 1)).thawAfter(new Snippet() {{
assertEquals("Jan 1", getDateString(newDateTime()));
}});
}
@Test
public void testGetDateStringWithDifferentYear() {
freezeAt(newDate(2013, 12, 31)).thawAfter(new Snippet() {{
assertEquals("Jan 1 '14", getDateString(new DateTime(2014, 1, 1, 0, 0, 0)));
}});
}
@Test
public void testOneMonthFromStartOfDecember() {
DateTime now = new DateTime(2013, 12, 1, 12, 19, 45, 192);
final long expected = new DateTime(2014, 1, 1, 12, 19, 45, 192).getMillis();
freezeAt(now).thawAfter(new Snippet() {{
assertEquals(expected, oneMonthFromNow());
}});
}
@Test
public void testOneMonthFromEndOfDecember() {
DateTime now = new DateTime(2013, 12, 31, 16, 31, 20, 597);
final long expected = new DateTime(2014, 1, 31, 16, 31, 20, 597).getMillis();
freezeAt(now).thawAfter(new Snippet() {{
assertEquals(expected, oneMonthFromNow());
}});
}
@Test
public void testGetSixMonthsFromEndOfDecember() {
final DateTime now = new DateTime(2013, 12, 31, 17, 17, 32, 900);
final long expected = new DateTime(2014, 7, 1, 17, 17, 32, 900).getMillis();
freezeAt(now).thawAfter(new Snippet() {{
assertEquals(expected, addCalendarMonthsToUnixtime(now.getMillis(), 6));
}});
}
@Test
public void testOneMonthFromEndOfJanuary() {
DateTime now = new DateTime(2014, 1, 31, 12, 54, 33, 175);
final long expected = new DateTime(2014, 3, 3, 12, 54, 33, 175).getMillis();
freezeAt(now).thawAfter(new Snippet() {{
assertEquals(expected, oneMonthFromNow());
}});
}
@Test
public void testOneMonthFromEndOfFebruary() {
DateTime now = new DateTime(2014, 2, 28, 9, 19, 7, 990);
final long expected = new DateTime(2014, 3, 28, 9, 19, 7, 990).getMillis();
freezeAt(now).thawAfter(new Snippet() {{
assertEquals(expected, oneMonthFromNow());
}});
}
@Test
public void testShouldGetStartOfDay() {
DateTime now = new DateTime(2014, 1, 3, 10, 41, 41, 520);
assertEquals(
now.startOfDay().getMillis(),
getStartOfDay(now.getMillis()));
}
@Test
public void testGetWeekdayLongString() {
assertEquals("Sunday", getWeekday(newDate(2013, 12, 29)));
assertEquals("Monday", getWeekday(newDate(2013, 12, 30)));
assertEquals("Tuesday", getWeekday(newDate(2013, 12, 31)));
assertEquals("Wednesday", getWeekday(newDate(2014, 1, 1)));
assertEquals("Thursday", getWeekday(newDate(2014, 1, 2)));
assertEquals("Friday", getWeekday(newDate(2014, 1, 3)));
assertEquals("Saturday", getWeekday(newDate(2014, 1, 4)));
}
@Test
public void testGetWeekdayShortString() {
assertEquals("Sun", getWeekdayShort(newDate(2013, 12, 29)));
assertEquals("Mon", getWeekdayShort(newDate(2013, 12, 30)));
assertEquals("Tue", getWeekdayShort(newDate(2013, 12, 31)));
assertEquals("Wed", getWeekdayShort(newDate(2014, 1, 1)));
assertEquals("Thu", getWeekdayShort(newDate(2014, 1, 2)));
assertEquals("Fri", getWeekdayShort(newDate(2014, 1, 3)));
assertEquals("Sat", getWeekdayShort(newDate(2014, 1, 4)));
}
@Test
public void testAddMonthsToTimestamp() {
assertEquals(newDate(2014, 1, 1).getMillis(), addCalendarMonthsToUnixtime(newDate(2013, 12, 1).getMillis(), 1));
assertEquals(newDate(2014, 12, 31).getMillis(), addCalendarMonthsToUnixtime(newDate(2013, 12, 31).getMillis(), 12));
}
@Test
public void testAddMonthsWithLessDays() {
assertEquals(newDate(2014, 3, 3).getMillis(), addCalendarMonthsToUnixtime(newDate(2013, 12, 31).getMillis(), 2));
}
@Test
public void testAddMonthsWithMoreDays() {
assertEquals(newDate(2014, 1, 30).getMillis(), addCalendarMonthsToUnixtime(newDate(2013, 11, 30).getMillis(), 2));
}
}

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

@ -0,0 +1,79 @@
package com.todoroo.andlib.utility;
import android.support.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.time.DateTime;
import java.util.Locale;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static com.todoroo.andlib.utility.DateUtilities.getRelativeDay;
import static junit.framework.Assert.assertEquals;
import static org.tasks.Freeze.freezeAt;
import static org.tasks.Freeze.thaw;
@RunWith(AndroidJUnit4.class)
public class RelativeDayTest {
private static Locale defaultLocale;
private static final DateTime now = new DateTime(2013, 12, 31, 11, 9, 42, 357);
@Before
public void setUp() {
defaultLocale = Locale.getDefault();
Locale.setDefault(Locale.US);
freezeAt(now);
}
@After
public void tearDown() {
Locale.setDefault(defaultLocale);
thaw();
}
@Test
public void testRelativeDayIsToday() {
checkRelativeDay(new DateTime(), "Today", "Today");
}
@Test
public void testRelativeDayIsTomorrow() {
checkRelativeDay(new DateTime().plusDays(1), "Tomorrow", "Tmrw");
}
@Test
public void testRelativeDayIsYesterday() {
checkRelativeDay(new DateTime().minusDays(1), "Yesterday", "Yest");
}
@Test
public void testRelativeDayTwo() {
checkRelativeDay(new DateTime().minusDays(2), "Sunday", "Sun");
checkRelativeDay(new DateTime().plusDays(2), "Thursday", "Thu");
}
@Test
public void testRelativeDaySix() {
checkRelativeDay(new DateTime().minusDays(6), "Wednesday", "Wed");
checkRelativeDay(new DateTime().plusDays(6), "Monday", "Mon");
}
@Test
public void testRelativeDayOneWeek() {
checkRelativeDay(new DateTime().minusDays(7), "Dec 24", "Dec 24");
}
@Test
public void testRelativeDayOneWeekNextYear() {
checkRelativeDay(new DateTime().plusDays(7), "Jan 7 '14", "Jan 7 '14");
}
private void checkRelativeDay(DateTime now, String full, String abbreviated) {
assertEquals(full, getRelativeDay(getTargetContext(), now.getMillis(), false));
assertEquals(abbreviated, getRelativeDay(getTargetContext(), now.getMillis(), true));
}
}

@ -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)
}
}

@ -0,0 +1,169 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.dao;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.sql.Query;
import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.test.DatabaseTestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.injection.TestComponent;
import java.util.List;
import javax.inject.Inject;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNotSame;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
@RunWith(AndroidJUnit4.class)
public class MetadataDaoTests extends DatabaseTestCase {
@Inject MetadataDao metadataDao;
@Inject TaskDao taskDao;
private Metadata metadata;
public static Property<?>[] KEYS = new Property<?>[] { Metadata.ID, Metadata.KEY };
@Override
public void setUp() {
super.setUp();
metadata = new Metadata();
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
/**
* Test basic creation, fetch, and save
*/
@Test
public void testCrud() throws Exception {
assertTrue(metadataDao.toList(Query.select(Metadata.ID)).isEmpty());
// create "happy"
Metadata metadata = new Metadata();
metadata.setTask(1L);
metadata.setKey("happy");
assertTrue(metadataDao.persist(metadata));
assertEquals(1, metadataDao.toList(Query.select(Metadata.ID)).size());
long happyId = metadata.getId();
assertNotSame(Metadata.NO_ID, happyId);
metadata = metadataDao.fetch(happyId, KEYS);
assertEquals("happy", metadata.getKey());
// create "sad"
metadata = new Metadata();
metadata.setTask(1L);
metadata.setKey("sad");
assertTrue(metadataDao.persist(metadata));
assertEquals(2, metadataDao.toList(Query.select(Metadata.ID)).size());
// rename sad to melancholy
long sadId = metadata.getId();
assertNotSame(Metadata.NO_ID, sadId);
metadata.setKey("melancholy");
assertTrue(metadataDao.persist(metadata));
assertEquals(2, metadataDao.toList(Query.select(Metadata.ID)).size());
// check state
metadata = metadataDao.fetch(happyId, KEYS);
assertEquals("happy", metadata.getKey());
metadata = metadataDao.fetch(sadId, KEYS);
assertEquals("melancholy", metadata.getKey());
// delete sad
assertTrue(metadataDao.delete(sadId));
List<Metadata> metadataList = metadataDao.toList(Query.select(KEYS));
assertEquals(1, metadataList.size());
assertEquals("happy", metadataList.get(0).getKey());
}
/**
* Test metadata bound to task
*/
@Test
public void testMetadataConditions() {
// create "happy"
Metadata metadata = new Metadata();
metadata.setKey("with1");
metadata.setTask(1L);
assertTrue(metadataDao.persist(metadata));
metadata = new Metadata();
metadata.setKey("with2");
metadata.setTask(2L);
assertTrue(metadataDao.persist(metadata));
metadata = new Metadata();
metadata.setKey("with1");
metadata.setTask(1L);
assertTrue(metadataDao.persist(metadata));
List<Metadata> metadataList = metadataDao.toList(Query.select(KEYS).where(MetadataCriteria.byTask(1)));
assertEquals(2, metadataList.size());
assertEquals("with1", metadataList.get(0).getKey());
assertEquals("with1", metadataList.get(1).getKey());
assertTrue(metadataDao.toList(Query.select(KEYS).where(MetadataCriteria.byTask(3))).isEmpty());
assertEquals(2, metadataDao.deleteWhere(MetadataCriteria.byTask(1)));
assertEquals(1, metadataDao.toList(Query.select(KEYS)).size());
}
@Test
public void testDontSaveMetadataWithoutTaskId() {
try {
metadataDao.persist(metadata);
fail("expected exception");
} catch(IllegalArgumentException e) {
assertTrue(e.getMessage().startsWith("metadata needs to be attached to a task"));
}
}
@Test
public void testSaveMetadata() {
metadata.setTask(1L);
metadataDao.persist(metadata);
assertNotNull(metadataDao.fetch(metadata.getId()));
}
@Test
public void testDontDeleteValidMetadata() {
final Task task = new Task();
taskDao.save(task);
metadata.setTask(task.getId());
metadataDao.persist(metadata);
metadataDao.removeDanglingMetadata();
assertNotNull(metadataDao.fetch(metadata.getId()));
}
@Test
public void testDeleteDangling() {
metadata.setTask(1L);
metadataDao.persist(metadata);
metadataDao.removeDanglingMetadata();
assertNull(metadataDao.fetch(1));
}
}

@ -0,0 +1,184 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.dao;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.sql.Query;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.test.DatabaseTestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.injection.TestComponent;
import java.util.List;
import javax.inject.Inject;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotSame;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class TaskDaoTests extends DatabaseTestCase {
public static Property<?>[] IDS = new Property<?>[] { Task.ID };
public static Property<?>[] TITLES = new Property<?>[] { Task.ID,
Task.TITLE };
@Inject TaskDao taskDao;
/**
* Test basic task creation, fetch, and save
*/
@Test
public void testTaskCreation() {
assertEquals(0, taskDao.toList(Query.select(IDS)).size());
// create task "happy"
Task task = new Task();
task.setTitle("happy");
taskDao.save(task);
assertEquals(1, taskDao.toList(Query.select(IDS)).size());
long happyId = task.getId();
assertNotSame(Task.NO_ID, happyId);
task = taskDao.fetch(happyId, TITLES);
assertEquals("happy", task.getTitle());
// create task "sad"
task = new Task();
task.setTitle("sad");
taskDao.save(task);
assertEquals(2, taskDao.toList(Query.select(IDS)).size());
// rename sad to melancholy
long sadId = task.getId();
assertNotSame(Task.NO_ID, sadId);
task.setTitle("melancholy");
taskDao.save(task);
assertEquals(2, taskDao.toList(Query.select(IDS)).size());
// check state
task = taskDao.fetch(happyId, TITLES);
assertEquals("happy", task.getTitle());
task = taskDao.fetch(sadId,TITLES);
assertEquals("melancholy", task.getTitle());
}
/**
* Test various task fetch conditions
*/
@Test
public void testTaskConditions() {
// create normal task
Task task = new Task();
task.setTitle("normal");
taskDao.save(task);
// create blank task
task = new Task();
task.setTitle("");
taskDao.save(task);
// create hidden task
task = new Task();
task.setTitle("hidden");
task.setHideUntil(DateUtilities.now() + 10000);
taskDao.save(task);
// create task with deadlines
task = new Task();
task.setTitle("deadlineInFuture");
task.setDueDate(DateUtilities.now() + 10000);
taskDao.save(task);
task = new Task();
task.setTitle("deadlineInPast");
task.setDueDate(DateUtilities.now() - 10000);
taskDao.save(task);
// create completed task
task = new Task();
task.setTitle("completed");
task.setCompletionDate(DateUtilities.now() - 10000);
taskDao.save(task);
// check has no name
List<Task> tasks = taskDao.toList(Query.select(TITLES).where(TaskCriteria.hasNoTitle()));
assertEquals(1, tasks.size());
assertEquals("", tasks.get(0).getTitle());
// check is active
assertEquals(5, taskDao.toList(Query.select(TITLES).where(TaskCriteria.isActive())).size());
// check is visible
assertEquals(5, taskDao.toList(Query.select(TITLES).where(TaskCriteria.isVisible())).size());
}
/**
* Test task deletion
*/
@Test
public void testTDeletion() {
assertEquals(0, taskDao.toList(Query.select(IDS)).size());
// create task "happy"
Task task = new Task();
task.setTitle("happy");
taskDao.save(task);
assertEquals(1, taskDao.toList(Query.select(IDS)).size());
// delete
long happyId = task.getId();
assertTrue(taskDao.delete(happyId));
assertEquals(0, taskDao.toList(Query.select(IDS)).size());
}
/**
* Test save without prior create doesn't work
*/
@Test
public void testSaveWithoutCreate() {
// try to save task "happy"
Task task = new Task();
task.setTitle("happy");
task.setID(1L);
taskDao.save(task);
assertEquals(0, taskDao.toList(Query.select(IDS)).size());
}
/**
* Test passing invalid task indices to various things
*/
@Test
public void testInvalidIndex() {
assertEquals(0, taskDao.toList(Query.select(IDS)).size());
assertNull(taskDao.fetch(1, IDS));
assertFalse(taskDao.delete(1));
// make sure db still works
assertEquals(0, taskDao.toList(Query.select(IDS)).size());
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
// TODO check eventing
}

@ -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)
}
}

@ -0,0 +1,281 @@
package com.todoroo.astrid.data;
import android.support.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.Snippet;
import org.tasks.time.DateTime;
import java.util.ArrayList;
import java.util.TreeSet;
import static com.todoroo.astrid.data.Task.COMPLETION_DATE;
import static com.todoroo.astrid.data.Task.DELETION_DATE;
import static com.todoroo.astrid.data.Task.DUE_DATE;
import static com.todoroo.astrid.data.Task.HIDE_UNTIL;
import static com.todoroo.astrid.data.Task.URGENCY_DAY_AFTER;
import static com.todoroo.astrid.data.Task.URGENCY_IN_TWO_WEEKS;
import static com.todoroo.astrid.data.Task.URGENCY_NEXT_MONTH;
import static com.todoroo.astrid.data.Task.URGENCY_NEXT_WEEK;
import static com.todoroo.astrid.data.Task.URGENCY_NONE;
import static com.todoroo.astrid.data.Task.URGENCY_SPECIFIC_DAY;
import static com.todoroo.astrid.data.Task.URGENCY_SPECIFIC_DAY_TIME;
import static com.todoroo.astrid.data.Task.URGENCY_TODAY;
import static com.todoroo.astrid.data.Task.URGENCY_TOMORROW;
import static com.todoroo.astrid.data.Task.createDueDate;
import static com.todoroo.astrid.data.Task.hasDueTime;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.tasks.Freeze.freezeAt;
import static org.tasks.Freeze.thaw;
import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
@RunWith(AndroidJUnit4.class)
public class TaskTest {
private static final DateTime now = new DateTime(2013, 12, 31, 16, 10, 53, 452);
private static final DateTime specificDueDate = new DateTime(2014, 3, 17, 9, 54, 27, 959);
@Before
public void setUp() {
freezeAt(now);
}
@After
public void tearDown() {
thaw();
}
@Test
public void testCreateDueDateNoUrgency() {
assertEquals(0, createDueDate(URGENCY_NONE, 1L));
}
@Test
public void testCreateDueDateToday() {
long expected = new DateTime(2013, 12, 31, 12, 0, 0, 0).getMillis();
assertEquals(expected, createDueDate(URGENCY_TODAY, -1L));
}
@Test
public void testCreateDueDateTomorrow() {
long expected = new DateTime(2014, 1, 1, 12, 0, 0, 0).getMillis();
assertEquals(expected, createDueDate(URGENCY_TOMORROW, -1L));
}
@Test
public void testCreateDueDateDayAfter() {
long expected = new DateTime(2014, 1, 2, 12, 0, 0, 0).getMillis();
assertEquals(expected, createDueDate(URGENCY_DAY_AFTER, -1L));
}
@Test
public void testCreateDueDateNextWeek() {
long expected = new DateTime(2014, 1, 7, 12, 0, 0, 0).getMillis();
assertEquals(expected, createDueDate(URGENCY_NEXT_WEEK, -1L));
}
@Test
public void testCreateDueDateInTwoWeeks() {
long expected = new DateTime(2014, 1, 14, 12, 0, 0, 0).getMillis();
assertEquals(expected, createDueDate(URGENCY_IN_TWO_WEEKS, -1L));
}
@Test
public void testCreateDueDateNextMonth() {
long expected = new DateTime(2014, 1, 31, 12, 0, 0, 0).getMillis();
assertEquals(expected, createDueDate(URGENCY_NEXT_MONTH, -1L));
}
@Test
public void testRemoveTimeForSpecificDay() {
long expected = specificDueDate
.withHourOfDay(12)
.withMinuteOfHour(0)
.withSecondOfMinute(0)
.withMillisOfSecond(0)
.getMillis();
assertEquals(expected, createDueDate(URGENCY_SPECIFIC_DAY, specificDueDate.getMillis()));
}
@Test
public void testRemoveSecondsForSpecificTime() {
long expected = specificDueDate
.withSecondOfMinute(1)
.withMillisOfSecond(0)
.getMillis();
assertEquals(expected, createDueDate(URGENCY_SPECIFIC_DAY_TIME, specificDueDate.getMillis()));
}
@Test
public void testTaskHasDueTime() {
Task task = new Task();
task.setValue(DUE_DATE, 1388516076000L);
assertTrue(task.hasDueTime());
assertTrue(task.hasDueDate());
}
@Test
public void testTaskHasDueDate() {
Task task = new Task();
task.setValue(DUE_DATE, 1388469600000L);
assertFalse(task.hasDueTime());
assertTrue(task.hasDueDate());
}
@Test
public void testDoesHaveDueTime() {
assertTrue(hasDueTime(1388516076000L));
}
@Test
public void testNoDueTime() {
assertFalse(hasDueTime(newDateTime().startOfDay().getMillis()));
assertFalse(hasDueTime(newDateTime().withMillisOfDay(60000).getMillis()));
}
@Test
public void testHasDueTime() {
assertTrue(hasDueTime(newDateTime().withMillisOfDay(1).getMillis()));
assertTrue(hasDueTime(newDateTime().withMillisOfDay(1000).getMillis()));
assertTrue(hasDueTime(newDateTime().withMillisOfDay(59999).getMillis()));
}
@Test
public void testDoesNotHaveDueTime() {
assertFalse(hasDueTime(1388469600000L));
}
@Test
public void testNewTaskIsNotCompleted() {
assertFalse(new Task().isCompleted());
}
@Test
public void testNewTaskNotDeleted() {
assertFalse(new Task().isDeleted());
}
@Test
public void testNewTaskNotHidden() {
assertFalse(new Task().isHidden());
}
@Test
public void testNewTaskDoesNotHaveDueDateOrTime() {
assertFalse(new Task().hasDueDate());
assertFalse(new Task().hasDueTime());
}
@Test
public void testTaskIsCompleted() {
Task task = new Task();
task.setValue(COMPLETION_DATE, 1L);
assertTrue(task.isCompleted());
}
@Test
public void testTaskIsNotHiddenAtHideUntilTime() {
final long now = currentTimeMillis();
freezeAt(now).thawAfter(new Snippet() {{
Task task = new Task();
task.setValue(HIDE_UNTIL, now);
assertFalse(task.isHidden());
}});
}
@Test
public void testTaskIsHiddenBeforeHideUntilTime() {
final long now = currentTimeMillis();
freezeAt(now).thawAfter(new Snippet() {{
Task task = new Task();
task.setValue(HIDE_UNTIL, now + 1);
assertTrue(task.isHidden());
}});
}
@Test
public void testTaskIsDeleted() {
Task task = new Task();
task.setValue(DELETION_DATE, 1L);
assertTrue(task.isDeleted());
}
@Test
public void testTaskWithNoDueDateIsOverdue() {
assertTrue(new Task().isOverdue());
}
@Test
public void testTaskNotOverdueAtDueTime() {
final long now = currentTimeMillis();
freezeAt(now).thawAfter(new Snippet() {{
Task task = new Task();
task.setValue(DUE_DATE, now);
assertFalse(task.isOverdue());
}});
}
@Test
public void testTaskIsOverduePastDueTime() {
final long dueDate = currentTimeMillis();
freezeAt(dueDate + 1).thawAfter(new Snippet() {{
Task task = new Task();
task.setValue(DUE_DATE, dueDate);
assertTrue(task.isOverdue());
}});
}
@Test
public void testTaskNotOverdueBeforeNoonOnDueDate() {
final DateTime dueDate = new DateTime().startOfDay();
freezeAt(dueDate.plusHours(12).minusMillis(1)).thawAfter(new Snippet() {{
Task task = new Task();
task.setValue(DUE_DATE, dueDate.getMillis());
assertFalse(task.hasDueTime());
assertFalse(task.isOverdue());
}});
}
@Test
public void testTaskOverdueAtNoonOnDueDate() {
final DateTime dueDate = new DateTime().startOfDay();
freezeAt(dueDate.plusHours(12)).thawAfter(new Snippet() {{
Task task = new Task();
task.setValue(DUE_DATE, dueDate.getMillis());
assertFalse(task.hasDueTime());
assertFalse(task.isOverdue());
}});
}
@Test
public void testTaskWithNoDueTimeIsOverdue() {
final DateTime dueDate = new DateTime().startOfDay();
freezeAt(dueDate.plusDays(1)).thawAfter(new Snippet() {{
Task task = new Task();
task.setValue(DUE_DATE, dueDate.getMillis());
assertFalse(task.hasDueTime());
assertTrue(task.isOverdue());
}});
}
@Test
public void testSanity() {
assertTrue(Task.IMPORTANCE_DO_OR_DIE < Task.IMPORTANCE_MUST_DO);
assertTrue(Task.IMPORTANCE_MUST_DO < Task.IMPORTANCE_SHOULD_DO);
assertTrue(Task.IMPORTANCE_SHOULD_DO < Task.IMPORTANCE_NONE);
ArrayList<Integer> reminderFlags = new ArrayList<>();
reminderFlags.add(Task.NOTIFY_AFTER_DEADLINE);
reminderFlags.add(Task.NOTIFY_AT_DEADLINE);
reminderFlags.add(Task.NOTIFY_MODE_NONSTOP);
// assert no duplicates
assertEquals(new TreeSet<>(reminderFlags).size(), reminderFlags.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))
}
}

@ -0,0 +1,71 @@
package com.todoroo.astrid.model;
import android.content.ContentValues;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.andlib.data.Property;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.Snippet;
import org.tasks.injection.InjectingTestCase;
import org.tasks.injection.TestComponent;
import org.tasks.preferences.Preferences;
import javax.inject.Inject;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.tasks.Freeze.freezeClock;
import static org.tasks.RemoteModelHelpers.asQueryProperties;
import static org.tasks.RemoteModelHelpers.compareRemoteModel;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
@RunWith(AndroidJUnit4.class)
public class TaskTest extends InjectingTestCase {
@Inject TaskDao taskDao;
@Inject Preferences preferences;
@Test
public void testNewTaskHasNoCreationDate() {
assertFalse(new Task().containsValue(Task.CREATION_DATE));
}
@Test
public void testSavedTaskHasCreationDate() {
freezeClock().thawAfter(new Snippet() {{
Task task = new Task();
taskDao.save(task);
assertEquals(currentTimeMillis(), (long) task.getCreationDate());
}});
}
@Test
public void testReadTaskFromDb() {
Task task = new Task();
taskDao.save(task);
Property[] properties = asQueryProperties(Task.TABLE, task.getDatabaseValues());
final Task fromDb = taskDao.fetch(task.getId(), properties);
compareRemoteModel(task, fromDb);
}
@Test
public void testDefaults() {
preferences.setDefaults();
ContentValues defaults = new Task().getDefaultValues();
assertTrue(defaults.containsKey(Task.TITLE.name));
assertTrue(defaults.containsKey(Task.DUE_DATE.name));
assertTrue(defaults.containsKey(Task.HIDE_UNTIL.name));
assertTrue(defaults.containsKey(Task.COMPLETION_DATE.name));
assertTrue(defaults.containsKey(Task.IMPORTANCE.name));
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
}

@ -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)
}
}

@ -0,0 +1,376 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.provider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.andlib.data.Property.IntegerProperty;
import com.todoroo.andlib.data.Property.StringProperty;
import com.todoroo.astrid.api.AstridApiConstants;
import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.StoreObject;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.test.DatabaseTestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.injection.TestComponent;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotSame;
import static junit.framework.Assert.fail;
@RunWith(AndroidJUnit4.class)
public class Astrid3ProviderTests extends DatabaseTestCase {
String[] PROJECTION = new String[] {
Task.ID.name,
Task.TITLE.name,
};
@Override
public void setUp() {
super.setUp();
// set up database
Astrid3ContentProvider.setDatabaseOverride(database);
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
/** Test CRUD over tasks with the ALL ITEMS cursor */
@Test
public void testAllItemsCrud() {
ContentResolver resolver = getTargetContext().getContentResolver();
// fetch all tasks, get nothing
Uri uri = Task.CONTENT_URI;
Cursor cursor = resolver.query(uri, PROJECTION, "1", null, null);
assertEquals(0, cursor.getCount());
cursor.close();
// insert a task
ContentValues values = new ContentValues();
values.put(Task.TITLE.name, "mf doom?");
resolver.insert(uri, values);
// fetch all tasks, get something
cursor = resolver.query(uri, PROJECTION, "1", null, null);
assertEquals(1, cursor.getCount());
cursor.moveToFirst();
assertEquals("mf doom?", cursor.getString(1));
cursor.close();
// update all tasks
values.put(Task.TITLE.name, "mf grimm?");
resolver.update(uri, values, "1", null);
// fetch all tasks, get something
cursor = resolver.query(uri, PROJECTION, "1", null, null);
cursor.moveToFirst();
assertEquals("mf grimm?", cursor.getString(1));
cursor.close();
// delete a task
assertEquals(1, resolver.delete(uri, "1", null));
// fetch all tasks, get nothing
cursor = resolver.query(uri, PROJECTION, null, null, null);
assertEquals(0, cursor.getCount());
cursor.close();
}
/** Test selecting data */
@Test
public void testSelection() {
ContentResolver resolver = getTargetContext().getContentResolver();
Uri uri = Task.CONTENT_URI;
// insert some tasks
ContentValues values = new ContentValues();
values.put(Task.TITLE.name, "tujiko noriko");
values.put(Task.IMPORTANCE.name, Task.IMPORTANCE_MUST_DO);
resolver.insert(uri, values);
values.clear();
values.put(Task.TITLE.name, "miho asahi");
values.put(Task.IMPORTANCE.name, Task.IMPORTANCE_NONE);
resolver.insert(uri, values);
// fetch all tasks with various selection parameters
Cursor cursor = resolver.query(uri, PROJECTION, "1", null, null);
assertEquals(2, cursor.getCount());
cursor.close();
cursor = resolver.query(uri, PROJECTION, null, null, null);
assertEquals(2, cursor.getCount());
cursor.close();
cursor = resolver.query(uri, PROJECTION, Task.IMPORTANCE + "=" +
Task.IMPORTANCE_MUST_DO, null, null);
assertEquals(1, cursor.getCount());
cursor.moveToFirst();
assertEquals("tujiko noriko", cursor.getString(1));
cursor.close();
cursor = resolver.query(uri, PROJECTION, Task.IMPORTANCE + ">" +
Task.IMPORTANCE_MUST_DO, null, null);
assertEquals(1, cursor.getCount());
cursor.moveToFirst();
assertEquals("miho asahi", cursor.getString(1));
cursor.close();
cursor = resolver.query(uri, PROJECTION, Task.IMPORTANCE + "=" +
Task.IMPORTANCE_DO_OR_DIE, null, null);
assertEquals(0, cursor.getCount());
cursor.close();
}
/** Test updating */
@Test
public void testUpdating() {
ContentResolver resolver = getTargetContext().getContentResolver();
// insert some tasks
ContentValues values = new ContentValues();
values.put(Task.TITLE.name, "carlos silva");
values.put(Task.IMPORTANCE.name, Task.IMPORTANCE_SHOULD_DO);
Uri carlosUri = resolver.insert(Task.CONTENT_URI, values);
values.clear();
values.put(Task.TITLE.name, "felix hernandez");
values.put(Task.IMPORTANCE.name, Task.IMPORTANCE_MUST_DO);
resolver.insert(Task.CONTENT_URI, values);
String[] projection = new String[] {
Task.ID.name,
Task.TITLE.name,
Task.IMPORTANCE.name,
};
// test updating with single item URI
Cursor cursor = resolver.query(Task.CONTENT_URI, projection,
Task.TITLE.eq("carlos who?").toString(), null, null);
assertEquals(0, cursor.getCount());
cursor.close();
values.clear();
values.put(Task.TITLE.name, "carlos who?");
assertEquals(1, resolver.update(carlosUri, values, null, null));
cursor = resolver.query(Task.CONTENT_URI, projection, Task.TITLE.eq("carlos who?").toString(), null, null);
assertEquals(1, cursor.getCount());
cursor.close();
// test updating with all items uri
cursor = resolver.query(Task.CONTENT_URI, PROJECTION,
Task.IMPORTANCE.eq(Task.IMPORTANCE_NONE).toString(), null, null);
assertEquals(0, cursor.getCount());
cursor.close();
values.clear();
values.put(Task.IMPORTANCE.name, Task.IMPORTANCE_NONE);
assertEquals(1, resolver.update(Task.CONTENT_URI, values,
Task.IMPORTANCE.eq(Task.IMPORTANCE_SHOULD_DO).toString(), null));
cursor = resolver.query(Task.CONTENT_URI, PROJECTION,
Task.IMPORTANCE.eq(Task.IMPORTANCE_NONE).toString(), null, null);
assertEquals(1, cursor.getCount());
cursor.close();
// test updating with group by uri
try {
Uri groupByUri = Uri.withAppendedPath(Task.CONTENT_URI,
AstridApiConstants.GROUP_BY_URI + Task.TITLE.name);
resolver.update(groupByUri, values, null, null);
fail("Able to update using groupby uri");
} catch (Exception e) {
// expected
}
}
/** Test deleting */
@Test
public void testDeleting() {
ContentResolver resolver = getTargetContext().getContentResolver();
Uri allItemsUri = Task.CONTENT_URI;
// insert some tasks
ContentValues values = new ContentValues();
values.put(Task.TITLE.name, "modest mouse");
values.put(Task.IMPORTANCE.name, Task.IMPORTANCE_DO_OR_DIE);
Uri modestMouse = resolver.insert(allItemsUri, values);
values.clear();
values.put(Task.TITLE.name, "death cab");
values.put(Task.IMPORTANCE.name, Task.IMPORTANCE_MUST_DO);
resolver.insert(allItemsUri, values);
values.clear();
values.put(Task.TITLE.name, "murder city devils");
values.put(Task.IMPORTANCE.name, Task.IMPORTANCE_SHOULD_DO);
resolver.insert(allItemsUri, values);
// test deleting with single URI
Cursor cursor = resolver.query(allItemsUri, PROJECTION, Task.TITLE.name +
" = 'modest mouse'", null, null);
assertEquals(1, cursor.getCount());
cursor.close();
assertEquals(1, resolver.delete(modestMouse, null, null));
cursor = resolver.query(allItemsUri, PROJECTION, Task.TITLE.name +
" = 'modest mouse'", null, null);
assertEquals(0, cursor.getCount());
cursor.close();
// test updating with all items uri
cursor = resolver.query(allItemsUri, PROJECTION, Task.TITLE.name +
" = 'murder city devils'", null, null);
assertEquals(1, cursor.getCount());
cursor.close();
assertEquals(1, resolver.delete(allItemsUri, Task.IMPORTANCE.name +
">" + Task.IMPORTANCE_MUST_DO, null));
cursor = resolver.query(allItemsUri, PROJECTION, Task.TITLE.name +
" = 'murder city devils'", null, null);
assertEquals(0, cursor.getCount());
cursor.close();
// test with group by uri
try {
Uri groupByUri = Uri.withAppendedPath(Task.CONTENT_URI,
AstridApiConstants.GROUP_BY_URI + Task.TITLE.name);
resolver.delete(groupByUri, null, null);
fail("Able to delete using groupby uri");
} catch (Exception e) {
// expected
}
}
/** Test CRUD over SINGLE ITEM uri */
@Test
public void testSingleItemCrud() {
ContentResolver resolver = getTargetContext().getContentResolver();
Uri uri = StoreObject.CONTENT_URI;
ContentValues values = new ContentValues();
values.put(StoreObject.TYPE.name, "rapper");
values.put(StoreObject.ITEM.name, "mf doom?");
Uri firstUri = resolver.insert(uri, values);
values.put(StoreObject.ITEM.name, "gm grimm!");
Uri secondUri = resolver.insert(uri, values);
assertNotSame(firstUri, secondUri);
String[] storeProjection = new String[] {
StoreObject.ITEM.name,
};
Cursor cursor = resolver.query(uri, storeProjection, null, null, null);
assertEquals(2, cursor.getCount());
cursor.close();
cursor = resolver.query(firstUri, storeProjection, null, null, null);
assertEquals(1, cursor.getCount());
cursor.moveToFirst();
assertEquals("mf doom?", cursor.getString(0));
cursor.close();
values.put(StoreObject.ITEM.name, "danger mouse.");
resolver.update(firstUri, values, null, null);
cursor = resolver.query(firstUri, storeProjection, null, null, null);
assertEquals(1, cursor.getCount());
cursor.moveToFirst();
assertEquals("danger mouse.", cursor.getString(0));
cursor.close();
assertEquals(1, resolver.delete(firstUri, null, null));
cursor = resolver.query(uri, storeProjection, null, null, null);
assertEquals(1, cursor.getCount());
cursor.close();
}
/** Test GROUP BY uri */
@Test
public void testGroupByCrud() {
ContentResolver resolver = getTargetContext().getContentResolver();
Uri uri = Task.CONTENT_URI;
ContentValues values = new ContentValues();
values.put(Task.TITLE.name, "catwoman");
resolver.insert(uri, values);
values.put(Task.TITLE.name, "the joker");
resolver.insert(uri, values);
resolver.insert(uri, values);
resolver.insert(uri, values);
values.put(Task.TITLE.name, "deep freeze");
resolver.insert(uri, values);
resolver.insert(uri, values);
Uri groupByUri = Uri.withAppendedPath(Task.CONTENT_URI,
AstridApiConstants.GROUP_BY_URI + Task.TITLE.name);
Cursor cursor = resolver.query(groupByUri, PROJECTION, null, null, Task.TITLE.name);
assertEquals(3, cursor.getCount());
cursor.moveToFirst();
assertEquals("catwoman", cursor.getString(1));
cursor.moveToNext();
assertEquals("deep freeze", cursor.getString(1));
cursor.moveToNext();
assertEquals("the joker", cursor.getString(1));
cursor.close();
// test "group-by" with metadata
IntegerProperty age = new IntegerProperty(Metadata.TABLE, Metadata.VALUE1.name);
StringProperty size = Metadata.VALUE2;
uri = Metadata.CONTENT_URI;
values.clear();
values.put(Metadata.TASK.name, 1);
values.put(Metadata.KEY.name, "sizes");
values.put(age.name, 50);
values.put(size.name, "large");
resolver.insert(uri, values);
values.put(age.name, 40);
values.put(size.name, "large");
resolver.insert(uri, values);
values.put(age.name, 20);
values.put(size.name, "small");
resolver.insert(uri, values);
String[] metadataProjection = new String[] { "AVG(" + age + ")" };
Uri groupBySizeUri = Uri.withAppendedPath(Metadata.CONTENT_URI,
AstridApiConstants.GROUP_BY_URI + size.name);
cursor = resolver.query(groupBySizeUri, metadataProjection, null, null, size.name);
assertEquals(2, cursor.getCount());
cursor.moveToFirst();
assertEquals(45, cursor.getInt(0));
cursor.moveToNext();
assertEquals(20, cursor.getInt(0));
}
}

@ -0,0 +1,193 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.reminders;
import android.content.Context;
import android.support.test.runner.AndroidJUnit4;
import android.support.v4.app.NotificationCompat;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.test.DatabaseTestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.LocalBroadcastManager;
import org.tasks.Notifier;
import org.tasks.injection.TestComponent;
import org.tasks.notifications.NotificationManager;
import org.tasks.themes.ThemeCache;
import javax.inject.Inject;
import dagger.Module;
import dagger.Provides;
import dagger.Subcomponent;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@RunWith(AndroidJUnit4.class)
public class NotificationTests extends DatabaseTestCase {
@Module
public static class NotificationTestsModule {
private final NotificationManager notificationManager = mock(NotificationManager.class);
private final LocalBroadcastManager localBroadcastManager = mock(LocalBroadcastManager.class);
private final Context context;
public NotificationTestsModule(Context context) {
this.context = context;
}
@Provides
public NotificationManager getNotificationManager() {
return notificationManager;
}
@Provides
public LocalBroadcastManager getBroadcaster() {
return localBroadcastManager;
}
@Provides
public ThemeCache getThemeCache() {
return new ThemeCache(context);
}
}
@Subcomponent(modules = NotificationTestsModule.class)
public interface NotificationTestsComponent {
void inject(NotificationTests notificationTests);
}
@Inject TaskDao taskDao;
@Inject NotificationManager notificationManager;
@Inject LocalBroadcastManager localBroadcastManager;
@Inject Notifier notifier;
@Override
public void tearDown() {
super.tearDown();
verifyNoMoreInteractions(notificationManager);
verifyNoMoreInteractions(localBroadcastManager);
}
@Test
public void testAlarmToNotification() {
final Task task = new Task() {{
setTitle("rubberduck");
setDueDate(DateUtilities.now() - DateUtilities.ONE_DAY);
}};
taskDao.persist(task);
notifier.triggerTaskNotification(task.getId(), ReminderService.TYPE_DUE);
verify(notificationManager).notify(eq((int) task.getId()), any(NotificationCompat.Builder.class), true, false, false);
}
@Test
public void testDeletedTaskDoesntTriggerNotification() {
final Task task = new Task() {{
setTitle("gooeyduck");
setDeletionDate(DateUtilities.now());
}};
taskDao.persist(task);
notifier.triggerTaskNotification(task.getId(),ReminderService.TYPE_DUE);
verify(notificationManager).cancel((int) task.getId());
}
@Test
public void testCompletedTaskDoesntTriggerNotification() {
final Task task = new Task() {{
setTitle("rubberduck");
setCompletionDate(DateUtilities.now());
}};
taskDao.persist(task);
notifier.triggerTaskNotification(task.getId(), ReminderService.TYPE_DUE);
verify(notificationManager).cancel((int) task.getId());
}
// public void testQuietHours() {
// final Task task = new Task();
// task.setTitle("rubberduck");
// taskDao.persist(task);
// Intent intent = new Intent();
// intent.putExtra(TaskNotificationReceiver.ID_KEY, task.getId());
//
// int hour = newDate().getHours();
// Preferences.setStringFromInteger(R.string.p_rmd_quietStart, hour - 1);
// Preferences.setStringFromInteger(R.string.p_rmd_quietEnd, hour + 1);
//
// // due date notification has vibrate
// TaskNotificationReceiver.setNotificationManager(new TestNotificationManager() {
// public void notify(int id, Notification notification) {
// assertNull(notification.sound);
// assertTrue((notification.defaults & Notification.DEFAULT_SOUND) == 0);
// assertNotNull(notification.vibrate);
// assertTrue(notification.vibrate.length > 0);
// }
// });
// intent.putExtra(TaskNotificationReceiver.EXTRAS_TYPE, ReminderService.TYPE_DUE);
// notificationReceiver.onReceive(getContext(), intent);
//
// // random notification does not
// TaskNotificationReceiver.setNotificationManager(new TestNotificationManager() {
// public void notify(int id, Notification notification) {
// assertNull(notification.sound);
// assertTrue((notification.defaults & Notification.DEFAULT_SOUND) == 0);
// assertTrue(notification.vibrate == null ||
// notification.vibrate.length == 0);
// }
// });
// intent.removeExtra(TaskNotificationReceiver.EXTRAS_TYPE);
// intent.putExtra(TaskNotificationReceiver.EXTRAS_TYPE, ReminderService.TYPE_RANDOM);
// notificationReceiver.onReceive(getContext(), intent);
//
// // wrapping works
// Preferences.setStringFromInteger(R.string.p_rmd_quietStart, hour + 2);
// Preferences.setStringFromInteger(R.string.p_rmd_quietEnd, hour + 1);
//
// TaskNotificationReceiver.setNotificationManager(new TestNotificationManager() {
// public void notify(int id, Notification notification) {
// assertNull(notification.sound);
// assertTrue((notification.defaults & Notification.DEFAULT_SOUND) == 0);
// }
// });
// intent.removeExtra(TaskNotificationReceiver.EXTRAS_TYPE);
// intent.putExtra(TaskNotificationReceiver.EXTRAS_TYPE, ReminderService.TYPE_DUE);
// notificationReceiver.onReceive(getContext(), intent);
//
// // nonstop notification still sounds
// task.setReminderFlags(Task.NOTIFY_MODE_NONSTOP);
// taskDao.persist(task);
// TaskNotificationReceiver.setNotificationManager(new TestNotificationManager() {
// public void notify(int id, Notification notification) {
// assertTrue(notification.sound != null ||
// (notification.defaults & Notification.DEFAULT_SOUND) > 0);
// }
// });
// notificationReceiver.onReceive(getContext(), intent);
// }
@Override
protected void inject(TestComponent component) {
component
.plus(new NotificationTestsModule(getTargetContext()))
.inject(this);
}
}

@ -0,0 +1,368 @@
package com.todoroo.astrid.reminders;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.astrid.data.Task;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.tasks.R;
import org.tasks.Snippet;
import org.tasks.injection.InjectingTestCase;
import org.tasks.injection.TestComponent;
import org.tasks.jobs.JobQueue;
import org.tasks.jobs.Reminder;
import org.tasks.preferences.Preferences;
import org.tasks.reminders.Random;
import org.tasks.time.DateTime;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import static com.natpryce.makeiteasy.MakeItEasy.with;
import static com.todoroo.andlib.utility.DateUtilities.ONE_HOUR;
import static com.todoroo.andlib.utility.DateUtilities.ONE_WEEK;
import static com.todoroo.astrid.data.Task.NOTIFY_AFTER_DEADLINE;
import static com.todoroo.astrid.data.Task.NOTIFY_AT_DEADLINE;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.tasks.Freeze.freezeClock;
import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.makers.TaskMaker.COMPLETION_TIME;
import static org.tasks.makers.TaskMaker.CREATION_TIME;
import static org.tasks.makers.TaskMaker.DELETION_TIME;
import static org.tasks.makers.TaskMaker.DUE_DATE;
import static org.tasks.makers.TaskMaker.DUE_TIME;
import static org.tasks.makers.TaskMaker.ID;
import static org.tasks.makers.TaskMaker.RANDOM_REMINDER_PERIOD;
import static org.tasks.makers.TaskMaker.REMINDERS;
import static org.tasks.makers.TaskMaker.REMINDER_LAST;
import static org.tasks.makers.TaskMaker.SNOOZE_TIME;
import static org.tasks.makers.TaskMaker.newTask;
@RunWith(AndroidJUnit4.class)
public class ReminderServiceTest extends InjectingTestCase {
@Inject Preferences preferences;
private ReminderService service;
private Random random;
private JobQueue jobs;
@Before
public void before() {
jobs = mock(JobQueue.class);
random = mock(Random.class);
when(random.nextFloat()).thenReturn(1.0f);
preferences.reset();
service = new ReminderService(preferences, jobs, random);
}
@After
public void after() {
verifyNoMoreInteractions(jobs);
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
@Test
public void dontScheduleDueDateReminderWhenFlagNotSet() {
service.scheduleAlarm(null, newTask(with(ID, 1L), with(DUE_TIME, newDateTime())));
verify(jobs).cancelReminder(1);
}
@Test
public void dontScheduleDueDateReminderWhenTimeNotSet() {
service.scheduleAlarm(null, newTask(with(ID, 1L), with(REMINDERS, NOTIFY_AT_DEADLINE)));
verify(jobs).cancelReminder(1);
}
@Test
public void schedulePastDueDate() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime().minusDays(1)),
with(REMINDERS, NOTIFY_AT_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1, task.getDueDate(), ReminderService.TYPE_DUE));
}
@Test
public void scheduleFutureDueDate() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime().plusDays(1)),
with(REMINDERS, NOTIFY_AT_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1, task.getDueDate(), ReminderService.TYPE_DUE));
}
@Test
public void scheduleReminderAtDefaultDueTime() {
DateTime now = newDateTime();
Task task = newTask(
with(ID, 1L),
with(DUE_DATE, now),
with(REMINDERS, NOTIFY_AT_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1, now.startOfDay().withHourOfDay(18).getMillis(), ReminderService.TYPE_DUE));
}
@Test
public void dontScheduleReminderForCompletedTask() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime().plusDays(1)),
with(COMPLETION_TIME, newDateTime()),
with(REMINDERS, NOTIFY_AT_DEADLINE));
service.scheduleAlarm(null, task);
verify(jobs).cancelReminder(1);
}
@Test
public void dontScheduleReminderForDeletedTask() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime().plusDays(1)),
with(DELETION_TIME, newDateTime()),
with(REMINDERS, NOTIFY_AT_DEADLINE));
service.scheduleAlarm(null, task);
verify(jobs).cancelReminder(1);
}
@Test
public void dontScheduleDueDateReminderWhenAlreadyReminded() {
DateTime now = newDateTime();
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, now),
with(REMINDER_LAST, now.plusSeconds(1)),
with(REMINDERS, NOTIFY_AT_DEADLINE));
service.scheduleAlarm(null, task);
verify(jobs).cancelReminder(1);
}
@Test
public void ignoreStaleSnoozeTime() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, newDateTime()),
with(SNOOZE_TIME, newDateTime().minusMinutes(5)),
with(REMINDER_LAST, newDateTime().minusMinutes(4)),
with(REMINDERS, NOTIFY_AT_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1, task.getDueDate(), ReminderService.TYPE_DUE));
}
@Test
public void dontIgnoreMissedSnoozeTime() {
DateTime dueDate = newDateTime();
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, dueDate),
with(SNOOZE_TIME, dueDate.minusMinutes(4)),
with(REMINDER_LAST, dueDate.minusMinutes(5)),
with(REMINDERS, NOTIFY_AT_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1, task.getReminderSnooze(), ReminderService.TYPE_SNOOZE));
}
@Test
public void scheduleInitialRandomReminder() {
freezeClock().thawAfter(new Snippet() {{
DateTime now = newDateTime();
when(random.nextFloat()).thenReturn(0.3865f);
Task task = newTask(
with(ID, 1L),
with(REMINDER_LAST, (DateTime) null),
with(CREATION_TIME, now.minusDays(1)),
with(RANDOM_REMINDER_PERIOD, ONE_WEEK));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1L, now.minusDays(1).getMillis() + 584206592, ReminderService.TYPE_RANDOM));
}});
}
@Test
public void scheduleNextRandomReminder() {
freezeClock().thawAfter(new Snippet() {{
DateTime now = newDateTime();
when(random.nextFloat()).thenReturn(0.3865f);
Task task = newTask(
with(ID, 1L),
with(REMINDER_LAST, now.minusDays(1)),
with(CREATION_TIME, now.minusDays(30)),
with(RANDOM_REMINDER_PERIOD, ONE_WEEK));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1L, now.minusDays(1).getMillis() + 584206592, ReminderService.TYPE_RANDOM));
}});
}
@Test
public void scheduleOverdueRandomReminder() {
freezeClock().thawAfter(new Snippet() {{
DateTime now = newDateTime();
when(random.nextFloat()).thenReturn(0.3865f);
Task task = newTask(
with(ID, 1L),
with(REMINDER_LAST, now.minusDays(14)),
with(CREATION_TIME, now.minusDays(30)),
with(RANDOM_REMINDER_PERIOD, ONE_WEEK));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1L, now.getMillis() + 10148400, ReminderService.TYPE_RANDOM));
}});
}
@Test
public void scheduleOverdueNoLastReminder() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, new DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, (DateTime) null),
with(REMINDERS, NOTIFY_AFTER_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1L, new DateTime(2017, 9, 23, 15, 30, 1, 0).getMillis(), ReminderService.TYPE_OVERDUE));
}
@Test
public void scheduleOverduePastLastReminder() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, new DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, new DateTime(2017, 9, 24, 12, 0)),
with(REMINDERS, NOTIFY_AFTER_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1L, new DateTime(2017, 9, 24, 15, 30, 1, 0).getMillis(), ReminderService.TYPE_OVERDUE));
}
@Test
public void scheduleOverdueBeforeLastReminder() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, new DateTime(2017, 9, 22, 12, 30)),
with(REMINDER_LAST, new DateTime(2017, 9, 24, 15, 0)),
with(REMINDERS, NOTIFY_AFTER_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1L, new DateTime(2017, 9, 25, 12, 30, 1, 0).getMillis(), ReminderService.TYPE_OVERDUE));
}
@Test
public void scheduleOverdueWithNoDueTime() {
preferences.setInt(R.string.p_rmd_time, (int) TimeUnit.HOURS.toMillis(15));
Task task = newTask(
with(ID, 1L),
with(DUE_DATE, new DateTime(2017, 9, 22)),
with(REMINDER_LAST, new DateTime(2017, 9, 23, 12, 17, 59, 999)),
with(REMINDERS, NOTIFY_AFTER_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1L, new DateTime(2017, 9, 23, 15, 0, 0, 0).getMillis(), ReminderService.TYPE_OVERDUE));
}
@Test
public void scheduleSubsequentOverdueReminder() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, new DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, new DateTime(2017, 9, 23, 15, 30, 59, 999)),
with(REMINDERS, NOTIFY_AFTER_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1L, new DateTime(2017, 9, 24, 15, 30, 1, 0).getMillis(), ReminderService.TYPE_OVERDUE));
}
@Test
public void scheduleOverdueAfterLastReminder() {
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, new DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, new DateTime(2017, 9, 23, 12, 17, 59, 999)),
with(REMINDERS, NOTIFY_AFTER_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1L, new DateTime(2017, 9, 23, 15, 30, 1, 0).getMillis(), ReminderService.TYPE_OVERDUE));
}
@Test
public void snoozeOverridesAll() {
DateTime now = newDateTime();
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, now),
with(SNOOZE_TIME, now.plusMonths(12)),
with(REMINDERS, NOTIFY_AT_DEADLINE | NOTIFY_AFTER_DEADLINE),
with(RANDOM_REMINDER_PERIOD, ONE_HOUR));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancelReminder(1);
order.verify(jobs).add(new Reminder(1, now.plusMonths(12).getMillis(), ReminderService.TYPE_SNOOZE));
}
}

@ -0,0 +1,270 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.repeats;
import android.support.test.runner.AndroidJUnit4;
import com.google.ical.values.Frequency;
import com.google.ical.values.RRule;
import com.google.ical.values.Weekday;
import com.google.ical.values.WeekdayNum;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.data.Task;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.time.DateTime;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import static junit.framework.Assert.assertEquals;
import static org.tasks.date.DateTimeUtils.newDateTime;
@RunWith(AndroidJUnit4.class)
public class AdvancedRepeatTest {
private static final int PREV_PREV = -2;
private static final int PREV = -1;
private static final int THIS = 1;
private static final int NEXT = 2;
private Task task;
private long nextDueDate;
private RRule rrule;
@Before
public void setUp() {
task = new Task();
task.setCompletionDate(DateUtilities.now());
rrule = new RRule();
}
// --- date with time tests
@Test
public void testDueDateSpecificTime() throws ParseException {
buildRRule(1, Frequency.DAILY);
// test specific day & time
long dayWithTime = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, new DateTime(2010, 8, 1, 10, 4, 0).getMillis());
task.setDueDate(dayWithTime);
long nextDayWithTime = dayWithTime + DateUtilities.ONE_DAY;
nextDueDate = RepeatTaskHelper.computeNextDueDate(task, rrule.toIcal(), false);
assertDateTimeEquals(nextDayWithTime, nextDueDate);
}
@Test
public void testCompletionDateSpecificTime() throws ParseException {
buildRRule(1, Frequency.DAILY);
// test specific day & time
long dayWithTime = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, new DateTime(2010, 8, 1, 10, 4, 0).getMillis());
task.setDueDate(dayWithTime);
DateTime todayWithTime = newDateTime()
.withHourOfDay(10)
.withMinuteOfHour(4)
.withSecondOfMinute(1);
long nextDayWithTimeLong = todayWithTime.getMillis();
nextDayWithTimeLong += DateUtilities.ONE_DAY;
nextDayWithTimeLong = nextDayWithTimeLong / 1000L * 1000;
nextDueDate = RepeatTaskHelper.computeNextDueDate(task, rrule.toIcal(), true);
assertDateTimeEquals(nextDayWithTimeLong, nextDueDate);
}
// --- due date tests
/** test multiple days per week - DUE DATE */
@Test
public void testDueDateInPastSingleWeekMultiDay() throws Exception {
buildRRule(1, Frequency.WEEKLY, Weekday.MO, Weekday.WE, Weekday.FR);
setTaskDueDate(THIS, Calendar.SUNDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.MONDAY);
setTaskDueDate(THIS, Calendar.MONDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.WEDNESDAY);
setTaskDueDate(THIS, Calendar.FRIDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.MONDAY);
}
/** test single day repeats - DUE DATE */
@Test
public void testDueDateSingleDay() throws Exception {
buildRRule(1, Frequency.WEEKLY, Weekday.MO);
setTaskDueDate(PREV_PREV, Calendar.MONDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY);
setTaskDueDate(PREV_PREV, Calendar.FRIDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.MONDAY);
setTaskDueDate(PREV, Calendar.MONDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY);
setTaskDueDate(PREV, Calendar.FRIDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.MONDAY);
setTaskDueDate(THIS, Calendar.SUNDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.MONDAY);
setTaskDueDate(THIS, Calendar.MONDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY);
}
/** test multiple days per week - DUE DATE */
@Test
public void testDueDateSingleWeekMultiDay() throws Exception {
buildRRule(1, Frequency.WEEKLY, Weekday.MO, Weekday.WE, Weekday.FR);
setTaskDueDate(THIS, Calendar.SUNDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.MONDAY);
setTaskDueDate(THIS, Calendar.MONDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.WEDNESDAY);
setTaskDueDate(THIS, Calendar.FRIDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.MONDAY);
}
/** test multiple days per week, multiple intervals - DUE DATE */
@Test
public void testDueDateMultiWeekMultiDay() throws Exception {
buildRRule(2, Frequency.WEEKLY, Weekday.MO, Weekday.WE, Weekday.FR);
setTaskDueDate(THIS, Calendar.SUNDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY);
setTaskDueDate(THIS, Calendar.MONDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, THIS, Calendar.WEDNESDAY);
setTaskDueDate(THIS, Calendar.FRIDAY);
computeNextDueDate(false);
assertDueDate(nextDueDate, NEXT, Calendar.MONDAY);
}
// --- completion tests
/** test multiple days per week - COMPLETE DATE */
@Test
public void testCompleteDateSingleWeek() throws Exception {
for(Weekday wday : Weekday.values()) {
buildRRule(1, Frequency.WEEKLY, wday);
computeNextDueDate(true);
long expected = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, THIS, wday.javaDayNum);
nextDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, nextDueDate);
assertEquals(expected, nextDueDate);
}
for(Weekday wday1 : Weekday.values()) {
for(Weekday wday2 : Weekday.values()) {
if(wday1 == wday2)
continue;
buildRRule(1, Frequency.WEEKLY, wday1, wday2);
long nextOne = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, THIS, wday1.javaDayNum);
long nextTwo = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, THIS, wday2.javaDayNum);
computeNextDueDate(true);
nextDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, nextDueDate);
assertEquals(Math.min(nextOne, nextTwo), nextDueDate);
}
}
}
/** test multiple days per week, multiple intervals - COMPLETE DATE */
@Test
public void testCompleteDateMultiWeek() throws Exception {
for(Weekday wday : Weekday.values()) {
buildRRule(2, Frequency.WEEKLY, wday);
computeNextDueDate(true);
long expected = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, NEXT, wday.javaDayNum);
nextDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, nextDueDate);
assertEquals(expected, nextDueDate);
}
for(Weekday wday1 : Weekday.values()) {
for(Weekday wday2 : Weekday.values()) {
if(wday1 == wday2)
continue;
buildRRule(2, Frequency.WEEKLY, wday1, wday2);
long nextOne = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, NEXT, wday1.javaDayNum);
long nextTwo = getDate(DateUtilities.now() + DateUtilities.ONE_DAY, NEXT, wday2.javaDayNum);
computeNextDueDate(true);
nextDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, nextDueDate);
assertEquals(Math.min(nextOne, nextTwo), nextDueDate);
}
}
}
// --- helpers
private void computeNextDueDate(boolean fromComplete) throws ParseException{
nextDueDate = RepeatTaskHelper.computeNextDueDate(task, rrule.toIcal(), fromComplete);
}
private void buildRRule(int interval, Frequency freq, Weekday... weekdays) {
rrule.setInterval(interval);
rrule.setFreq(freq);
setRRuleDays(rrule, weekdays);
}
private void assertDueDate(long actual, int expectedWhich, int expectedDayOfWeek) {
long expected = getDate(task.getDueDate(), expectedWhich, expectedDayOfWeek);
assertEquals(expected, actual);
}
public static void assertDateTimeEquals(long date, long other) {
assertEquals("Expected: " + newDateTime(date) + ", Actual: " + newDateTime(other),
date, other);
}
private void setRRuleDays(RRule rrule, Weekday... weekdays) {
ArrayList<WeekdayNum> days = new ArrayList<>();
for(Weekday wd : weekdays)
days.add(new WeekdayNum(0, wd));
rrule.setByDay(days);
}
private void setTaskDueDate(int which, int day) {
long time = getDate(DateUtilities.now(), which, day);
task.setDueDate(time);
}
private long getDate(long start, int which, int dayOfWeek) {
Calendar c = Calendar.getInstance();
c.setTimeInMillis(start);
int direction = which > 0 ? 1 : -1;
while(c.get(Calendar.DAY_OF_WEEK) != dayOfWeek) {
c.add(Calendar.DAY_OF_MONTH, direction);
}
c.add(Calendar.DAY_OF_MONTH, (Math.abs(which) - 1) * direction * 7);
return Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, c.getTimeInMillis());
}
}

@ -0,0 +1,259 @@
package com.todoroo.astrid.repeats;
import android.support.test.runner.AndroidJUnit4;
import com.google.ical.values.Frequency;
import com.google.ical.values.RRule;
import com.google.ical.values.Weekday;
import com.google.ical.values.WeekdayNum;
import com.todoroo.astrid.data.Task;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.time.DateTime;
import java.text.ParseException;
import static com.todoroo.astrid.repeats.RepeatTaskHelper.computeNextDueDate;
import static java.util.Arrays.asList;
import static junit.framework.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class NewRepeatTests {
@Test
public void testRepeatMinutelyFromDueDate() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 26, 12, 30);
Task task = newFromDue(Frequency.MINUTELY, 1, dueDateTime);
assertEquals(newDayTime(2016, 8, 26, 12, 31), calculateNextDueDate(task));
}
@Test
public void testRepeatHourlyFromDueDate() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 26, 12, 30);
Task task = newFromDue(Frequency.HOURLY, 1, dueDateTime);
assertEquals(newDayTime(2016, 8, 26, 13, 30), calculateNextDueDate(task));
}
@Test
public void testRepeatDailyFromDueDate() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 26, 12, 30);
Task task = newFromDue(Frequency.DAILY, 1, dueDateTime);
assertEquals(newDayTime(2016, 8, 27, 12, 30), calculateNextDueDate(task));
}
@Test
public void testRepeatWeeklyFromDueDate() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 28, 1, 34);
Task task = newFromDue(Frequency.WEEKLY, 1, dueDateTime);
assertEquals(newDayTime(2016, 9, 4, 1, 34), calculateNextDueDate(task));
}
@Test
public void testRepeatMonthlyFromDueDate() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 28, 1, 44);
Task task = newFromDue(Frequency.MONTHLY, 1, dueDateTime);
assertEquals(newDayTime(2016, 9, 28, 1, 44), calculateNextDueDate(task));
}
@Test
public void testRepeatYearlyFromDueDate() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 28, 1, 44);
Task task = newFromDue(Frequency.YEARLY, 1, dueDateTime);
assertEquals(newDayTime(2017, 8, 28, 1, 44), calculateNextDueDate(task));
}
/** Tests for repeating from completionDate */
@Test
public void testRepeatMinutelyFromCompleteDateCompleteBefore() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 30, 0, 25);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.MINUTELY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 8, 29, 0, 15), calculateNextDueDate(task));
}
@Test
public void testRepeatMinutelyFromCompleteDateCompleteAfter() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 28, 0, 4);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.MINUTELY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 8, 29, 0, 15), calculateNextDueDate(task));
}
@Test
public void testRepeatHourlyFromCompleteDateCompleteBefore() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 30, 0, 25);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.HOURLY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 8, 29, 1, 14), calculateNextDueDate(task));
}
@Test
public void testRepeatHourlyFromCompleteDateCompleteAfter() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 28, 0, 4);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.HOURLY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 8, 29, 1, 14), calculateNextDueDate(task));
}
@Test
public void testRepeatDailyFromCompleteDateCompleteBefore() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 30, 0, 25);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.DAILY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 8, 30, 0, 25), calculateNextDueDate(task));
}
@Test
public void testRepeatDailyFromCompleteDateCompleteAfter() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 28, 0, 4);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.DAILY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 8, 30, 0, 4), calculateNextDueDate(task));
}
@Test
public void testRepeatWeeklyFromCompleteDateCompleteBefore() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 30, 0, 25);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.WEEKLY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 9, 5, 0, 25), calculateNextDueDate(task));
}
@Test
public void testRepeatWeeklyFromCompleteDateCompleteAfter() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 28, 0, 4);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.WEEKLY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 9, 5, 0, 4), calculateNextDueDate(task));
}
@Test
public void testRepeatMonthlyFromCompleteDateCompleteBefore() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 30, 0, 25);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.MONTHLY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 9, 29, 0, 25), calculateNextDueDate(task));
}
@Test
public void testRepeatMonthlyFromCompleteDateCompleteAfter() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 28, 0, 4);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.MONTHLY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2016, 9, 29, 0, 4), calculateNextDueDate(task));
}
@Test
public void testRepeatYearlyFromCompleteDateCompleteBefore() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 30, 0, 25);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.YEARLY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2017, 8, 29, 0, 25), calculateNextDueDate(task));
}
@Test
public void testRepeatYearlyFromCompleteDateCompleteAfter() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 28, 0, 4);
DateTime completionDateTime = newDayTime(2016, 8, 29, 0, 14);
Task task = newFromCompleted(Frequency.YEARLY, 1, dueDateTime, completionDateTime);
assertEquals(newDayTime(2017, 8, 29, 0, 4), calculateNextDueDate(task));
}
@Test
public void testAdvancedRepeatWeeklyFromDueDate() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 29, 0, 25);
Task task = newWeeklyFromDue(1, dueDateTime, new WeekdayNum(0, Weekday.MO), new WeekdayNum(0, Weekday.WE));
assertEquals(newDayTime(2016, 8, 31, 0, 25), calculateNextDueDate(task));
}
@Test
public void testAdvancedRepeatWeeklyFromCompleteDateCompleteBefore() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 29, 0, 25);
DateTime completionDateTime = newDayTime(2016, 8, 28, 1, 9);
Task task = newWeeklyFromCompleted(1, dueDateTime, completionDateTime, new WeekdayNum(0, Weekday.MO), new WeekdayNum(0, Weekday.WE));
assertEquals(newDayTime(2016, 8, 29, 0, 25), calculateNextDueDate(task));
}
@Test
public void testAdvancedRepeatWeeklyFromCompleteDateCompleteAfter() throws ParseException {
DateTime dueDateTime = newDayTime(2016, 8, 29, 0, 25);
DateTime completionDateTime = newDayTime(2016, 9, 1, 1, 9);
Task task = newWeeklyFromCompleted(1, dueDateTime, completionDateTime, new WeekdayNum(0, Weekday.MO), new WeekdayNum(0, Weekday.WE));
assertEquals(newDayTime(2016, 9, 5, 0, 25), calculateNextDueDate(task));
}
private DateTime newDayTime(int year, int month, int day, int hour, int minute) {
return new DateTime(Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, new DateTime(year, month, day, hour, minute).getMillis()));
}
private DateTime calculateNextDueDate(Task task) throws ParseException {
return new DateTime(computeNextDueDate(task, task.sanitizedRecurrence(), task.repeatAfterCompletion()));
}
private Task newFromDue(Frequency frequency, int interval, DateTime dueDate) {
return new Task() {{
setRecurrence(getRecurrenceRule(frequency, interval, false));
setDueDate(dueDate.getMillis());
}};
}
private Task newWeeklyFromDue(int interval, DateTime dueDate, WeekdayNum... weekdays) {
return new Task() {{
setRecurrence(getRecurrenceRule(Frequency.WEEKLY, interval, false, weekdays));
setDueDate(dueDate.getMillis());
}};
}
private Task newFromCompleted(Frequency frequency, int interval, DateTime dueDate, DateTime completionDate) {
return new Task() {{
setRecurrence(getRecurrenceRule(frequency, interval, true));
setDueDate(dueDate.getMillis());
setCompletionDate(completionDate.getMillis());
}};
}
private Task newWeeklyFromCompleted(int interval, DateTime dueDate, DateTime completionDate, WeekdayNum... weekdays) {
return new Task() {{
setRecurrence(getRecurrenceRule(Frequency.WEEKLY, interval, true, weekdays));
setDueDate(dueDate.getMillis());
setCompletionDate(completionDate.getMillis());
}};
}
private String getRecurrenceRule(Frequency frequency, int interval, boolean fromCompletion, WeekdayNum... weekdays) {
RRule rrule = new RRule();
rrule.setFreq(frequency);
rrule.setInterval(interval);
if (weekdays != null) {
rrule.setByDay(asList(weekdays));
}
String result = rrule.toIcal();
if (fromCompletion) {
result += ";FROM=COMPLETION";
}
return result;
}
}

@ -0,0 +1,107 @@
package com.todoroo.astrid.repeats;
import android.annotation.SuppressLint;
import android.support.test.runner.AndroidJUnit4;
import com.google.ical.values.Frequency;
import com.google.ical.values.RRule;
import com.todoroo.astrid.data.Task;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.time.DateTime;
import java.text.ParseException;
import static com.google.ical.values.Frequency.DAILY;
import static com.google.ical.values.Frequency.HOURLY;
import static com.google.ical.values.Frequency.MINUTELY;
import static com.google.ical.values.Frequency.WEEKLY;
import static com.google.ical.values.Frequency.YEARLY;
import static com.todoroo.andlib.utility.DateUtilities.addCalendarMonthsToUnixtime;
import static com.todoroo.astrid.repeats.RepeatTaskHelper.computeNextDueDate;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static junit.framework.Assert.assertEquals;
@SuppressLint("NewApi")
@RunWith(AndroidJUnit4.class)
public class RepeatTaskHelperTest {
private final Task task = new Task();
private final long dueDate;
private final long completionDate;
{
completionDate = new DateTime(2014, 1, 7, 17, 17, 32, 900).getMillis();
dueDate = new DateTime(2013, 12, 31, 17, 17, 32, 900).getMillis();
task.setDueDate(dueDate);
task.setCompletionDate(completionDate);
}
@Test
public void testMinutelyRepeat() {
checkFrequency(6, MINUTES.toMillis(1), MINUTELY);
}
@Test
public void testHourlyRepeat() {
checkFrequency(6, HOURS.toMillis(1), HOURLY);
}
@Test
public void testDailyRepeat() {
checkFrequency(6, DAYS.toMillis(1), DAILY);
}
@Test
public void testWeeklyRepeat() {
checkFrequency(6, DAYS.toMillis(7), WEEKLY);
}
@Test
public void testMonthlyRepeat() {
assertEquals(
new DateTime(2014, 7, 7, 17, 17, 1, 0).getMillis(),
nextDueDate(6, Frequency.MONTHLY, true));
}
@Test
public void testMonthlyRepeatAtEndOfMonth() {
assertEquals(
new DateTime(2014, 6, 30, 17, 17, 1, 0).getMillis(),
nextDueDate(6, Frequency.MONTHLY, false));
}
@Test
public void testYearlyRepeat() {
checkExpected(6, addCalendarMonthsToUnixtime(dueDate, 6 * 12), YEARLY, false);
checkExpected(6, addCalendarMonthsToUnixtime(completionDate, 6 * 12), YEARLY, true);
}
private void checkFrequency(int count, long interval, Frequency frequency) {
checkExpected(count, dueDate + count * interval, frequency, false);
checkExpected(count, completionDate + count * interval, frequency, true);
}
private void checkExpected(int count, long expected, Frequency frequency, boolean repeatAfterCompletion) {
assertEquals(
new DateTime(expected)
.withSecondOfMinute(1)
.withMillisOfSecond(0)
.getMillis(),
nextDueDate(count, frequency, repeatAfterCompletion));
}
private long nextDueDate(int count, Frequency frequency, boolean repeatAfterCompletion) {
RRule rrule = new RRule();
rrule.setInterval(count);
rrule.setFreq(frequency);
try {
return computeNextDueDate(task, rrule.toIcal(), repeatAfterCompletion);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}

@ -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)
}
}

@ -0,0 +1,111 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.test.DatabaseTestCase;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.injection.TestComponent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import static junit.framework.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class QuickAddMarkupTest extends DatabaseTestCase {
@Inject TaskCreator taskCreator;
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
@Test
public void 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
public void 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();
}
@Test
public void testImportances() {
whenTitleIs("eat !1");
assertTitleBecomes("eat");
assertImportanceIs(Task.IMPORTANCE_SHOULD_DO);
whenTitleIs("super cool!");
assertTitleBecomes("super cool!");
whenTitleIs("stay alive !4");
assertTitleBecomes("stay alive");
assertImportanceIs(Task.IMPORTANCE_DO_OR_DIE);
}
@Test
public void testMixed() {
whenTitleIs("eat #food !2");
assertTitleBecomes("eat");
assertTagsAre("food");
assertImportanceIs(Task.IMPORTANCE_MUST_DO);
}
// --- helpers
private Task task;
private final ArrayList<String> tags = new ArrayList<>();
private void assertTagsAre(String... expectedTags) {
List<String> expected = Arrays.asList(expectedTags);
assertEquals(expected.toString(), tags.toString());
}
private void assertTitleBecomes(String title) {
assertEquals(title, task.getTitle());
}
private void whenTitleIs(String title) {
task = new Task();
task.setTitle(title);
tags.clear();
taskCreator.parseQuickAddMarkup(task, tags);
}
private void assertImportanceIs(int importance) {
assertEquals(importance, (int)task.getImportance());
}
}

@ -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,
)
)
}
}

@ -0,0 +1,604 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.service;
import android.support.test.runner.AndroidJUnit4;
import com.google.ical.values.Frequency;
import com.google.ical.values.RRule;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.tags.TagService;
import com.todoroo.astrid.test.DatabaseTestCase;
import com.todoroo.astrid.utility.TitleParser;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.R;
import org.tasks.injection.TestComponent;
import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime;
import java.util.ArrayList;
import java.util.Calendar;
import javax.inject.Inject;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotSame;
import static junit.framework.Assert.assertTrue;
import static org.tasks.date.DateTimeUtils.newDateTime;
@RunWith(AndroidJUnit4.class)
public class TitleParserTest extends DatabaseTestCase {
@Inject TaskDao taskDao;
@Inject TagService tagService;
@Inject Preferences preferences;
@Inject TaskCreator taskCreator;
@Override
public void setUp() {
super.setUp();
preferences.setStringFromInteger(R.string.p_default_urgency_key, 0);
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
/** test that completing a task w/ no regular expressions creates a simple task with no date, no repeat, no lists*/
@Test
public void testNoRegexes() throws Exception{
Task task = new Task();
Task nothing = new Task();
task.setTitle("Jog");
taskDao.save(task);
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
assertEquals(task.getRecurrence(), nothing.getRecurrence());
}
/** Tests correct date is parsed **/
@Test
public void testMonthDate() {
Task task = new Task();
String[] titleMonthStrings = {
"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 (int i = 0; i < 23; i++) {
String testTitle = "Jog on " + titleMonthStrings[i] + " 12.";
insertTitleAddTask(testTitle, task);
DateTime date = newDateTime(task.getDueDate());
assertEquals(date.getMonthOfYear(), i/2 + 1);
assertEquals(date.getDayOfMonth(), 12);
}
}
@Test
public void testMonthSlashDay() {
Task task = new Task();
for (int i = 1; i < 13; i++) {
String testTitle = "Jog on " + i + "/12/13";
insertTitleAddTask(testTitle, task);
DateTime date = newDateTime(task.getDueDate());
assertEquals(date.getMonthOfYear(), i);
assertEquals(date.getDayOfMonth(), 12);
assertEquals(date.getYear(), 2013);
}
}
@Test
public void testArmyTime() {
Task task = new Task();
String testTitle = "Jog on 23:21.";
insertTitleAddTask(testTitle, task);
DateTime date = newDateTime(task.getDueDate());
assertEquals(date.getHourOfDay(), 23);
assertEquals(date.getMinuteOfHour(), 21);
}
@Test
public void test_AM_PM() {
Task task = new Task();
String testTitle = "Jog at 8:33 PM.";
insertTitleAddTask(testTitle, task);
DateTime date = newDateTime(task.getDueDate());
assertEquals(date.getHourOfDay(), 20);
assertEquals(date.getMinuteOfHour(), 33);
}
@Test
public void test_at_hour() {
Task task = new Task();
String testTitle = "Jog at 8 PM.";
insertTitleAddTask(testTitle, task);
DateTime date = newDateTime(task.getDueDate());
assertEquals(date.getHourOfDay(), 20);
assertEquals(date.getMinuteOfHour(), 0);
}
@Test
public void test_oclock_AM() {
Task task = new Task();
String testTitle = "Jog at 8 o'clock AM.";
insertTitleAddTask(testTitle, task);
DateTime date = newDateTime(task.getDueDate());
assertEquals(date.getHourOfDay(), 8);
assertEquals(date.getMinuteOfHour(), 0);
}
@Test
public void test_several_forms_of_eight() {
Task task = new Task();
String[] testTitles = {
"Jog 8 AM",
"Jog 8 o'clock AM",
"at 8:00 AM"
};
for (String testTitle: testTitles) {
insertTitleAddTask(testTitle, task);
DateTime date = newDateTime(task.getDueDate());
assertEquals(date.getHourOfDay(), 8);
assertEquals(date.getMinuteOfHour(), 0);
}
}
@Test
public void test_several_forms_of_1230PM() {
Task task = new Task();
String[] testTitles = {
"Jog 12:30 PM",
"at 12:30 PM",
"Do something on 12:30 PM",
"Jog at 12:30 PM Friday"
};
for (String testTitle: testTitles) {
insertTitleAddTask(testTitle, task);
DateTime date = newDateTime(task.getDueDate());
assertEquals(date.getHourOfDay(), 12);
assertEquals(date.getMinuteOfHour(), 30);
}
}
private void insertTitleAddTask(String title, Task task) {
task.clear();
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
}
// ----------------Days begin----------------//
@Test
public void testDays() {
Calendar today = Calendar.getInstance();
Task task = new Task();
String title = "Jog today";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
DateTime date = newDateTime(task.getDueDate());
assertEquals(date.getDayOfWeek(), today.get(Calendar.DAY_OF_WEEK));
//Calendar starts 1-6, date.getDay() starts at 0
task = new Task();
title = "Jog tomorrow";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
date = newDateTime(task.getDueDate());
assertEquals((date.getDayOfWeek()) % 7, (today.get(Calendar.DAY_OF_WEEK)+1) % 7);
String[] days = {
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
};
String[] abrevDays = {
"sun.",
"mon.",
"tue.",
"wed.",
"thu.",
"fri.",
"sat."
};
for (int i = 1; i <= 6; i++){
task = new Task();
title = "Jog "+ days[i];
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
date = newDateTime(task.getDueDate());
assertEquals(date.getDayOfWeek(), i + 1);
task = new Task();
title = "Jog "+ abrevDays[i];
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
date = newDateTime(task.getDueDate());
assertEquals(date.getDayOfWeek(), i + 1);
}
}
//----------------Days end----------------//
//----------------Priority begin----------------//
/** tests all words using priority 0 */
@Test
public void testPriority0() throws Exception {
String[] acceptedStrings = {
"priority 0",
"least priority",
"lowest priority",
"bang 0"
};
Task task;
for (String acceptedString:acceptedStrings){
task = new Task();
String title = "Jog " + acceptedString;
task.setTitle(title); //test at end of task. should set importance.
taskCreator.createWithValues(task, null, title);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_NONE);
}
for (String acceptedString:acceptedStrings){
task = new Task();
String title = acceptedString + " jog";
task.setTitle(title); //test at beginning of task. should not set importance.
taskCreator.createWithValues(task, null, title);
assertNotSame(task.getImportance(),Task.IMPORTANCE_NONE);
}
}
@Test
public void testPriority1() throws Exception {
String[] acceptedStringsAtEnd = {
"priority 1",
"low priority",
"bang",
"bang 1"
};
String[] acceptedStringsAnywhere = {
"!1",
"!"
};
Task task;
for (String acceptedStringAtEnd:acceptedStringsAtEnd){
task = new Task();
task.setTitle("Jog " + acceptedStringAtEnd); //test at end of task. should set importance.
taskDao.save(task);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_SHOULD_DO);
}
for (String acceptedStringAtEnd:acceptedStringsAtEnd){
task = new Task();
task.setTitle(acceptedStringAtEnd + " jog"); //test at beginning of task. should not set importance.
taskDao.save(task);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_SHOULD_DO);
}
for (String acceptedStringAnywhere:acceptedStringsAnywhere){
task = new Task();
task.setTitle("Jog " + acceptedStringAnywhere); //test at end of task. should set importance.
taskDao.save(task);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_SHOULD_DO);
task.setTitle(acceptedStringAnywhere + " jog"); //test at beginning of task. should set importance.
taskDao.save(task);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_SHOULD_DO);
}
}
@Test
public void testPriority2() throws Exception {
String[] acceptedStringsAtEnd = {
"priority 2",
"high priority",
"bang bang",
"bang 2"
};
String[] acceptedStringsAnywhere = {
"!2",
"!!"
};
Task task;
for (String acceptedStringAtEnd:acceptedStringsAtEnd){
task = new Task();
String title = "Jog " + acceptedStringAtEnd;
task.setTitle(title); //test at end of task. should set importance.
taskCreator.createWithValues(task, null, title);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_MUST_DO);
task = new Task();
title = acceptedStringAtEnd + " jog";
task.setTitle(title); //test at beginning of task. should not set importance.
taskCreator.createWithValues(task, null, title);
assertNotSame(task.getImportance(), Task.IMPORTANCE_MUST_DO);
}
for (String acceptedStringAnywhere:acceptedStringsAnywhere){
task = new Task();
String title = "Jog " + acceptedStringAnywhere;
task.setTitle(title); //test at end of task. should set importance.
taskCreator.createWithValues(task, null, title);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_MUST_DO);
title = acceptedStringAnywhere + " jog";
task.setTitle(title); //test at beginning of task. should set importance.
taskCreator.createWithValues(task, null, title);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_MUST_DO);
}
}
@Test
public void testPriority3() throws Exception {
String[] acceptedStringsAtEnd = {
"priority 3",
"highest priority",
"bang bang bang",
"bang 3",
"bang bang bang bang bang bang bang"
};
String[] acceptedStringsAnywhere = {
"!3",
"!!!",
"!6",
"!!!!!!!!!!!!!"
};
Task task;
for (String acceptedStringAtEnd:acceptedStringsAtEnd){
task = new Task();
String title = "Jog " + acceptedStringAtEnd;
task.setTitle(title); //test at end of task. should set importance.
taskCreator.createWithValues(task, null, title);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_DO_OR_DIE);
task = new Task();
title = acceptedStringAtEnd + " jog";
task.setTitle(title); //test at beginning of task. should not set importance.
taskCreator.createWithValues(task, null, title);
assertNotSame(task.getImportance(), Task.IMPORTANCE_DO_OR_DIE);
}
for (String acceptedStringAnywhere:acceptedStringsAnywhere){
task = new Task();
String title = "Jog " + acceptedStringAnywhere;
task.setTitle(title); //test at end of task. should set importance.
taskCreator.createWithValues(task, null, title);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_DO_OR_DIE);
title = acceptedStringAnywhere + " jog";
task.setTitle(title); //test at beginning of task. should set importance.
taskCreator.createWithValues(task, null, title);
assertEquals((int)task.getImportance(), Task.IMPORTANCE_DO_OR_DIE);
}
}
//----------------Priority end----------------//
//----------------Repeats begin----------------//
/** test daily repeat from due date, but with no due date set */
@Test
public void testDailyWithNoDueDate() throws Exception {
Task task = new Task();
String title = "Jog daily";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
RRule rrule = new RRule();
rrule.setFreq(Frequency.DAILY);
rrule.setInterval(1);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
title = "Jog every day";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
for (int i = 1; i <= 12; i++){
title = "Jog every " + i + " days.";
task.setTitle(title);
rrule.setInterval(i);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
task = new Task();
}
}
/** test weekly repeat from due date, with no due date & time set */
@Test
public void testWeeklyWithNoDueDate() throws Exception {
Task task = new Task();
String title = "Jog weekly";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
RRule rrule = new RRule();
rrule.setFreq(Frequency.WEEKLY);
rrule.setInterval(1);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
title = "Jog every week";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
for (int i = 1; i <= 12; i++){
title = "Jog every " + i + " weeks";
task.setTitle(title);
rrule.setInterval(i);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
task = new Task();
}
}
/** test hourly repeat from due date, with no due date but no time */
@Test
public void testMonthlyFromNoDueDate() throws Exception {
Task task = new Task();
String title = "Jog monthly";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
RRule rrule = new RRule();
rrule.setFreq(Frequency.MONTHLY);
rrule.setInterval(1);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
title = "Jog every month";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
for (int i = 1; i <= 12; i++){
title = "Jog every " + i + " months";
task.setTitle(title);
rrule.setInterval(i);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertFalse(task.hasDueTime());
assertFalse(task.hasDueDate());
task = new Task();
}
}
@Test
public void testDailyFromDueDate() throws Exception {
Task task = new Task();
String title = "Jog daily starting from today";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
RRule rrule = new RRule();
rrule.setFreq(Frequency.DAILY);
rrule.setInterval(1);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertTrue(task.hasDueDate());
task.clearValue(Task.ID);
task.clearValue(Task.UUID);
title = "Jog every day starting from today";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertTrue(task.hasDueDate());
for (int i = 1; i <= 12; i++){
title = "Jog every " + i + " days starting from today";
task.setTitle(title);
rrule.setInterval(i);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertTrue(task.hasDueDate());
task = new Task();
}
}
@Test
public void testWeeklyFromDueDate() throws Exception {
Task task = new Task();
String title = "Jog weekly starting from today";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
RRule rrule = new RRule();
rrule.setFreq(Frequency.WEEKLY);
rrule.setInterval(1);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertTrue(task.hasDueDate());
task.clearValue(Task.ID);
task.clearValue(Task.UUID);
title = "Jog every week starting from today";
task.setTitle(title);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertTrue(task.hasDueDate());
for (int i = 1; i <= 12; i++){
title = "Jog every " + i + " weeks starting from today";
task.setTitle(title);
rrule.setInterval(i);
taskCreator.createWithValues(task, null, title);
assertEquals(task.getRecurrence(), rrule.toIcal());
assertTrue(task.hasDueDate());
task = new Task();
}
}
//----------------Repeats end----------------//
//----------------Tags begin----------------//
/** tests all words using priority 0 */
@Test
public void testTagsPound() throws Exception {
String[] acceptedStrings = {
"#tag",
"#a",
"#(a cool tag)",
"#(cool)"
};
Task task;
for (String acceptedString : acceptedStrings) {
task = new Task();
task.setTitle("Jog " + acceptedString); //test at end of task. should set importance.
ArrayList<String> tags = new ArrayList<>();
TitleParser.listHelper(tagService, task, tags);
String tag = TitleParser.trimParenthesis(acceptedString);
assertTrue("test pound at failed for string: " + acceptedString + " for tags: " + tags.toString(), tags.contains(tag));
}
}
/** tests all words using priority 0 */
@Test
public void testTagsAt() throws Exception {
String[] acceptedStrings = {
"@tag",
"@a",
"@(a cool tag)",
"@(cool)"
};
Task task;
for (String acceptedString : acceptedStrings) {
task = new Task();
task.setTitle("Jog " + acceptedString); //test at end of task. should set importance.
ArrayList<String> tags = new ArrayList<>();
TitleParser.listHelper(tagService, task, tags);
String tag = TitleParser.trimParenthesis(acceptedString);
assertTrue("testTagsAt failed for string: " + acceptedString+ " for tags: " + tags.toString(), tags.contains(tag));
}
}
}

@ -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()
}
}

@ -0,0 +1,73 @@
package com.todoroo.astrid.subtasks;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskListMetadata;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.injection.TestComponent;
import javax.inject.Inject;
import static junit.framework.Assert.assertEquals;
@RunWith(AndroidJUnit4.class)
public class SubtasksHelperTest extends SubtasksTestCase {
@Inject TaskDao taskDao;
@Override
public void setUp() {
super.setUp();
createTasks();
TaskListMetadata m = new TaskListMetadata();
m.setFilter(TaskListMetadata.FILTER_ID_ALL);
updater.initializeFromSerializedTree(m, filter, SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE));
}
private void createTask(String title, String uuid) {
Task t = new Task();
t.setTitle(title);
t.setUuid(uuid);
taskDao.save(t);
}
private void 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
}
private static final String[] EXPECTED_ORDER = { "-1", "1", "2", "3", "4", "5", "6" };
@Test
public void testOrderedIdArray() {
String[] ids = SubtasksHelper.getStringIdArray(DEFAULT_SERIALIZED_TREE);
assertEquals(EXPECTED_ORDER.length, ids.length);
for (int i = 0; i < EXPECTED_ORDER.length; i++) {
assertEquals(EXPECTED_ORDER[i], ids[i]);
}
}
// Default order: "[-1, [1, 2, [3, 4]], 5, 6]"
private static String EXPECTED_REMOTE = "[\"-1\", [\"6\", \"4\", [\"3\", \"1\"]], \"2\", \"5\"]".replaceAll("\\s", "");
@Test
public void testLocalToRemoteIdMapping() {
String mapped = SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE).replaceAll("\\s", "");
assertEquals(EXPECTED_REMOTE, mapped);
}
@Override
protected void inject(TestComponent component) {
super.inject(component);
component.inject(this);
}
}

@ -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(), "")
}
}

@ -0,0 +1,134 @@
package com.todoroo.astrid.subtasks;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.data.TaskListMetadata;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.inject.Inject;
@RunWith(AndroidJUnit4.class)
public class SubtasksMovingTest extends SubtasksTestCase {
@Inject TaskDao taskDao;
private Task A, B, C, D, E, F;
// @Override
protected void disabled_setUp() {
super.setUp();
createTasks();
TaskListMetadata m = new TaskListMetadata();
m.setFilter(TaskListMetadata.FILTER_ID_ALL);
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 void createTasks() {
A = createTask("A");
B = createTask("B");
C = createTask("C");
D = createTask("D");
E = createTask("E");
F = createTask("F");
}
private Task createTask(String title) {
Task task = new Task();
task.setTitle(title);
taskDao.save(task);
return task;
}
private void whenTriggerMoveBefore(Task target, Task before) {
String beforeId = (before == null ? "-1" : before.getUuid());
updater.moveTo(null, filter, target.getUuid(), beforeId);
}
/* Starting State (see SubtasksTestCase):
*
* A
* B
* C
* D
* E
* F
*/
@Ignore
@Test
public void 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);
}
@Ignore
@Test
public void 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);
}
@Ignore
@Test
public void 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);
}
@Ignore
@Test
public void 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);
}
@Ignore
@Test
public void 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,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)
}
}

@ -0,0 +1,66 @@
package com.todoroo.astrid.subtasks;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.core.BuiltInFilterExposer;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.dao.TaskListMetadataDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.subtasks.SubtasksFilterUpdater.Node;
import com.todoroo.astrid.test.DatabaseTestCase;
import org.tasks.injection.TestComponent;
import org.tasks.preferences.Preferences;
import javax.inject.Inject;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
/**
* Contains useful methods common to all subtasks tests
* @author Sam
*
*/
public class SubtasksTestCase extends DatabaseTestCase {
@Inject TaskListMetadataDao taskListMetadataDao;
@Inject TaskDao taskDao;
@Inject Preferences preferences;
protected SubtasksFilterUpdater updater;
protected Filter filter;
/* Starting State:
*
* A
* B
* C
* D
* E
* F
*/
public static final String DEFAULT_SERIALIZED_TREE = "[-1, [1, 2, [3, 4]], 5, 6]".replaceAll("\\s", "");
@Override
public void setUp() {
super.setUp();
filter = BuiltInFilterExposer.getMyTasksFilter(getTargetContext().getResources());
preferences.clear(SubtasksFilterUpdater.ACTIVE_TASKS_ORDER);
updater = new SubtasksFilterUpdater(taskListMetadataDao, taskDao);
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
protected void expectParentAndPosition(Task task, Task parent, int positionInParent) {
String parentId = (parent == null ? "-1" : parent.getUuid());
Node n = updater.findNodeForTask(task.getUuid());
assertNotNull("No node found for task " + task.getTitle(), n);
assertEquals("Parent mismatch", parentId, n.parent.uuid);
assertEquals("Position mismatch", positionInParent, n.parent.children.indexOf(n));
}
}

@ -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(), "")
}
}

@ -0,0 +1,50 @@
package com.todoroo.astrid.sync;
import com.todoroo.astrid.dao.TagDataDao;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.test.DatabaseTestCase;
import org.tasks.injection.TestComponent;
import javax.inject.Inject;
public class NewSyncTestCase extends DatabaseTestCase {
@Inject TaskDao taskDao;
@Inject TagDataDao tagDataDao;
protected Task createTask(String title) {
Task task = new Task();
task.setTitle(title);
task.setImportance(SYNC_TASK_IMPORTANCE);
taskDao.createNew(task);
return task;
}
public static final String SYNC_TASK_TITLE = "new title";
public static final int SYNC_TASK_IMPORTANCE = Task.IMPORTANCE_MUST_DO;
protected Task createTask() {
return createTask(SYNC_TASK_TITLE);
}
protected TagData createTagData(String name) {
TagData tag = new TagData();
tag.setName(name);
tagDataDao.createNew(tag);
return tag;
}
protected TagData createTagData() {
return createTagData("new tag");
}
@Override
protected void inject(TestComponent component) {
component.inject(this);
}
}

@ -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
}
}

@ -0,0 +1,29 @@
package com.todoroo.astrid.sync;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.astrid.data.RemoteModel;
import com.todoroo.astrid.data.TagData;
import com.todoroo.astrid.data.Task;
import org.junit.Test;
import org.junit.runner.RunWith;
import static junit.framework.Assert.assertFalse;
@RunWith(AndroidJUnit4.class)
public class SyncModelTest extends NewSyncTestCase {
@Test
public void testCreateTaskMakesUuid() {
Task task = createTask();
assertFalse(RemoteModel.NO_UUID.equals(task.getUUID()));
}
@Test
public void testCreateTagMakesUuid() {
TagData tag = createTagData();
assertFalse(RemoteModel.NO_UUID.equals(tag.getUUID()));
}
}

@ -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)
}
}

@ -0,0 +1,34 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.test;
import com.todoroo.astrid.dao.Database;
import org.junit.After;
import org.tasks.injection.InjectingTestCase;
import static android.support.test.InstrumentationRegistry.getTargetContext;
public abstract class DatabaseTestCase extends InjectingTestCase {
protected Database database;
@Override
public void setUp() {
super.setUp();
database = component.getDatabase();
database.close();
getTargetContext().deleteDatabase(database.getName());
database.openForWriting();
}
@After
public void tearDown() {
database.close();
}
}

@ -0,0 +1,31 @@
package org.tasks;
import org.tasks.time.DateTime;
import org.tasks.time.DateTimeUtils;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
public class Freeze {
public static Freeze freezeClock() {
return freezeAt(currentTimeMillis());
}
public static Freeze freezeAt(DateTime dateTime) {
return freezeAt(dateTime.getMillis());
}
public static Freeze freezeAt(long millis) {
DateTimeUtils.setCurrentMillisFixed(millis);
return new Freeze();
}
public static void thaw() {
DateTimeUtils.setCurrentMillisSystem();
}
@SuppressWarnings("UnusedParameters")
public void thawAfter(Snippet snippet) {
thaw();
}
}

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

@ -0,0 +1,53 @@
package org.tasks;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.data.Table;
import com.todoroo.astrid.data.RemoteModel;
import java.util.Set;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.fail;
@SuppressLint("NewApi")
public class RemoteModelHelpers {
public static Property[] asQueryProperties(Table table, ContentValues contentValues) {
Set<String> keys = contentValues.keySet();
Property[] result = new Property[keys.size()];
int index = 0;
for (String key : keys) {
result[index++] = new Property.StringProperty(table, key);
}
return result;
}
public static void compareRemoteModel(RemoteModel expected, RemoteModel actual) {
compareContentValues(expected.getSetValues(), actual.getSetValues());
compareContentValues(expected.getDatabaseValues(), actual.getDatabaseValues());
}
private static void compareContentValues(ContentValues expected, ContentValues actual) {
if (expected == null && actual == null) {
return;
}
if (expected == null || actual == null) {
fail();
}
for (String key : expected.keySet()) {
Object entry = expected.get(key);
if (entry instanceof Integer) {
assertEquals(entry, actual.getAsInteger(key));
} else if (entry instanceof String) {
assertEquals(entry, actual.getAsString(key));
} else if (entry instanceof Long) {
assertEquals(entry, actual.getAsLong(key));
} else {
fail("Unhandled property type: " + entry.getClass());
}
}
}
}

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

Loading…
Cancel
Save