mirror of https://github.com/tasks/tasks
Compare commits
No commits in common. 'main' and '5.0.0' have entirely different histories.
@ -1,4 +0,0 @@
|
|||||||
github: abaker
|
|
||||||
liberapay: tasks
|
|
||||||
patreon: tasks
|
|
||||||
custom: tasks.org/donate
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
name: Assemble bundle
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
uses: ./.github/workflows/check.yml
|
|
||||||
bundle:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [ check ]
|
|
||||||
steps:
|
|
||||||
- name: Decode Keystore
|
|
||||||
run: |
|
|
||||||
echo ${{ secrets.KEY_STORE }} | base64 -di > "${RUNNER_TEMP}"/keystore.jks
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
bundler-cache: true
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '21'
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
|
|
||||||
- name: Bundle
|
|
||||||
env:
|
|
||||||
KEY_PATH: ${{ runner.temp }}/keystore.jks
|
|
||||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
|
||||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
|
||||||
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
||||||
MAPBOX_KEY: ${{ secrets.MAPBOX_KEY }}
|
|
||||||
GOOGLE_KEY: ${{ secrets.GOOGLE_KEY }}
|
|
||||||
run: bundle exec fastlane bundle
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
with:
|
|
||||||
name: release
|
|
||||||
path: |
|
|
||||||
app/build/outputs/**
|
|
||||||
wear/build/outputs/**
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
name: Run automated checks
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
bundler-cache: true
|
|
||||||
|
|
||||||
- name: Set up JDK 21
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '21'
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
|
|
||||||
- name: Lint checks
|
|
||||||
run: bundle exec fastlane lint
|
|
||||||
|
|
||||||
- name: Archive lint reports
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: lint-reports
|
|
||||||
path: app/build/reports/*.html
|
|
||||||
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
flavor: [Googleplay, Generic]
|
|
||||||
api-level: [29]
|
|
||||||
steps:
|
|
||||||
- name: checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Set up JDK 21
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '21'
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
|
|
||||||
- name: Enable KVM
|
|
||||||
run: |
|
|
||||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
|
||||||
sudo udevadm control --reload-rules
|
|
||||||
sudo udevadm trigger --name-match=kvm
|
|
||||||
|
|
||||||
# - name: AVD cache
|
|
||||||
# uses: actions/cache@v4
|
|
||||||
# id: avd-cache
|
|
||||||
# with:
|
|
||||||
# path: |
|
|
||||||
# ~/.android/avd/*
|
|
||||||
# ~/.android/adb*
|
|
||||||
# key: avd-${{ matrix.api-level }}
|
|
||||||
#
|
|
||||||
# - name: create AVD and generate snapshot for caching
|
|
||||||
# if: steps.avd-cache.outputs.cache-hit != 'true'
|
|
||||||
# uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
# with:
|
|
||||||
# api-level: ${{ matrix.api-level }}
|
|
||||||
# force-avd-creation: false
|
|
||||||
# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
# disable-animations: false
|
|
||||||
# script: echo "Generated AVD snapshot for caching."
|
|
||||||
|
|
||||||
- name: run tests
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2
|
|
||||||
with:
|
|
||||||
api-level: ${{ matrix.api-level }}
|
|
||||||
force-avd-creation: false
|
|
||||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
|
||||||
disable-animations: true
|
|
||||||
script: ./gradlew -Pcoverage app:test${{ matrix.flavor }}DebugUnitTest app:connected${{ matrix.flavor }}DebugAndroidTest
|
|
||||||
|
|
||||||
- name: Upload test reports
|
|
||||||
uses: actions/upload-artifact@v6
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: test-reports-${{ matrix.flavor }}
|
|
||||||
path: app/build/reports/**
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
name: Update Dependency Diff
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'gradle/libs.versions.toml'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'gradle/libs.versions.toml'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update-deps:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
ref: ${{ github.head_ref }}
|
|
||||||
|
|
||||||
- name: Set up JDK
|
|
||||||
uses: actions/setup-java@v5
|
|
||||||
with:
|
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '21'
|
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
|
|
||||||
- name: Update dependency diffs
|
|
||||||
run: ./update_dependency_diff
|
|
||||||
|
|
||||||
- name: Commit changes
|
|
||||||
run: |
|
|
||||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git config --local user.name "github-actions[bot]"
|
|
||||||
git add deps_*.txt
|
|
||||||
git diff --staged --quiet || git commit -m "Update dependency diffs"
|
|
||||||
git push
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
name: Deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
FASTLANE: ${{ secrets.FASTLANE }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
bundle:
|
|
||||||
uses: ./.github/workflows/bundle.yml
|
|
||||||
secrets: inherit
|
|
||||||
deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [ bundle ]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
- name: Fastlane key
|
|
||||||
run: |
|
|
||||||
echo "$FASTLANE" > ./fastlane.json
|
|
||||||
- uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
bundler-cache: true
|
|
||||||
- uses: actions/download-artifact@v7
|
|
||||||
with:
|
|
||||||
name: release
|
|
||||||
path: .
|
|
||||||
- name: Deploy
|
|
||||||
run: bundle exec fastlane deploy
|
|
||||||
@ -1,14 +1,7 @@
|
|||||||
.kotlin
|
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
.gradle
|
.gradle
|
||||||
build/
|
build/
|
||||||
*.apk
|
*.apk
|
||||||
*.apks
|
|
||||||
*.aab
|
|
||||||
local.properties
|
local.properties
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
/captures/
|
|
||||||
/fastlane/report.xml
|
|
||||||
/compose-metrics/
|
|
||||||
.DS_Store
|
|
||||||
@ -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.
|
[](https://travis-ci.org/tasks/tasks)
|
||||||
|
|
||||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png"
|
[](https://pledgie.com/campaigns/24281)
|
||||||
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)
|
|
||||||
|
|
||||||
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>
|
||||||
|
[](https://www.amazon.com/gp/product/B00QHGTL7O/ref=mas_pm_tasks_astrid_to_do_list_clone)
|
||||||
|
|
||||||
---
|
Visit the wiki to
|
||||||
|
* [help with translations](https://github.com/tasks/tasks/wiki/Translations)
|
||||||
[](https://tasks.org/docs/donate)
|
* [become a beta tester](https://github.com/tasks/tasks/wiki/Beta-Testing)
|
||||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=alex@tasks.org)
|
* [get started with development](https://github.com/tasks/tasks/wiki/Getting-Started-with-Development)
|
||||||
[](https://liberapay.com/tasks/donate)
|
|
||||||
|
|
||||||
[](https://github.com/tasks/tasks/actions/workflows/bundle.yml) [](https://hosted.weblate.org/engage/tasks/?utm_source=widget)
|
|
||||||
|
|
||||||
### Contributing
|
|
||||||
|
|
||||||
Contributions are always welcome! Whether translations, code changes, bug reports, feature requests, or otherwise, your help is appreciated. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
### Communication
|
|
||||||
|
|
||||||
You can submit questions to [GitHub Discussions](https://github.com/tasks/tasks/discussions).
|
|
||||||
|
|
||||||
If you have a suggestion or want to report a bug, please see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
||||||
|
|||||||
@ -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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<lint>
|
<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="MissingQuantity" severity="ignore"/>
|
||||||
<issue id="ImpliedQuantity" severity="ignore"/>
|
<issue id="ImpliedQuantity" severity="ignore"/>
|
||||||
|
|
||||||
<issue id="InvalidPackage">
|
|
||||||
<ignore regexp="net.fortuna.ical4j.util.JCacheTimeZoneCache"/>
|
|
||||||
</issue>
|
|
||||||
</lint>
|
</lint>
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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…
Reference in New Issue