Compare commits

..

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

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

3
.gitignore vendored

@ -1,4 +1,3 @@
.kotlin
.idea .idea
*.iml *.iml
.gradle .gradle
@ -10,5 +9,3 @@ local.properties
Thumbs.db Thumbs.db
/captures/ /captures/
/fastlane/report.xml /fastlane/report.xml
/compose-metrics/
.DS_Store

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CMakeSettings">
<configurations>
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

@ -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,99 @@
# https://github.com/andstatus/todoagenda/blob/aa2bc56effa379e145eecb104cc5bac33975b7aa/.travis.yml
# Based on https://travis-ci.org/ankidroid/Anki-Android/builds/624268367
# See also https://travis-ci.community/t/is-android-28-emulator-supported/1718/6
sudo: true
language: bash
# ignored on non-linux platforms, but bionic is required for nested virtualization
dist: bionic
stages:
- install
- test
- cache
env:
global:
- COMPILE_API=29
- ANDROID_BUILD_TOOLS=29.0.2
- ABI=x86_64
- ADB_INSTALL_TIMEOUT=8
- ANDROID_HOME=${HOME}/android-sdk
- ANDROID_TOOLS_URL="https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip"
- EMU_FLAVOR=default # use google_apis flavor if no default flavor emulator
- GRAVIS="https://raw.githubusercontent.com/DanySK/Gravis-CI/master/"
- JDK="1.8"
- TOOLS=${ANDROID_HOME}/tools
# PATH order is incredibly important. e.g. the 'emulator' script exists in more than one place!
- PATH=${ANDROID_HOME}:${ANDROID_HOME}/emulator:${TOOLS}:${TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}
matrix:
- API=29
before_install:
# This section may run on all platforms, and may run for unit tests or for coverage finalization
# It should not make assumptions about os platform or desired tool installation
# Set up JDK 8 for Android SDK - Java is universally needed: codacy, unit tests, emulators
- curl "${GRAVIS}.install-jdk-travis.sh" --output ~/.install-jdk-travis.sh
- export TARGET_JDK="${JDK}"
- JDK="1.8"
- source ~/.install-jdk-travis.sh
# Set up Android SDK - this is needed everywhere but coverage finalization, so toggle on that
- wget -q "${ANDROID_TOOLS_URL}" -O android-sdk-tools.zip
- unzip -q android-sdk-tools.zip -d ${ANDROID_HOME}
- rm android-sdk-tools.zip
- mkdir ~/.android # avoid harmless sdkmanager warning
- echo 'count=0' > ~/.android/repositories.cfg # avoid harmless sdkmanager warning
- yes | sdkmanager --licenses >/dev/null # accept all sdkmanager warnings
- echo y | sdkmanager --no_https "platform-tools" >/dev/null
- echo y | sdkmanager --no_https "tools" >/dev/null # A second time per Travis docs, gets latest versions
- echo y | sdkmanager --no_https "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null # Implicit gradle dependency - gradle drives changes
- echo y | sdkmanager --no_https "platforms;android-${COMPILE_API}" >/dev/null # We need the API of the current compileSdkVersion from gradle.properties
install:
# In our setup, install only runs on matrix entries we want full emulator tests on
# That only happens currently on linux, so this section can assume linux + emulator is desired
# Download required emulator tools
- echo y | sdkmanager --no_https "platforms;android-$API" >/dev/null # We need the API of the emulator we will run
- echo y | sdkmanager --no_https "emulator" >/dev/null
- echo y | sdkmanager --no_https "system-images;android-$API;$EMU_FLAVOR;$ABI" >/dev/null # install our emulator
# Set up KVM on linux for hardware acceleration. Manually here so it only happens for emulator tests, takes ~30s
- sudo -E apt-get -yq --no-install-suggests --no-install-recommends install bridge-utils libpulse0 libvirt-bin qemu-kvm virtinst ubuntu-vm-builder
- sudo adduser $USER libvirt
- sudo adduser $USER kvm
# Create an Android emulator
- echo no | avdmanager create avd --force -n test -k "system-images;android-$API;$EMU_FLAVOR;$ABI" -c 10M
- |
EMU_PARAMS="-verbose -no-snapshot -no-window -camera-back none -camera-front none -selinux permissive -qemu -m 2048"
EMU_COMMAND="emulator"
# This double "sudo" monstrosity is used to have Travis execute the
# emulator with its new group permissions and help preserve the rule
# of least privilege.
sudo -E sudo -u $USER -E bash -c "${ANDROID_HOME}/emulator/${EMU_COMMAND} -avd test ${AUDIO} ${EMU_PARAMS} &"
# Wait for emulator to be ready
- ./.wait_for_emulator.sh
- adb shell input keyevent 82 &
# Switch back to our target JDK version to build and run tests
- JDK="${TARGET_JDK}"
- source ~/.install-jdk-travis.sh
script:
- ./gradlew :app:lintGoogleplayRelease
- ./gradlew :app:jacocoTestReportGoogleplayDebug
- ./gradlew -Pcoverage :app:createGoogleplayDebugAndroidTestCoverageReport
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- curl "${GRAVIS}.clean_gradle_cache.sh" --output ~/.clean_gradle_cache.sh
- bash ~/.clean_gradle_cache.sh > /dev/null
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
after_success:
- bash <(curl -s https://codecov.io/bash)

@ -0,0 +1,24 @@
#!/bin/bash
set +e
bootanim=""
failcounter=0
timeout_in_sec=600 # 10 minutes
until [[ "$bootanim" =~ "stopped" ]]; do
bootanim=`adb -e shell getprop init.svc.bootanim 2>&1 &`
if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline"
|| "$bootanim" =~ "running" || "$bootanim" =~ "error: no emulators found" ]]; then
let "failcounter += 1"
echo "Waiting for emulator to start"
if [[ $failcounter -gt timeout_in_sec ]]; then
echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator"
exit 1
fi
fi
sleep 1
done
echo "Emulator is ready"

File diff suppressed because it is too large Load Diff

@ -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 +1,3 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "fastlane" gem "fastlane"
gem "abbrev"

@ -1,230 +1,161 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.7) CFPropertyList (3.0.2)
base64 addressable (2.7.0)
nkf public_suffix (>= 2.0.2, < 5.0)
rexml
abbrev (0.1.2)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.4.0) babosa (1.0.3)
aws-partitions (1.1196.0) claide (1.0.3)
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) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander (4.6.0) commander-fastlane (4.4.6)
highline (~> 2.0.0) highline (~> 1.7.2)
declarative (0.0.20) declarative (0.0.10)
digest-crc (0.7.0) declarative-option (0.1.0)
rake (>= 12.0.0, < 14.0.0) digest-crc (0.4.1)
domain_name (0.6.20240107) domain_name (0.5.20190701)
dotenv (2.8.1) unf (>= 0.0.5, < 1.0.0)
emoji_regex (3.2.3) dotenv (2.7.5)
excon (0.112.0) emoji_regex (1.0.1)
faraday (1.10.4) excon (0.72.0)
faraday-em_http (~> 1.0) faraday (0.17.3)
faraday-em_synchrony (~> 1.0) multipart-post (>= 1.2, < 3)
faraday-excon (~> 1.1) faraday-cookie_jar (0.0.6)
faraday-httpclient (~> 1.0) faraday (>= 0.7.4)
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) http-cookie (~> 1.0.0)
faraday-em_http (1.0.0) faraday_middleware (0.13.1)
faraday-em_synchrony (1.0.1) faraday (>= 0.7.4, < 1.0)
faraday-excon (1.1.0) fastimage (2.1.7)
faraday-httpclient (1.0.1) fastlane (2.141.0)
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) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.3, < 3.0.0)
artifactory (~> 3.0) babosa (>= 1.0.2, < 2.0.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0) bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2) colored
commander (~> 4.6) commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0) dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0) emoji_regex (>= 0.1, < 2.0)
excon (>= 0.71.0, < 1.0.0) excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0) faraday (~> 0.17)
faraday-cookie_jar (~> 0.0.6) faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0) faraday_middleware (~> 0.13.1)
fastimage (>= 2.1.0, < 3.0.0) fastimage (>= 2.1.0, < 3.0.0)
fastlane-sirp (>= 1.0.0)
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3) google-api-client (>= 0.29.2, < 0.37.0)
google-apis-playcustomapp_v1 (~> 0.1) google-cloud-storage (>= 1.15.0, < 2.0.0)
google-cloud-env (>= 1.6.0, < 2.0.0) highline (>= 1.7.2, < 2.0.0)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0) json (< 3.0.0)
jwt (>= 2.1.0, < 3) jwt (~> 2.1.0)
mini_magick (>= 4.9.4, < 5.0.0) mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0) multi_xml (~> 0.5)
naturally (~> 2.2) multipart-post (~> 2.0.0)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0) plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0) public_suffix (~> 2.0.0)
security (= 0.1.5) rubyzip (>= 1.3.0, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3) simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (~> 3) terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0) tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0) word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0) xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.1) xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) xcpretty-travis-formatter (>= 0.0.3)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.54.0) google-api-client (0.36.4)
google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0) mini_mime (~> 1.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.0)
rexml signet (~> 0.12)
google-apis-iamcredentials_v1 (0.17.0) google-cloud-core (1.5.0)
google-apis-core (>= 0.11.0, < 2.a) google-cloud-env (~> 1.0)
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-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.3.0)
faraday (>= 0.17.3, < 3.0) faraday (~> 0.11)
google-cloud-errors (1.5.0) google-cloud-errors (1.0.0)
google-cloud-storage (1.47.0) google-cloud-storage (1.25.1)
addressable (~> 2.8) addressable (~> 2.5)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-api-client (~> 0.33)
google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.2)
google-cloud-core (~> 1.6) googleauth (~> 0.9)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.8.1) googleauth (0.10.0)
faraday (>= 0.17.3, < 3.a) faraday (~> 0.12)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (~> 0.12)
highline (2.0.3) highline (1.7.10)
http-cookie (1.0.8) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.9.0) httpclient (2.8.3)
mutex_m json (2.3.0)
jmespath (1.6.2) jwt (2.1.0)
json (2.12.2) memoist (0.16.2)
jwt (2.10.2) mini_magick (4.10.1)
base64 mini_mime (1.0.2)
logger (1.7.0) multi_json (1.14.1)
mini_magick (4.13.2) multi_xml (0.6.0)
mini_mime (1.1.5) multipart-post (2.0.0)
multi_json (1.15.0) nanaimo (0.2.6)
multipart-post (2.4.1) naturally (2.2.0)
mutex_m (0.3.0) os (1.0.1)
nanaimo (0.4.0) plist (3.5.0)
naturally (2.3.0) public_suffix (2.0.5)
nkf (0.2.0) representable (3.0.4)
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) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) declarative-option (< 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.4.2) rouge (2.0.7)
rouge (3.28.0) rubyzip (1.3.0)
ruby2_keywords (0.0.5) security (0.1.3)
rubyzip (2.4.1) signet (0.12.0)
security (0.1.5) addressable (~> 2.3)
signet (0.20.0) faraday (~> 0.9)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
multi_json (~> 1.10) multi_json (~> 1.10)
simctl (1.6.10) simctl (1.6.8)
CFPropertyList CFPropertyList
naturally naturally
sysrandom (1.0.5) slack-notifier (2.3.2)
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (3.0.2) terminal-table (1.8.0)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-screen (0.8.2) tty-screen (0.7.1)
tty-spinner (0.9.3) tty-spinner (0.9.3)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.6.0) unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.1)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.27.0) xcodeproj (1.15.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1) colored2 (~> 3.1)
nanaimo (~> 0.4.0) nanaimo (~> 0.2.6)
rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0)
xcpretty (0.4.1) rouge (~> 2.0.7)
rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7) xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS PLATFORMS
ruby ruby
DEPENDENCIES DEPENDENCIES
abbrev
fastlane fastlane
BUNDLED WITH BUNDLED WITH
2.6.9 2.1.2

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

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

@ -1,32 +1,25 @@
@file:Suppress("UnstableApiUsage")
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.android.application) id("com.android.application")
id("com.google.gms.google-services") id("checkstyle")
id("com.google.firebase.crashlytics") id("com.google.firebase.crashlytics")
kotlin("android") kotlin("android")
id("dagger.hilt.android.plugin") kotlin("kapt")
id("com.google.android.gms.oss-licenses-plugin") id("com.cookpad.android.plugin.license-tools") version "1.2.2"
alias(libs.plugins.kotlin.parcelize) id("com.github.ben-manes.versions") version "0.28.0"
alias(libs.plugins.ksp) id("com.vanniktech.android.junit.jacoco") version "0.16.0"
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose.compiler)
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
} }
composeCompiler { repositories {
enableStrongSkippingMode = true jcenter()
google()
maven(url = "https://jitpack.io")
} }
android { android {
val commonTest = "src/commonTest/java"
sourceSets["test"].java.srcDir(commonTest)
sourceSets["androidTest"].java.srcDirs("src/androidTest/java", commonTest)
bundle { bundle {
language { language {
enableSplit = false enableSplit = false
@ -36,26 +29,31 @@ android {
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
dataBinding = true dataBinding = true
compose = true
buildConfig = true
} }
lint { lintOptions {
disable("InvalidPeriodicWorkRequestInterval")
lintConfig = file("lint.xml") lintConfig = file("lint.xml")
textOutput = File("stdout") textOutput("stdout")
textReport = true textReport = true
} }
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdkVersion(Versions.targetSdk)
defaultConfig { defaultConfig {
testApplicationId = "org.tasks.test" testApplicationId = "org.tasks.test"
applicationId = "org.tasks" applicationId = "org.tasks"
versionCode = libs.versions.versionCode.get().toInt() versionCode = 90703
versionName = libs.versions.versionName.get() versionName = "9.7.2"
targetSdk = libs.versions.android.targetSdk.get().toInt() targetSdkVersion(Versions.targetSdk)
minSdk = libs.versions.android.minSdk.get().toInt() minSdkVersion(Versions.minSdk)
testInstrumentationRunner = "org.tasks.TestRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
} }
signingConfigs { signingConfigs {
@ -73,32 +71,30 @@ android {
} }
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true coreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_1_8
} }
flavorDimensions += listOf("store") kotlinOptions {
jvmTarget = "1.8"
}
@Suppress("LocalVariableName") @Suppress("LocalVariableName")
buildTypes { buildTypes {
debug { getByName("debug") {
configure<CrashlyticsExtension> {
mappingFileUploadEnabled = false
}
val tasks_mapbox_key_debug: String? by project val tasks_mapbox_key_debug: String? by project
val tasks_google_key_debug: String? by project val tasks_google_key_debug: String? by project
val tasks_caldav_url: String? by project
applicationIdSuffix = ".debug"
resValue("string", "mapbox_key", tasks_mapbox_key_debug ?: "") resValue("string", "mapbox_key", tasks_mapbox_key_debug ?: "")
resValue("string", "google_key", tasks_google_key_debug ?: "") resValue("string", "google_key", tasks_google_key_debug ?: "")
resValue("string", "tasks_caldav_url", tasks_caldav_url ?: "https://caldav.tasks.org") isTestCoverageEnabled = project.hasProperty("coverage")
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 { getByName("release") {
val tasks_mapbox_key: String? by project val tasks_mapbox_key: String? by project
val tasks_google_key: String? by project val tasks_google_key: String? by project
resValue("string", "mapbox_key", tasks_mapbox_key ?: "") resValue("string", "mapbox_key", tasks_mapbox_key ?: "")
resValue("string", "google_key", tasks_google_key ?: "") resValue("string", "google_key", tasks_google_key ?: "")
isMinifyEnabled = true isMinifyEnabled = true
@ -107,186 +103,119 @@ android {
} }
} }
flavorDimensions("store")
productFlavors { productFlavors {
create("generic") { create("generic") {
dimension = "store" setDimension("store")
} }
create("googleplay") { create("googleplay") {
isDefault = true setDimension("store")
dimension = "store"
}
}
packaging {
resources {
excludes += setOf("META-INF/*.kotlin_module", "META-INF/INDEX.LIST")
} }
} }
testOptions { packagingOptions {
managedDevices { exclude("META-INF/*.kotlin_module")
localDevices {
create("pixel2api30") {
device = "Pixel 2"
apiLevel = 30
systemImageSource = "aosp-atd"
}
}
}
} }
}
namespace = "org.tasks" configure<CheckstyleExtension> {
configFile = project.file("google_checks.xml")
toolVersion = "8.16"
} }
configurations.all { configurations.all {
exclude(group = "org.apache.httpcomponents") exclude(group = "com.google.guava", module = "guava-jdk5")
exclude(group = "org.apache.httpcomponents", module = "httpclient")
exclude(group = "com.google.http-client", module = "google-http-client-apache")
exclude(group = "org.checkerframework") exclude(group = "org.checkerframework")
exclude(group = "com.google.code.findbugs") exclude(group = "com.google.code.findbugs")
exclude(group = "com.google.errorprone") exclude(group = "com.google.errorprone")
exclude(group = "com.google.j2objc") 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 genericImplementation by configurations
val googleplayImplementation by configurations val googleplayImplementation by configurations
dependencies { dependencies {
implementation(projects.data) coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.0.5")
implementation(projects.kmp) implementation("com.gitlab.bitfireAT:dav4jvm:2.1")
implementation(projects.icons) implementation("com.gitlab.bitfireAT:ical4android:a675c7194b")
implementation(libs.androidx.navigation) implementation("com.gitlab.bitfireAT:cert4android:1488e39a66")
implementation(libs.androidx.adaptive.navigation.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) kapt("com.google.dagger:dagger-compiler:${Versions.dagger}")
implementation(libs.bitfire.dav4jvm) { implementation("com.google.dagger:dagger:${Versions.dagger}")
exclude(group = "junit")
exclude(group = "org.ogce", module = "xpp3") implementation("androidx.room:room-rxjava2:${Versions.room}")
} kapt("androidx.room:room-compiler:${Versions.room}")
implementation(libs.bitfire.ical4android) { implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
exclude(group = "commons-logging") implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
exclude(group = "org.json", module = "json") implementation("androidx.paging:paging-runtime:2.1.2")
exclude(group = "org.codehaus.groovy", module = "groovy")
exclude(group = "org.codehaus.groovy", module = "groovy-dateutil") kapt("com.jakewharton:butterknife-compiler:${Versions.butterknife}")
} implementation("com.jakewharton:butterknife:${Versions.butterknife}")
implementation(libs.bitfire.cert4android)
implementation(libs.dmfs.opentasks.provider) { debugImplementation("com.facebook.flipper:flipper:${Versions.flipper}")
exclude("com.github.tasks.opentasks", "opentasks-contract") debugImplementation("com.facebook.flipper:flipper-network-plugin:${Versions.flipper}")
} debugImplementation("com.facebook.soloader:soloader:0.9.0")
implementation(libs.dmfs.rfc5545.datetime)
implementation(libs.dmfs.recur) debugImplementation("com.squareup.leakcanary:leakcanary-android:${Versions.leakcanary}")
implementation(libs.dmfs.jems)
implementation("org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}")
implementation(libs.dagger.hilt) implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3.2")
ksp(libs.dagger.hilt.compiler) implementation("com.squareup.okhttp3:okhttp:${Versions.okhttp}")
ksp(libs.androidx.hilt.compiler) implementation("com.google.code.gson:gson:2.8.6")
implementation(libs.androidx.hilt.navigation) implementation("com.google.android.material:material:1.1.0")
implementation(libs.androidx.hilt.work) implementation("androidx.annotation:annotation:1.1.0")
implementation("androidx.constraintlayout:constraintlayout:2.0.0-beta4")
implementation(libs.androidx.core.splashscreen) implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
implementation(libs.androidx.datastore) implementation("androidx.preference:preference:1.1.1")
implementation(libs.androidx.fragment.compose) implementation("com.jakewharton.timber:timber:4.7.1")
implementation(libs.androidx.lifecycle.runtime) implementation("com.google.android.apps.dashclock:dashclock-api:2.0.0")
implementation(libs.androidx.lifecycle.runtime.compose) implementation("com.twofortyfouram:android-plugin-api-for-locale:1.0.2") {
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 isTransitive = false
} }
implementation(libs.jchronic) { implementation("com.rubiconproject.oss:jchronic:0.2.6") {
isTransitive = false isTransitive = false
} }
implementation(libs.shortcut.badger) implementation("org.scala-saddle:google-rfc-2445:20110304") {
implementation(libs.google.api.tasks) isTransitive = false
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) implementation("com.wdullaer:materialdatetimepicker:4.2.3")
implementation("me.leolin:ShortcutBadger:1.1.22@aar")
androidTestImplementation(libs.dagger.hilt.testing) implementation("com.google.apis:google-api-services-tasks:v1-rev20200129-1.30.9")
kspAndroidTest(libs.dagger.hilt.compiler) implementation("com.google.apis:google-api-services-drive:v3-rev20200413-1.30.9")
kspAndroidTest(libs.androidx.hilt.compiler) implementation("com.google.auth:google-auth-library-oauth2-http:0.20.0")
androidTestImplementation(libs.mockito.android) implementation("androidx.work:work-runtime:${Versions.work}")
androidTestImplementation(libs.make.it.easy) implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.2.1")
androidTestImplementation(libs.androidx.test.runner) implementation("com.etesync:journalmanager:1.1.0")
androidTestImplementation(libs.androidx.test.rules) implementation("com.github.QuadFlask:colorpicker:0.0.15")
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.okhttp.mockwebserver) // https://github.com/mapbox/mapbox-gl-native-android/issues/316
genericImplementation("com.mapbox.mapboxsdk:mapbox-android-sdk:7.4.1")
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test) googleplayImplementation("com.mapbox.mapboxsdk:mapbox-android-sdk:9.2.1")
testImplementation(libs.make.it.easy) googleplayImplementation("com.crashlytics.sdk.android:crashlytics:${Versions.crashlytics}")
testImplementation(libs.androidx.test.core) googleplayImplementation("com.google.firebase:firebase-analytics:${Versions.analytics}")
testImplementation(libs.mockito.core) googleplayImplementation("com.google.firebase:firebase-config-ktx:${Versions.remote_config}")
testImplementation(libs.xpp3) googleplayImplementation("com.google.android.gms:play-services-location:17.0.0")
googleplayImplementation("com.google.android.gms:play-services-maps:17.0.0")
googleplayImplementation("com.google.android.libraries.places:places:2.2.0")
googleplayImplementation("com.android.billingclient:billing:1.2.2")
kaptAndroidTest("com.google.dagger:dagger-compiler:${Versions.dagger}")
kaptAndroidTest("com.jakewharton:butterknife-compiler:${Versions.butterknife}")
androidTestImplementation("org.mockito:mockito-android:${Versions.mockito}")
androidTestImplementation("com.natpryce:make-it-easy:${Versions.make_it_easy}")
androidTestImplementation("androidx.test:runner:${Versions.androidx_test}")
androidTestImplementation("androidx.test:rules:${Versions.androidx_test}")
androidTestImplementation("androidx.test.ext:junit:1.1.1")
androidTestImplementation("androidx.annotation:annotation:1.1.0")
testImplementation("junit:junit:4.13")
testImplementation("com.natpryce:make-it-easy:${Versions.make_it_easy}")
testImplementation("androidx.test:core:${Versions.androidx_test}")
testImplementation("org.mockito:mockito-core:${Versions.mockito}")
} }
apply(mapOf("plugin" to "com.google.gms.google-services"))

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

@ -0,0 +1,263 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<!-- https://raw.githubusercontent.com/checkstyle/checkstyle/checkstyle-8.16/src/main/resources/google_checks.xml -->
<!--
Checkstyle configuration that checks the Google coding conventions from Google Java Style
that can be found at https://google.github.io/styleguide/javaguide.html.
Checkstyle is very configurable. Be sure to read the documentation at
http://checkstyle.sf.net (or in your downloaded distribution).
To completely disable a check, just comment it out or delete it from the file.
Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
-->
<module name = "Checker">
<property name="charset" value="UTF-8"/>
<property name="severity" value="warning"/>
<property name="fileExtensions" value="java, properties, xml"/>
<!-- Checks for whitespace -->
<!-- See http://checkstyle.sf.net/config_whitespace.html -->
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
</module>
<module name="TreeWalker">
<module name="OuterTypeFilename"/>
<module name="IllegalTokenText">
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
<property name="format"
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
<property name="message"
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
</module>
<module name="AvoidEscapedUnicodeCharacters">
<property name="allowEscapesForControlCharacters" value="true"/>
<property name="allowByTailComment" value="true"/>
<property name="allowNonPrintableEscapes" value="true"/>
</module>
<module name="LineLength">
<property name="max" value="100"/>
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
</module>
<module name="AvoidStarImport"/>
<module name="OneTopLevelClass"/>
<module name="NoLineWrap"/>
<module name="EmptyBlock">
<property name="option" value="TEXT"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
</module>
<module name="NeedBraces"/>
<module name="LeftCurly"/>
<module name="RightCurly">
<property name="id" value="RightCurlySame"/>
<property name="tokens"
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
LITERAL_DO"/>
</module>
<module name="RightCurly">
<property name="id" value="RightCurlyAlone"/>
<property name="option" value="alone"/>
<property name="tokens"
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
INSTANCE_INIT"/>
</module>
<module name="WhitespaceAround">
<property name="allowEmptyConstructors" value="true"/>
<property name="allowEmptyMethods" value="true"/>
<property name="allowEmptyTypes" value="true"/>
<property name="allowEmptyLoops" value="true"/>
<message key="ws.notFollowed"
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
<message key="ws.notPreceded"
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
</module>
<module name="OneStatementPerLine"/>
<module name="MultipleVariableDeclarations"/>
<module name="ArrayTypeStyle"/>
<module name="MissingSwitchDefault"/>
<module name="FallThrough"/>
<module name="UpperEll"/>
<module name="ModifierOrder"/>
<module name="EmptyLineSeparator">
<property name="allowNoEmptyLineBetweenFields" value="true"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapDot"/>
<property name="tokens" value="DOT"/>
<property name="option" value="nl"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapComma"/>
<property name="tokens" value="COMMA"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
<property name="id" value="SeparatorWrapEllipsis"/>
<property name="tokens" value="ELLIPSIS"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
<property name="id" value="SeparatorWrapArrayDeclarator"/>
<property name="tokens" value="ARRAY_DECLARATOR"/>
<property name="option" value="EOL"/>
</module>
<module name="SeparatorWrap">
<property name="id" value="SeparatorWrapMethodRef"/>
<property name="tokens" value="METHOD_REF"/>
<property name="option" value="nl"/>
</module>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern"
value="Package name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="TypeName">
<message key="name.invalidPattern"
value="Type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MemberName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
<message key="name.invalidPattern"
value="Member name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LambdaParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="CatchParameterName">
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="LocalVariableName">
<property name="tokens" value="VARIABLE_DEF"/>
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
<message key="name.invalidPattern"
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="ClassTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Class type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="MethodTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Method type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="InterfaceTypeParameterName">
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
<message key="name.invalidPattern"
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="NoFinalizer"/>
<module name="GenericWhitespace">
<message key="ws.followed"
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
<message key="ws.preceded"
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
<message key="ws.illegalFollow"
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
<message key="ws.notPreceded"
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
</module>
<module name="Indentation">
<property name="basicOffset" value="2"/>
<property name="braceAdjustment" value="0"/>
<property name="caseIndent" value="2"/>
<property name="throwsIndent" value="4"/>
<property name="lineWrappingIndentation" value="4"/>
<property name="arrayInitIndent" value="2"/>
</module>
<module name="AbbreviationAsWordInName">
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
</module>
<module name="OverloadMethodsDeclarationOrder"/>
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
<property name="separateLineBetweenGroups" value="true"/>
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
</module>
<module name="MethodParamPad"/>
<module name="NoWhitespaceBefore">
<property name="tokens"
value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
<property name="allowLineBreaks" value="true"/>
</module>
<module name="ParenPad"/>
<module name="OperatorWrap">
<property name="option" value="NL"/>
<property name="tokens"
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationMostCases"/>
<property name="tokens"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
</module>
<module name="AnnotationLocation">
<property name="id" value="AnnotationLocationVariables"/>
<property name="tokens" value="VARIABLE_DEF"/>
<property name="allowSamelineMultipleAnnotations" value="true"/>
</module>
<module name="NonEmptyAtclauseDescription"/>
<module name="JavadocTagContinuationIndentation">
<property name="severity" value="ignore" />
</module>
<module name="SummaryJavadoc">
<property name="severity" value="ignore" />
<property name="forbiddenSummaryFragments"
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
</module>
<module name="JavadocParagraph">
<property name="severity" value="ignore" />
</module>
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
<property name="target"
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
</module>
<module name="JavadocMethod">
<property name="severity" value="ignore" />
<property name="scope" value="public"/>
<property name="allowMissingParamTags" value="true"/>
<property name="allowMissingThrowsTags" value="true"/>
<property name="allowMissingReturnTag" value="true"/>
<property name="minLineCount" value="2"/>
<property name="allowedAnnotations" value="Override, Test"/>
<property name="allowThrowsTagsForSubclasses" value="true"/>
</module>
<module name="MethodName">
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
<message key="name.invalidPattern"
value="Method name ''{0}'' must match pattern ''{1}''."/>
</module>
<module name="SingleLineJavadoc">
<property name="severity" value="ignore"/>
</module>
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="expected"/>
</module>
<module name="CommentsIndentation"/>
</module>
</module>

@ -0,0 +1,756 @@
- artifact: com.gitlab.bitfireAT:dav4jvm:+
name: dav4jvm
copyrightHolder: bitfire web engineering (Ricki Hirner, Bernhard Stockmann)
license: Mozilla Public License, Version 2.0
licenseUrl: https://www.mozilla.org/en-US/MPL/2.0/
- artifact: com.gitlab.bitfireAT:ical4android:+
name: ical4android
copyrightHolder: bitfire web engineering (Ricki Hirner, Bernhard Stockmann)
license: GNU General Public License, Version 3.0
licenseUrl: https://www.gnu.org/licenses/gpl.txt
- artifact: com.gitlab.bitfireAT:cert4android:+
name: cert4android
copyrightHolder: bitfire web engineering (Ricki Hirner, Bernhard Stockmann)
licenseUrl: https://www.gnu.org/licenses/gpl.txt
license: GNU General Public License, Version 3.0
- artifact: androidx.coordinatorlayout:coordinatorlayout:+
name: Android Support Library Coordinator Layout
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.constraintlayout:constraintlayout:+
name: Android ConstraintLayout
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://tools.android.com
- artifact: androidx.sqlite:sqlite:+
name: Android DB
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.google.apis:google-api-services-drive:+
name: Drive API v3-rev136-1.25.0
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.fragment:fragment:+
name: Android Support Library fragment
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.vectordrawable:vectordrawable-animated:+
name: Android Support AnimatedVectorDrawable
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.mapbox.mapboxsdk:mapbox-sdk-services:+
name: Mapbox services
copyrightHolder: Mapbox
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: https://github.com/mapbox/mapbox-java
forceGenerate: true
- artifact: androidx.core:core:+
name: Android Support Library compat
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.arch.core:core-common:+
name: Android Arch-Common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.room:room-common:+
name: Android Room-Common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.room:room-runtime:+
name: Android Room-Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.google.code.gson:gson:+
name: Gson
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: me.leolin:ShortcutBadger:+
name: ShortcutBadger
copyrightHolder: Leo Lin
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/leolin310148/ShortcutBadger
- artifact: androidx.lifecycle:lifecycle-runtime:+
name: Android Lifecycle Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.versionedparcelable:versionedparcelable:+
name: VersionedParcelable and friends
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.mapbox.mapboxsdk:mapbox-sdk-turf:+
name: Mapbox services-turf
copyrightHolder: Mapbox
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: https://github.com/mapbox/mapbox-java
forceGenerate: true
- artifact: androidx.viewpager:viewpager:+
name: Android Support Library View Pager
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.mapbox.mapboxsdk:mapbox-android-core:+
name: Mapbox Android Core Library
copyrightHolder: Mapbox
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: https://github.com/mapbox/mapbox-events-android
forceGenerate: true
- artifact: androidx.lifecycle:lifecycle-livedata:+
name: Android Lifecycle LiveData
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: commons-codec:commons-codec:+
name: Apache Commons Codec
copyrightHolder: The Apache Software Foundation
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://commons.apache.org/proper/commons-codec/
- artifact: com.mapbox.mapboxsdk:mapbox-android-sdk:+
name: Mapbox Maps SDK for Android
copyrightHolder: Mapbox
license: BSD 2-Clause
licenseUrl: https://opensource.org/licenses/BSD-2-Clause
url: https://github.com/mapbox/mapbox-gl-native
forceGenerate: true
- artifact: androidx.annotation:annotation:+
name: Android Support Library Annotations
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.interpolator:interpolator:+
name: Android Support Library Interpolators
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: javax.inject:javax.inject:+
name: javax.inject
copyrightHolder: The JSR-330 Expert Group
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://code.google.com/p/atinject/
- artifact: com.twofortyfouram:android-plugin-api-for-locale:+
name: android-plugin-api-for-locale
copyrightHolder: two forty four a.m. LLC.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.lifecycle:lifecycle-viewmodel:+
name: Android Lifecycle ViewModel
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: org.scala-saddle:google-rfc-2445:+
name: google-rfc-2445
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://code.google.com/p/google-rfc-2445/
- artifact: com.google.dagger:dagger:+
name: Dagger
copyrightHolder: The Dagger Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/google/dagger
- artifact: com.google.guava:guava:+
name: Guava Google Core Libraries for Java
copyrightHolder: The Guava Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.jetbrains:annotations:+
name: JetBrains Java Annotations
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/JetBrains/java-annotations
- artifact: com.wdullaer:materialdatetimepicker:+
name: MaterialDateTimePicker
copyrightHolder: Wouter Dullaert
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/wdullaer/MaterialDateTimePicker
- artifact: org.apache.commons:commons-lang3:+
name: Apache Commons Lang
copyrightHolder: The Apache Software Foundation
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://commons.apache.org/proper/commons-lang/
- artifact: com.mapbox.mapboxsdk:mapbox-sdk-geojson:+
name: Mapbox services-geojson
copyrightHolder: Mapbox
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: https://github.com/mapbox/mapbox-java
forceGenerate: true
- artifact: com.mapbox.mapboxsdk:mapbox-android-telemetry:+
name: Mapbox Android Telemetry Library
copyrightHolder: Mapbox
license: BSD 2-Clause
licenseUrl: https://opensource.org/licenses/BSD-2-Clause
url: https://github.com/mapbox/mapbox-events-android
forceGenerate: true
- artifact: androidx.loader:loader:+
name: Android Support Library loader
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.cursoradapter:cursoradapter:+
name: Android Support Library Cursor Adapter
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.lifecycle:lifecycle-livedata-core:+
name: Android Lifecycle LiveData Core
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.customview:customview:+
name: Android Support Library Custom View
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: io.reactivex.rxjava2:rxandroid:+
name: RxAndroid
copyrightHolder: The RxAndroid authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/ReactiveX/RxAndroid
- artifact: androidx.swiperefreshlayout:swiperefreshlayout:+
name: Android Support Library Custom View
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.documentfile:documentfile:+
name: Android Support Library Document File
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.lifecycle:lifecycle-extensions:+
name: Android Lifecycle Extensions
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.legacy:legacy-support-core-utils:+
name: Android Support Library core utils
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.arch.core:core-runtime:+
name: Android Arch-Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: org.apache.commons:commons-collections4:+
name: Apache Commons Collections
copyrightHolder: The Apache Software Foundation
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://commons.apache.org/proper/commons-collections/
- artifact: org.mnode.ical4j:ical4j:+
name: ical4j
copyrightHolder: Ben Fortuna
license: BSD 3-Clause
licenseUrl: https://opensource.org/licenses/BSD-3-Clause
url: http://ical4j.github.io
forceGenerate: true
- artifact: androidx.recyclerview:recyclerview:+
name: Android Support RecyclerView v7
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.collection:collection:+
name: Android Support Library collections
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.fasterxml.jackson.core:jackson-core:+
name: Jackson-core
copyrightHolder: FasterXML
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/FasterXML/jackson-core
- artifact: androidx.cardview:cardview:+
name: Android Support CardView v7
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.print:print:+
name: Android Support Library Print
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.rubiconproject.oss:jchronic:+
name: jchronic
copyrightHolder: The jchronic authors
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: http://github.com/samtingleff/jchronic
- artifact: androidx.sqlite:sqlite-framework:+
name: Android Support SQLite - Framework Implementation
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.google.android.material:material:+
name: Material Components for Android
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.localbroadcastmanager:localbroadcastmanager:+
name: Android Support Library Local Broadcast Manager
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.google.android.apps.dashclock:dashclock-api:+
name: DashClock API
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://dashclock.com/api
- artifact: androidx.vectordrawable:vectordrawable:+
name: Android Support VectorDrawable
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: org.reactivestreams:reactive-streams:+
name: reactive-streams
copyrightHolder: Public domain
license: CC0
licenseUrl: http://creativecommons.org/publicdomain/zero/1.0/
url: http://www.reactive-streams.org/
- artifact: androidx.work:work-runtime:+
name: Android WorkManager Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.appcompat:appcompat:+
name: Android AppCompat Library v7
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.lifecycle:lifecycle-common:+
name: Android Lifecycle-Common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.mapbox.mapboxsdk:mapbox-sdk-core:+
name: Mapbox services-core
copyrightHolder: Mapbox
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: https://github.com/mapbox/mapbox-java
forceGenerate: true
- artifact: com.mapbox.mapboxsdk:mapbox-android-gestures:+
name: Mapbox Android Gestures Library
copyrightHolder: Mapbox
license: BSD 2-Clause
licenseUrl: https://opensource.org/licenses/BSD-2-Clause
url: https://github.com/mapbox/mapbox-gestures-android
forceGenerate: true
- artifact: androidx.lifecycle:lifecycle-process:+
name: Android Lifecycle Process
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.lifecycle:lifecycle-service:+
name: Android Lifecycle Service
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.transition:transition:+
name: Android Transition Support Library
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.jakewharton.timber:timber:+
name: Timber
copyrightHolder: Jake Wharton
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/JakeWharton/timber
- artifact: com.google.oauth-client:google-oauth-client:+
name: Google OAuth Client Library for Java
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.room:room-rxjava2:+
name: Android Room RXJava2
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.drawerlayout:drawerlayout:+
name: Android Support Library Drawer Layout
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: io.reactivex.rxjava2:rxjava:+
name: RxJava
copyrightHolder: RxJava Contributors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/ReactiveX/RxJava
- artifact: com.google.apis:google-api-services-tasks:+
name: Tasks API v1-rev55-1.25.0
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: com.google.api-client:google-api-client:+
name: Google APIs Client Library for Java
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.constraintlayout:constraintlayout-solver:+
name: Android ConstraintLayout Solver
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://tools.android.com
- artifact: org.jetbrains.kotlin:kotlin-stdlib:+
name: org.jetbrains.kotlin:kotlin-stdlib
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://kotlinlang.org/
- artifact: com.google.http-client:google-http-client:+
name: Google HTTP Client Library for Java
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.slf4j:slf4j-jdk14:+
name: SLF4J JDK14 Binding
copyrightHolder: QOS.ch
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: http://www.slf4j.org
- artifact: com.squareup.okhttp3:okhttp:+
name: OkHttp
copyrightHolder: Square, Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: com.squareup.retrofit2:retrofit:+
name: Retrofit
copyrightHolder: Square, Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.slf4j:slf4j-api:+
name: SLF4J API Module
copyrightHolder: QOS.ch
license: MIT License
licenseUrl: http://www.opensource.org/licenses/mit-license.php
url: http://www.slf4j.org
- artifact: org.jetbrains.kotlin:kotlin-stdlib-common:+
name: org.jetbrains.kotlin:kotlin-stdlib-common
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://kotlinlang.org/
- artifact: com.google.http-client:google-http-client-jackson2:+
name: Jackson 2 extensions to the Google HTTP Client Library for Java.
copyrightHolder: Google Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: com.jakewharton:butterknife:+
name: ButterKnife
copyrightHolder: Jake Wharton
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/JakeWharton/butterknife/
- artifact: com.jakewharton:butterknife-annotations:+
name: ButterKnife Annotations
copyrightHolder: Jake Wharton
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/JakeWharton/butterknife/
- artifact: com.squareup.retrofit2:converter-gson:+
name: "Converter: Gson"
copyrightHolder: Square, Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: com.squareup.okhttp3:logging-interceptor:+
name: OkHttp Logging Interceptor
copyrightHolder: Square, Inc.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.jetbrains.kotlin:kotlin-stdlib-jdk7:+
name: org.jetbrains.kotlin:kotlin-stdlib-jdk7
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://kotlinlang.org/
- artifact: com.jakewharton:butterknife-runtime:+
name: ButterKnife Runtime
copyrightHolder: Jake Wharton
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/JakeWharton/butterknife/
- artifact: io.grpc:grpc-context:+
name: io.grpc:grpc-context
copyrightHolder: The gRPC Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/grpc/grpc-java
- artifact: com.google.guava:listenablefuture:+
name: Guava ListenableFuture only
copyrightHolder: The Guava Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: io.opencensus:opencensus-api:+
name: OpenCensus API
copyrightHolder: OpenCensus Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/census-instrumentation/opencensus-java
- artifact: com.google.guava:failureaccess:+
name: Guava InternalFutureFailureAccess and InternalFutures
copyrightHolder: The Guava Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: io.opencensus:opencensus-contrib-http-util:+
name: OpenCensus contrib-http-util
copyrightHolder: OpenCensus Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/census-instrumentation/opencensus-java
- artifact: androidx.core:core-ktx:+
name: Core Kotlin Extensions
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.appcompat:appcompat-resources:+
name: Android Resources Library
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.viewpager2:viewpager2:+
name: AndroidX Widget ViewPager2
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.savedstate:savedstate:+
name: Activity
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.activity:activity:+
name: Activity
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.paging:paging-runtime:+
name: Android Paging-Runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: androidx.paging:paging-common:+
name: Android Paging-Common
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-core-common:+
name: kotlinx-coroutines-core-common
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/Kotlin/kotlinx.coroutines
- artifact: org.conscrypt:conscrypt-android:+
name: org.conscrypt:conscrypt-android
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: https://www.apache.org/licenses/LICENSE-2.0
url: https://conscrypt.org/
- artifact: org.jetbrains.kotlin:kotlin-stdlib-jdk8:+
name: org.jetbrains.kotlin:kotlin-stdlib-jdk8
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://kotlinlang.org/
- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-android:+
name: kotlinx-coroutines-android
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/Kotlin/kotlinx.coroutines
- artifact: androidx.databinding:databinding-adapters:+
name: databinding-adapters
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: androidx.lifecycle:lifecycle-viewmodel-ktx:+
name: Android Lifecycle ViewModel Kotlin Extensions
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.annotation:annotation-experimental:+
name: Experimental annotation
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: org.jetbrains.kotlinx:kotlinx-coroutines-core:+
name: kotlinx-coroutines-core
copyrightHolder: JetBrains s.r.o.
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/Kotlin/kotlinx.coroutines
- artifact: androidx.databinding:databinding-common:+
name: Data Binding Base Library
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/studio
- artifact: androidx.databinding:databinding-runtime:+
name: databinding-runtime
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: org.apache.httpcomponents:httpcore:+
name: Apache HttpCore
copyrightHolder: The Apache Software Foundation
license: The Apache Software License, Version 2.0
url: http://hc.apache.org/httpcomponents-core-ga
- artifact: androidx.databinding:viewbinding:+
name: viewbinding
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
- artifact: com.madgag.spongycastle:core:+
name: Spongy Castle Core
copyrightHolder: The Legion of the Bouncy Castle Inc.
license: Bouncy Castle Licence
licenseUrl: http://www.bouncycastle.org/licence.html
url: http://rtyley.github.io/spongycastle/
- artifact: com.etesync:journalmanager:+
name: EteSync JVM
copyrightHolder: Tom Hacohen
license: LGPL-3.0-only
licenseUrl: https://spdx.org/licenses/LGPL-3.0-only.html
url: https://www.etesync.com
- artifact: com.madgag.spongycastle:prov:+
name: Spongy Castle
copyrightHolder: The Legion of the Bouncy Castle Inc.
license: Bouncy Castle Licence
licenseUrl: http://www.bouncycastle.org/licence.html
url: http://rtyley.github.io/spongycastle/
- artifact: androidx.preference:preference:+
name: AndroidX Preference
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx
- artifact: androidx.lifecycle:lifecycle-viewmodel-savedstate:+
name: Android Lifecycle ViewModel with SavedState
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/topic/libraries/architecture/index.html
- artifact: com.github.QuadFlask:colorpicker:+
name: QuadFlask/colorpicker
copyrightHolder: QuadFlask
license: The Apache Software License, Version 2.0
url: https://github.com/QuadFlask/colorpicker
- artifact: com.google.auth:google-auth-library-credentials:+
name: Google Auth Library for Java - Credentials
copyrightHolder: Google Inc.
license: BSD 3-Clause
- artifact: com.google.auth:google-auth-library-oauth2-http:+
name: Google Auth Library for Java - OAuth2 HTTP
copyrightHolder: Google Inc.
license: BSD 3-Clause
- artifact: com.google.auto.value:auto-value-annotations:+
name: AutoValue Annotations
copyrightHolder: Google LLC
license: The Apache Software License, Version 2.0
url: https://github.com/google/auto/tree/master/value
- artifact: com.mapbox.mapboxsdk:mapbox-sdk-directions-refresh-models:+
name: mapbox-sdk-directions-refresh-models
copyrightHolder: Mapbox
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/mapbox/mapbox-java
- artifact: com.mapbox.mapboxsdk:mapbox-sdk-directions-models:+
name: mapbox-sdk-directions-models
copyrightHolder: Mapbox
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/mapbox/mapbox-java
- artifact: com.mapbox.mapboxsdk:mapbox-android-accounts:+
name: mapbox-android-accounts
copyrightHolder: Mapbox
license: Mapbox Terms of Service
licenseUrl: https://www.mapbox.com/tos/
url: https://github.com/mapbox/mapbox-accounts-android
- artifact: com.sun.mail:android-mail:+
name: android-mail
copyrightHolder: Oracle and/or its affiliates
license: Eclipse Public License, Version 2.0
- artifact: commons-io:commons-io:+
name: commons-io
copyrightHolder: The Apache Software Foundation
license: The Apache Software License, Version 2.0
url: http://commons.apache.org/proper/commons-io/
- artifact: com.sun.mail:android-activation:+
name: android-activation
copyrightHolder: Oracle and/or its affiliates
license: Eclipse Public License, Version 2.0

44
app/proguard.pro vendored

@ -2,6 +2,22 @@
-keep class org.tasks.** { *; } -keep class org.tasks.** { *; }
# remove logging statements
-assumenosideeffects class timber.log.Timber* {
public static *** v(...);
public static *** d(...);
public static *** i(...);
}
# google-rfc-2445-20110304
-dontwarn com.google.ical.compat.jodatime.**
# https://github.com/JakeWharton/butterknife/blob/581666a28022796fdd62caaf3420e621215abfda/butterknife/proguard-rules.txt
-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
-keep class butterknife.*
-keepclasseswithmembernames class * { @butterknife.* <methods>; }
-keepclasseswithmembernames class * { @butterknife.* <fields>; }
# guava # guava
-dontwarn sun.misc.Unsafe -dontwarn sun.misc.Unsafe
-dontwarn java.lang.ClassValue -dontwarn java.lang.ClassValue
@ -9,7 +25,9 @@
-dontwarn javax.inject.** -dontwarn javax.inject.**
-dontwarn com.google.j2objc.annotations.** -dontwarn com.google.j2objc.annotations.**
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn com.google.errorprone.annotations.** -dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
-dontwarn com.google.errorprone.annotations.concurrent.LazyInit
-dontwarn com.google.errorprone.annotations.ForOverride
# https://github.com/square/okhttp/blob/0b74bba08805c28f6aede626cf06f213ef6480f2/README.md # https://github.com/square/okhttp/blob/0b74bba08805c28f6aede626cf06f213ef6480f2/README.md
-dontwarn okhttp3.** -dontwarn okhttp3.**
@ -26,9 +44,8 @@
-dontwarn net.fortuna.ical4j.model.** -dontwarn net.fortuna.ical4j.model.**
-dontwarn org.codehaus.groovy.** -dontwarn org.codehaus.groovy.**
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency -dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
-dontwarn com.github.erosb.jsonsKema.** # ical4android
-dontwarn org.jparsec.** # ical4android
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime) -keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
-keep class org.threeten.bp.** { *; } # keep ThreeTen (for time zone processing)
-keep class at.bitfire.** { *; } # all DAVdroid code is required -keep class at.bitfire.** { *; } # all DAVdroid code is required
# https://github.com/google/google-api-java-client-samples/blob/34c3b43cb15f4ee1b636a0e01521cc81a2451dcd/tasks-android-sample/proguard-google-api-client.txt # https://github.com/google/google-api-java-client-samples/blob/34c3b43cb15f4ee1b636a0e01521cc81a2451dcd/tasks-android-sample/proguard-google-api-client.txt
@ -42,23 +59,4 @@
-dontnote java.nio.file.Files, java.nio.file.Path -dontnote java.nio.file.Files, java.nio.file.Path
-dontnote **.ILicensingService -dontnote **.ILicensingService
-dontnote sun.misc.Unsafe -dontnote sun.misc.Unsafe
-dontwarn sun.misc.Unsafe -dontwarn sun.misc.Unsafe
# errors from upgrading to AGP 8
-dontwarn java.beans.Transient
-dontwarn org.joda.convert.FromString
-dontwarn org.joda.convert.ToString
-dontwarn org.json.JSONString
# material icons
-keep class androidx.compose.material.icons.outlined.** { *; }
# microsoft authentication
-dontwarn com.microsoft.device.display.DisplayMask
-dontwarn com.google.android.libraries.identity.**
-dontwarn edu.umd.cs.findbugs.annotations.**
-dontwarn com.google.crypto.tink.subtle.**
-dontwarn net.jcip.annotations.**
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { <fields>; }

@ -9,7 +9,7 @@ import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import androidx.test.InstrumentationRegistry import androidx.test.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.tasks.R.string import org.tasks.R.string
@ -27,7 +27,7 @@ class TranslationTests {
private fun forEachLocale(callback: (Resources) -> Unit) { private fun forEachLocale(callback: (Resources) -> Unit) {
val locales = Locale.getAvailableLocales() val locales = Locale.getAvailableLocales()
for (locale in locales) { for (locale in locales) {
callback(getResourcesForLocale(locale)) callback.invoke(getResourcesForLocale(locale))
} }
} }
@ -38,6 +38,77 @@ class TranslationTests {
return Resources(resources.assets, resources.displayMetrics, configuration) return Resources(resources.assets, resources.displayMetrics, configuration)
} }
/** Internal test of format string parser */
@Test
fun testFormatStringParser() {
var s = "abc"
var data = FormatStringData(s)
assertEquals(s, data.string)
assertEquals(0, data.characters.size)
s = "abc %s def"
data = FormatStringData(s)
assertEquals(1, data.characters.size)
assertEquals('s', data.characters[0])
s = "abc %%s def %d"
data = FormatStringData(s)
assertEquals(2, data.characters.size)
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(FormatStringData("espanol %% und %d si")))
assertFalse(data.matches(FormatStringData("ingles %d ja %% pon")))
s = "% abc %"
data = FormatStringData(s)
assertEquals(2, data.characters.size)
assertEquals(' ', data.characters[0])
assertEquals('\u0000', data.characters[1])
}
/**
* Test that the format specifiers in translations match exactly the translations in the default
* text
*/
@Test
fun testFormatStringsMatch() {
val resources = InstrumentationRegistry.getTargetContext().resources
val strings = getResourceIds(string::class.java)
val formatStrings = arrayOfNulls<FormatStringData>(strings.size)
val failures = StringBuilder()
for (i in strings.indices) {
try {
val string = resources.getString(strings[i])
formatStrings[i] = FormatStringData(string)
} catch (e: Exception) {
val name = resources.getResourceName(strings[i])
failures.append(String.format("error opening %s: %s\n", name, e.message))
}
}
forEachLocale { r: Resources ->
val locale = r.configuration.locale
for (i in strings.indices) {
try {
if (strings[i] == string.abc_shareactionprovider_share_with_application) {
continue
}
val string = r.getString(strings[i])
val newFS = FormatStringData(string)
if (!newFS.matches(formatStrings[i])) {
val name = r.getResourceName(strings[i])
failures.append(String.format(
"%s (%s): %s != %s\n", name, locale.toString(), newFS, formatStrings[i]))
}
} catch (e: Exception) {
val name = r.getResourceName(strings[i])
failures.append(String.format(
"%s: error opening %s: %s\n", locale.toString(), name, e.message))
}
}
}
assertEquals(failures.toString(), 0, errorCount(failures))
}
/** check if string contains contains substrings */ /** check if string contains contains substrings */
private fun contains(r: Resources, resource: Int, failures: StringBuilder, expected: String) { private fun contains(r: Resources, resource: Int, failures: StringBuilder, expected: String) {
val translation = r.getString(resource) val translation = r.getString(resource)
@ -55,11 +126,105 @@ class TranslationTests {
forEachLocale { r: Resources -> forEachLocale { r: Resources ->
contains(r, string.CFC_tag_text, failures, "?") contains(r, string.CFC_tag_text, failures, "?")
contains(r, string.CFC_title_contains_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_dueBefore_text, failures, "?")
contains(r, string.CFC_tag_contains_text, failures, "?") contains(r, string.CFC_tag_contains_text, failures, "?")
contains(r, string.CFC_gtasks_list_text, failures, "?") contains(r, string.CFC_gtasks_list_text, failures, "?")
} }
assertEquals(failures.toString(), 0, failures.toString().replace("[^\n]".toRegex(), "").length) assertEquals(failures.toString(), 0, failures.toString().replace("[^\n]".toRegex(), "").length)
} }
/** Count newlines */
private fun errorCount(failures: StringBuilder): Int {
var count = 0
var 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 fun getResourceIds(resources: Class<*>): IntArray {
val fields = resources.declaredFields
val ids: MutableList<Int> = ArrayList(fields.size)
for (field in fields) {
try {
ids.add(field.getInt(null))
} catch (e: Exception) {
// not a field we care about
}
}
val idsAsIntArray = IntArray(ids.size)
for (i in ids.indices) {
idsAsIntArray[i] = ids[i]
}
return idsAsIntArray
}
private class FormatStringData internal constructor(
/** the original string */
val string: String) {
/** format characters */
val characters: CharArray
/** test that the characters match */
fun matches(other: FormatStringData?): Boolean {
if (characters.size != other!!.characters.size) {
return false
}
outer@ for (i in characters.indices) {
if (Character.isDigit(characters[i])) {
for (j in other.characters.indices) {
if (characters[i] == other.characters[j]) {
break@outer
}
}
return false
} else if (characters[i] != other.characters[i]) {
return false
}
}
return true
}
override fun toString(): String {
val value = StringBuilder("[")
for (i in characters.indices) {
value.append(characters[i])
if (i < characters.size - 1) {
value.append(',')
}
}
value.append("]: '").append(string).append('\'')
return value.toString()
}
companion object {
private val scratch = CharArray(10)
}
init {
var pos = -1
var count = 0
while (true) {
pos = string.indexOf('%', ++pos)
if (pos++ == -1) {
break
}
if (pos >= string.length) {
scratch[count++] = '\u0000'
} else {
scratch[count++] = string[pos]
}
}
characters = CharArray(count)
for (i in 0 until count) {
characters[i] = scratch[i]
}
}
}
} }

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

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

@ -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,27 +1,25 @@
package com.todoroo.astrid.adapter package com.todoroo.astrid.adapter
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.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.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.CaldavTaskMaker.TASK
@ -33,21 +31,17 @@ import org.tasks.preferences.Preferences
import org.tasks.time.DateTime import org.tasks.time.DateTime
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class CaldavManualSortTaskAdapterTest : InjectingTestCase() { class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: CaldavManualSortTaskAdapter private lateinit var adapter: CaldavManualSortTaskAdapter
private val tasks = ArrayList<TaskContainer>() private val tasks = ArrayList<TaskContainer>()
private val filter = CaldavFilter( private val filter = CaldavFilter(CaldavCalendar("calendar", "1234"))
calendar = CaldavCalendar(name = "calendar", uuid = "1234"),
account = CaldavAccount(accountType = TYPE_CALDAV)
)
private val dataSource = object : TaskAdapterDataSource { private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position] override fun getItem(position: Int) = tasks[position]
@ -60,7 +54,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
preferences.clear() preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true) preferences.setBoolean(R.string.p_manual_sort, true)
tasks.clear() tasks.clear()
adapter = CaldavManualSortTaskAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) adapter = CaldavManualSortTaskAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource) adapter.setDataSource(dataSource)
} }
@ -219,16 +213,16 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
checkOrder(created.plusSeconds(6), 3) checkOrder(created.plusSeconds(6), 3)
} }
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking { private fun move(from: Int, to: Int, indent: Int = 0) {
tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter))) tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter, it) })
val adjustedTo = if (from < to) to + 1 else to // match DragAndDropRecyclerAdapter behavior val adjustedTo = if (from < to) to + 1 else to // match DragAndDropRecyclerAdapter behavior
adapter.moved(from, adjustedTo, indent) adapter.moved(from, adjustedTo, indent)
} }
private fun checkOrder(dateTime: DateTime, index: Int) = checkOrder(dateTime.toAppleEpoch(), index) private fun checkOrder(dateTime: DateTime, index: Int) = checkOrder(dateTime.toAppleEpoch(), index)
private fun checkOrder(order: Long?, index: Int) = runBlocking { private fun checkOrder(order: Long?, index: Int) {
val sortOrder = taskDao.fetch(adapter.getTask(index).id)!!.order val sortOrder = caldavDao.getTask(adapter.getTask(index).id)!!.order
if (order == null) { if (order == null) {
assertNull(sortOrder) assertNull(sortOrder)
} else { } else {
@ -236,7 +230,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
} }
} }
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking { private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long {
val task = newTask(*properties) val task = newTask(*properties)
taskDao.createNew(task) taskDao.createNew(task)
val remoteParent = if (task.parent > 0) caldavDao.getRemoteIdForTask(task.parent) else null val remoteParent = if (task.parent > 0) caldavDao.getRemoteIdForTask(task.parent) else null
@ -245,6 +239,8 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
with(TASK, task.id), with(TASK, task.id),
with(CALENDAR, "1234"), with(CALENDAR, "1234"),
with(REMOTE_PARENT, remoteParent))) with(REMOTE_PARENT, remoteParent)))
task.id return task.id
} }
override fun inject(component: TestComponent) = component.inject(this)
} }

@ -1,31 +1,28 @@
package com.todoroo.astrid.adapter package com.todoroo.astrid.adapter
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.* import org.tasks.data.*
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.makers.TaskContainerMaker.PARENT import org.tasks.makers.TaskContainerMaker.PARENT
import org.tasks.makers.TaskContainerMaker.newTaskContainer import org.tasks.makers.TaskContainerMaker.newTaskContainer
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class CaldavTaskAdapterTest : InjectingTestCase() { class CaldavTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: TaskAdapter private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>() private val tasks = ArrayList<TaskContainer>()
@ -35,7 +32,7 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
super.setUp() super.setUp()
tasks.clear() tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(object : TaskAdapterDataSource { adapter.setDataSource(object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position] override fun getItem(position: Int) = tasks[position]
@ -138,7 +135,7 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
} }
@Test @Test
fun movingTaskToNewParentSetsId() = runBlocking { fun movingTaskToNewParentSetsId() {
addTask() addTask()
addTask() addTask()
@ -148,7 +145,7 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
} }
@Test @Test
fun movingTaskToNewParentSetsRemoteId() = runBlocking { fun movingTaskToNewParentSetsRemoteId() {
addTask() addTask()
addTask() addTask()
@ -161,7 +158,7 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
} }
@Test @Test
fun unindentingTaskRemovesParent() = runBlocking { fun unindentingTaskRemovesParent() {
addTask() addTask()
addTask(with(PARENT, tasks[0])) addTask(with(PARENT, tasks[0]))
@ -172,7 +169,7 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
} }
@Test @Test
fun moveSubtaskUpToParent() = runBlocking { fun moveSubtaskUpToParent() {
addTask() addTask()
addTask(with(PARENT, tasks[0])) addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[1])) addTask(with(PARENT, tasks[1]))
@ -183,7 +180,7 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
} }
@Test @Test
fun moveSubtaskUpToGrandparent() = runBlocking { fun moveSubtaskUpToGrandparent() {
addTask() addTask()
addTask(with(PARENT, tasks[0])) addTask(with(PARENT, tasks[0]))
addTask(with(PARENT, tasks[1])) addTask(with(PARENT, tasks[1]))
@ -194,20 +191,26 @@ class CaldavTaskAdapterTest : InjectingTestCase() {
assertEquals(tasks[0].id, taskDao.fetch(tasks[3].id)!!.parent) assertEquals(tasks[0].id, taskDao.fetch(tasks[3].id)!!.parent)
} }
private fun addTask(vararg properties: PropertyValue<in TaskContainer?, *>) = runBlocking { private fun addTask(vararg properties: PropertyValue<in TaskContainer?, *>) {
val t = newTaskContainer(*properties) val t = newTaskContainer(*properties)
tasks.add(t)
val task = t.task val task = t.task
taskDao.createNew(task) taskDao.createNew(task)
val caldavTask = CaldavTask(task = t.id, calendar = "calendar") val caldavTask = CaldavTask(t.id, "calendar")
if (task.parent > 0) { if (task.parent > 0) {
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent) caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.parent)
} }
tasks.add( caldavTask.id = caldavDao.insert(caldavTask)
t.copy( t.caldavTask = caldavTask.toSubset()
caldavTask = caldavTask.copy( }
id = caldavDao.insert(caldavTask)
) private fun CaldavTask.toSubset(): SubsetCaldav {
) val result = SubsetCaldav()
) result.cd_id = id
} result.cd_calendar = calendar
} result.cd_remote_parent = remoteParent
return result
}
override fun inject(component: TestComponent) = component.inject(this)
}

@ -1,49 +1,46 @@
package com.todoroo.astrid.adapter package com.todoroo.astrid.adapter
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.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.injection.InjectingTestCase
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.injection.TestComponent
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.GoogleTaskListMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.GoogleTaskListMaker.newGoogleTaskList
import org.tasks.makers.GoogleTaskMaker
import org.tasks.makers.GoogleTaskMaker.LIST
import org.tasks.makers.GoogleTaskMaker.TASK
import org.tasks.makers.GoogleTaskMaker.newGoogleTask
import org.tasks.makers.TaskMaker.PARENT import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class GoogleTaskManualSortAdapterTest : InjectingTestCase() { class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: GoogleTaskManualSortAdapter private lateinit var adapter: GoogleTaskManualSortAdapter
private val tasks = ArrayList<TaskContainer>() private val tasks = ArrayList<TaskContainer>()
private val filter = CaldavFilter( private val filter = GtasksFilter(newGoogleTaskList(with(REMOTE_ID, "1234")))
calendar = CaldavCalendar(uuid = "1234"),
account = CaldavAccount(accountType = TYPE_GOOGLE_TASKS)
)
private val dataSource = object : TaskAdapterDataSource { private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position] override fun getItem(position: Int) = tasks[position]
@ -418,33 +415,35 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
preferences.clear() preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true) preferences.setBoolean(R.string.p_manual_sort, true)
tasks.clear() tasks.clear()
adapter = GoogleTaskManualSortAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) adapter = GoogleTaskManualSortAdapter(googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource) adapter.setDataSource(dataSource)
} }
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking { private fun move(from: Int, to: Int, indent: Int = 0) {
tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter))) tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter, it) })
val adjustedTo = if (from < to) to + 1 else to val adjustedTo = if (from < to) to + 1 else to
adapter.moved(from, adjustedTo, indent) adapter.moved(from, adjustedTo, indent)
} }
private fun checkOrder(order: Long, index: Int, parent: Long = 0) = runBlocking { private fun checkOrder(order: Long, index: Int, parent: Long = 0) {
val googleTask = taskDao.fetch(adapter.getTask(index).id)!! val googleTask = googleTaskDao.getByTaskId(adapter.getTask(index).id)!!
assertEquals(order, googleTask.order) assertEquals(order, googleTask.order)
assertEquals(parent, googleTask.parent) assertEquals(parent, googleTask.parent)
} }
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking { private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long {
val task = newTask(*properties) val task = newTask(*properties)
val parent = task.parent
task.parent = 0
taskDao.createNew(task) taskDao.createNew(task)
googleTaskDao.insertAndShift( googleTaskDao.insertAndShift(
task, newGoogleTask(
newCaldavTask( with(TASK, task.id),
with(TASK, task.id), with(LIST, "1234"),
with(CALENDAR, "1234"), with(GoogleTaskMaker.PARENT, parent)),
), false)
false return task.id
)
task.id
} }
override fun inject(component: TestComponent) = component.inject(this)
} }

@ -0,0 +1,79 @@
package com.todoroo.astrid.adapter
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import javax.inject.Inject
@RunWith(AndroidJUnit4::class)
class NonRecursiveQueryTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = BuiltInFilterExposer.getMyTasksFilter(ApplicationProvider.getApplicationContext<Context>().resources)
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
override fun getTaskCount() = tasks.size
}
@Before
override fun setUp() {
super.setUp()
preferences.clear()
preferences.setBoolean(R.string.p_use_paged_queries, true)
tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource)
}
@Test
fun ignoreSubtasks() {
val parent = addTask()
val child = addTask(with(PARENT, parent))
query()
assertEquals(child, tasks[1].id)
assertEquals(parent, tasks[1].parent)
assertEquals(0, tasks[1].indent)
}
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long {
val task = newTask(*properties)
taskDao.createNew(task)
return task.id
}
private fun query() {
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter, it) })
}
override fun inject(component: TestComponent) = component.inject(this)
}

@ -1,34 +1,54 @@
package com.todoroo.astrid.adapter package com.todoroo.astrid.adapter
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest import com.todoroo.astrid.data.Task
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.LocalBroadcastManager
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.entity.Task
import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.makers.TaskMaker.PARENT import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class OfflineSubtaskTest : InjectingTestCase() { class OfflineSubtaskTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = BuiltInFilterExposer.getMyTasksFilter(ApplicationProvider.getApplicationContext<Context>().resources)
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
private val filter = runBlocking { MyTasksFilter.create() } override fun getTaskCount() = tasks.size
}
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
preferences.clear() preferences.clear()
tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager)
adapter.setDataSource(dataSource)
} }
@Test @Test
@ -36,7 +56,7 @@ class OfflineSubtaskTest : InjectingTestCase() {
val parent = addTask() val parent = addTask()
val child = addTask(with(PARENT, parent)) val child = addTask(with(PARENT, parent))
val tasks = query() query()
assertEquals(child, tasks[1].id) assertEquals(child, tasks[1].id)
assertEquals(parent, tasks[1].parent) assertEquals(parent, tasks[1].parent)
@ -49,81 +69,22 @@ class OfflineSubtaskTest : InjectingTestCase() {
val parent = addTask(with(PARENT, grandparent)) val parent = addTask(with(PARENT, grandparent))
val child = addTask(with(PARENT, parent)) val child = addTask(with(PARENT, parent))
val tasks = query() query()
assertEquals(child, tasks[2].id) assertEquals(child, tasks[2].id)
assertEquals(parent, tasks[2].parent) assertEquals(parent, tasks[2].parent)
assertEquals(2, tasks[2].indent) assertEquals(2, tasks[2].indent)
} }
@Test private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long {
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) val task = newTask(*properties)
taskDao.createNew(task) taskDao.createNew(task)
task.id return task.id
} }
private fun query(): List<TaskContainer> = runBlocking { private fun query() {
taskDao.fetchTasks(getQuery(preferences, filter)) tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter, it) })
} }
}
override fun inject(component: TestComponent) = component.inject(this)
}

@ -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 +1,52 @@
package com.todoroo.astrid.alarms package com.todoroo.astrid.alarms
import dagger.hilt.android.testing.HiltAndroidTest import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.runBlocking import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt import org.junit.runner.RunWith
import org.tasks.data.createDueDate import org.tasks.data.Alarm
import org.tasks.data.dao.TaskDao import org.tasks.data.AlarmDao
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Notification
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.jobs.AlarmEntry
import org.tasks.jobs.NotificationQueue
import org.tasks.makers.TaskMaker.REMINDER_LAST
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class AlarmJobServiceTest : InjectingTestCase() { class AlarmJobServiceTest : InjectingTestCase() {
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var jobs: NotificationQueue
@Inject lateinit var alarmService: AlarmService @Inject lateinit var alarmService: AlarmService
@Test @Test
fun testNoAlarms() = runBlocking { fun scheduleAlarm() {
testResults(emptyList(), 0) val task = newTask()
} taskDao.createNew(task)
val alarmTime = DateTime(2017, 9, 24, 19, 57)
@Test val alarm = Alarm(task.id, alarmTime.millis)
fun futureAlarmWithNoPastAlarm() = runBlocking { alarm.id = alarmDao.insert(alarm)
freezeAt(DateTime(2024, 5, 17, 23, 20)) { alarmService.scheduleAllAlarms()
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 18).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
testResults(emptyList(), DateTime(2024, 5, 18, 18, 0).millis) assertEquals(listOf(AlarmEntry(alarm)), jobs.getJobs())
}
} }
@Test @Test
fun pastAlarmWithNoFutureAlarm() = runBlocking { fun ignoreStaleAlarm() {
freezeAt(DateTime(2024, 5, 17, 23, 20)) { val alarmTime = DateTime(2017, 9, 24, 19, 57)
taskDao.insert( val task = newTask(with(REMINDER_LAST, alarmTime.endOfMinute()))
Task( taskDao.createNew(task)
dueDate = createDueDate( alarmDao.insert(Alarm(task.id, alarmTime.millis))
Task.URGENCY_SPECIFIC_DAY, alarmService.scheduleAllAlarms()
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
testResults( assertTrue(jobs.getJobs().isEmpty())
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
0
)
}
} }
@Test override fun inject(component: TestComponent) = component.inject(this)
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)
}
} }

@ -5,25 +5,29 @@
*/ */
package com.todoroo.astrid.dao package com.todoroo.astrid.dao
import org.tasks.data.entity.Task import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.injection.TestComponent
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class TaskDaoTests : InjectingTestCase() { class TaskDaoTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter @Inject lateinit var taskDeleter: TaskDeleter
/** Test basic task creation, fetch, and save */ /** Test basic task creation, fetch, and save */
@Test @Test
fun testTaskCreation() = runBlocking { fun testTaskCreation() {
assertEquals(0, taskDao.getAll().size) assertEquals(0, taskDao.getAll().size)
// create task "happy" // create task "happy"
@ -58,7 +62,7 @@ class TaskDaoTests : InjectingTestCase() {
/** Test various task fetch conditions */ /** Test various task fetch conditions */
@Test @Test
fun testTaskConditions() = runBlocking { fun testTaskConditions() {
// create normal task // create normal task
var task = Task() var task = Task()
task.title = "normal" task.title = "normal"
@ -72,23 +76,23 @@ class TaskDaoTests : InjectingTestCase() {
// create hidden task // create hidden task
task = Task() task = Task()
task.title = "hidden" task.title = "hidden"
task.hideUntil = currentTimeMillis() + 10000 task.hideUntil = DateUtilities.now() + 10000
taskDao.createNew(task) taskDao.createNew(task)
// create task with deadlines // create task with deadlines
task = Task() task = Task()
task.title = "deadlineInFuture" task.title = "deadlineInFuture"
task.dueDate = currentTimeMillis() + 10000 task.dueDate = DateUtilities.now() + 10000
taskDao.createNew(task) taskDao.createNew(task)
task = Task() task = Task()
task.title = "deadlineInPast" task.title = "deadlineInPast"
task.dueDate = currentTimeMillis() - 10000 task.dueDate = DateUtilities.now() - 10000
taskDao.createNew(task) taskDao.createNew(task)
// create completed task // create completed task
task = Task() task = Task()
task.title = "completed" task.title = "completed"
task.completionDate = currentTimeMillis() - 10000 task.completionDate = DateUtilities.now() - 10000
taskDao.createNew(task) taskDao.createNew(task)
// check is active // check is active
@ -100,7 +104,7 @@ class TaskDaoTests : InjectingTestCase() {
/** Test task deletion */ /** Test task deletion */
@Test @Test
fun testTDeletion() = runBlocking { fun testTDeletion() {
assertEquals(0, taskDao.getAll().size) assertEquals(0, taskDao.getAll().size)
// create task "happy" // create task "happy"
@ -116,7 +120,7 @@ class TaskDaoTests : InjectingTestCase() {
/** Test save without prior create doesn't work */ /** Test save without prior create doesn't work */
@Test @Test
fun testSaveWithoutCreate() = runBlocking { fun testSaveWithoutCreate() {
// try to save task "happy" // try to save task "happy"
val task = Task() val task = Task()
task.title = "happy" task.title = "happy"
@ -127,7 +131,7 @@ class TaskDaoTests : InjectingTestCase() {
/** Test passing invalid task indices to various things */ /** Test passing invalid task indices to various things */
@Test @Test
fun testInvalidIndex() = runBlocking { fun testInvalidIndex() {
assertEquals(0, taskDao.getAll().size) assertEquals(0, taskDao.getAll().size)
assertNull(taskDao.fetch(1)) assertNull(taskDao.fetch(1))
taskDeleter.delete(listOf(1L)) taskDeleter.delete(listOf(1L))
@ -135,4 +139,29 @@ class TaskDaoTests : InjectingTestCase() {
// make sure db still works // make sure db still works
assertEquals(0, taskDao.getAll().size) assertEquals(0, taskDao.getAll().size)
} }
@Test
fun findChildrenInList() {
taskDao.createNew(newTask(with(ID, 1L)))
taskDao.createNew(newTask(with(ID, 2L), with(PARENT, 1L)))
assertEquals(listOf(2L), taskDao.getChildren(listOf(1L, 2L)))
}
@Test
fun findRecursiveChildrenInList() {
taskDao.createNew(newTask(with(ID, 1L)))
taskDao.createNew(newTask(with(ID, 2L), with(PARENT, 1L)))
taskDao.createNew(newTask(with(ID, 3L), with(PARENT, 2L)))
assertEquals(listOf(2L, 3L, 3L), taskDao.getChildren(listOf(1L, 2L, 3L)))
}
@Test
fun findRecursiveChildrenInListAfterSkippingParent() {
taskDao.createNew(newTask(with(ID, 1L)))
taskDao.createNew(newTask(with(ID, 2L), with(PARENT, 1L)))
taskDao.createNew(newTask(with(ID, 3L), with(PARENT, 2L)))
assertEquals(listOf(2L, 3L), taskDao.getChildren(listOf(1L, 3L)))
}
override fun inject(component: TestComponent) = component.inject(this)
} }

@ -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 +1,92 @@
package com.todoroo.astrid.gtasks package com.todoroo.astrid.gtasks
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.api.services.tasks.model.TaskList import com.google.api.services.tasks.model.TaskList
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.service.TaskDeleter 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.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao import org.tasks.data.GoogleTaskAccount
import org.tasks.data.entity.CaldavAccount import org.tasks.data.GoogleTaskListDao
import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.makers.GtaskListMaker.ID
import org.tasks.makers.GtaskListMaker.NAME
import org.tasks.makers.GtaskListMaker.REMOTE_ID
import org.tasks.makers.GtaskListMaker.newGtaskList
import org.tasks.makers.RemoteGtaskListMaker import org.tasks.makers.RemoteGtaskListMaker
import org.tasks.makers.RemoteGtaskListMaker.newRemoteList import org.tasks.makers.RemoteGtaskListMaker.newRemoteList
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class GtasksListServiceTest : InjectingTestCase() { class GtasksListServiceTest : InjectingTestCase() {
@Inject lateinit var taskDeleter: TaskDeleter @Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var googleTaskListDao: GoogleTaskListDao
private lateinit var gtasksListService: GtasksListService private lateinit var gtasksListService: GtasksListService
@Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
gtasksListService = GtasksListService(caldavDao, taskDeleter, localBroadcastManager) gtasksListService = GtasksListService(googleTaskListDao, taskDeleter, localBroadcastManager)
} }
override fun inject(component: TestComponent) = component.inject(this)
@Test @Test
fun testCreateNewList() = runBlocking { fun testCreateNewList() {
setLists( setLists(
newRemoteList( newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "Default"))) with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "Default")))
assertEquals( assertEquals(
CaldavCalendar(id = 1, account = "account", uuid = "1", name = "Default"), newGtaskList(with(ID, 1L), with(REMOTE_ID, "1"), with(NAME, "Default")),
caldavDao.getCalendarById(1L) googleTaskListDao.getById(1L))
)
} }
@Test @Test
fun testGetListByRemoteId() = runBlocking { fun testGetListByRemoteId() {
val list = CaldavCalendar(uuid = "1") val list = newGtaskList(with(REMOTE_ID, "1"))
caldavDao.insert(list) list.id = googleTaskListDao.insertOrReplace(list)
assertEquals(list, caldavDao.getCalendarByUuid("1")) assertEquals(list, gtasksListService.getList("1"))
} }
@Test @Test
fun testGetListReturnsNullWhenNotFound() = runBlocking { fun testGetListReturnsNullWhenNotFound() {
assertNull(caldavDao.getCalendarByUuid("1")) assertNull(gtasksListService.getList("1"))
} }
@Test @Test
fun testDeleteMissingList() = runBlocking { fun testDeleteMissingList() {
caldavDao.insert(CaldavCalendar(account = "account", uuid = "1")) googleTaskListDao.insertOrReplace(newGtaskList(with(ID, 1L), with(REMOTE_ID, "1")))
val taskList = newRemoteList(with(RemoteGtaskListMaker.REMOTE_ID, "2")) val taskList = newRemoteList(with(RemoteGtaskListMaker.REMOTE_ID, "2"))
setLists(taskList) setLists(taskList)
assertEquals( assertEquals(
listOf(CaldavCalendar(id = 2, account = "account", uuid = "2", name = "Default")), listOf(newGtaskList(with(ID, 2L), with(REMOTE_ID, "2"))),
caldavDao.getCalendarsByAccount("account") googleTaskListDao.getLists("account"))
)
} }
@Test @Test
fun testUpdateListName() = runBlocking { fun testUpdateListName() {
val calendar = CaldavCalendar(uuid = "1", name = "oldName", account = "account") googleTaskListDao.insertOrReplace(
caldavDao.insert(calendar) newGtaskList(with(ID, 1L), with(REMOTE_ID, "1"), with(NAME, "oldName")))
setLists( setLists(
newRemoteList( newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "newName"))) with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "newName")))
assertEquals("newName", caldavDao.getCalendarById(calendar.id)!!.name) assertEquals("newName", googleTaskListDao.getById(1)!!.title)
} }
@Test @Test
fun testNewListLastSyncIsZero() = runBlocking { fun testNewListLastSyncIsZero() {
setLists(TaskList().setId("1")) setLists(TaskList().setId("1"))
assertEquals(0L, caldavDao.getCalendarByUuid("1")!!.lastSync) assertEquals(0L, gtasksListService.getList("1").lastSync)
} }
private suspend fun setLists(vararg list: TaskList) { private fun setLists(vararg list: TaskList) {
val account = CaldavAccount( val account = GoogleTaskAccount("account")
username = "account", googleTaskListDao.insert(account)
uuid = "account",
)
caldavDao.insert(account)
gtasksListService.updateLists(account, listOf(*list)) gtasksListService.updateLists(account, listOf(*list))
} }
} }

@ -0,0 +1,81 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.gtasks
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import javax.inject.Inject
@RunWith(AndroidJUnit4::class)
class GtasksMetadataServiceTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
private var task: Task? = null
private var metadata: GoogleTask? = null
override fun inject(component: TestComponent) = component.inject(this)
@Test
fun testMetadataFound() {
givenTask(taskWithMetadata(null))
whenSearchForMetadata()
thenExpectMetadataFound()
}
@Test
fun testMetadataDoesntExist() {
givenTask(taskWithoutMetadata())
whenSearchForMetadata()
thenExpectNoMetadataFound()
}
private fun thenExpectNoMetadataFound() {
assertNull(metadata)
}
private fun thenExpectMetadataFound() {
assertNotNull(metadata)
}
// --- helpers
private fun whenSearchForMetadata() {
metadata = googleTaskDao.getByTaskId(task!!.id)
}
private fun taskWithMetadata(id: String?): Task {
val task = Task()
task.title = "cats"
taskDao.createNew(task)
val metadata = GoogleTask(task.id, "")
if (id != null) {
metadata.remoteId = id
}
metadata.task = task.id
googleTaskDao.insert(metadata)
return task
}
private fun givenTask(taskToTest: Task) {
task = taskToTest
}
private fun taskWithoutMetadata(): Task {
val task = Task()
task.title = "dogs"
taskDao.createNew(task)
return task
}
}

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

@ -0,0 +1,326 @@
package com.todoroo.astrid.reminders
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.Freeze
import org.tasks.R
import org.tasks.date.DateTimeUtils
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.jobs.NotificationQueue
import org.tasks.jobs.ReminderEntry
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_DATE
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.RANDOM_REMINDER_PERIOD
import org.tasks.makers.TaskMaker.REMINDERS
import org.tasks.makers.TaskMaker.REMINDER_LAST
import org.tasks.makers.TaskMaker.SNOOZE_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import org.tasks.reminders.Random
import org.tasks.time.DateTime
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@RunWith(AndroidJUnit4::class)
class ReminderServiceTest : InjectingTestCase() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var jobs: NotificationQueue
private lateinit var service: ReminderService
private lateinit var random: RandomStub
override fun setUp() {
super.setUp()
random = RandomStub()
preferences.clear()
service = ReminderService(preferences, jobs, random, taskDao)
}
override fun inject(component: TestComponent) = component.inject(this)
@Test
fun dontScheduleDueDateReminderWhenFlagNotSet() {
service.scheduleAlarm(newTask(with(ID, 1L), with(DUE_TIME, DateTimeUtils.newDateTime())))
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenTimeNotSet() {
service.scheduleAlarm(newTask(with(ID, 1L), with(REMINDERS, Task.NOTIFY_AT_DEADLINE)))
assertTrue(jobs.isEmpty())
}
@Test
fun schedulePastDueDate() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime().minusDays(1)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.dueDate, ReminderService.TYPE_DUE))
}
@Test
fun scheduleFutureDueDate() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime().plusDays(1)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.dueDate, ReminderService.TYPE_DUE))
}
@Test
fun scheduleReminderAtDefaultDueTime() {
val now = DateTimeUtils.newDateTime()
val task = newTask(with(ID, 1L), with(DUE_DATE, now), with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, now.startOfDay().withHourOfDay(18).millis, ReminderService.TYPE_DUE))
}
@Test
fun dontScheduleReminderForCompletedTask() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime().plusDays(1)),
with(COMPLETION_TIME, DateTimeUtils.newDateTime()),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleReminderForDeletedTask() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime().plusDays(1)),
with(DELETION_TIME, DateTimeUtils.newDateTime()),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun dontScheduleDueDateReminderWhenAlreadyReminded() {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(DUE_TIME, now),
with(REMINDER_LAST, now.plusSeconds(1)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
assertTrue(jobs.isEmpty())
}
@Test
fun ignoreStaleSnoozeTime() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTimeUtils.newDateTime()),
with(SNOOZE_TIME, DateTimeUtils.newDateTime().minusMinutes(5)),
with(REMINDER_LAST, DateTimeUtils.newDateTime().minusMinutes(4)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.dueDate, ReminderService.TYPE_DUE))
}
@Test
fun dontIgnoreMissedSnoozeTime() {
val dueDate = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(DUE_TIME, dueDate),
with(SNOOZE_TIME, dueDate.minusMinutes(4)),
with(REMINDER_LAST, dueDate.minusMinutes(5)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1, task.reminderSnooze, ReminderService.TYPE_SNOOZE))
}
@Test
fun scheduleInitialRandomReminder() {
random.seed = 0.3865f
Freeze.freezeClock {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(REMINDER_LAST, null as DateTime?),
with(CREATION_TIME, now.minusDays(1)),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_WEEK))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, now.minusDays(1).millis + 584206592, ReminderService.TYPE_RANDOM))
}
}
@Test
fun scheduleNextRandomReminder() {
random.seed = 0.3865f
Freeze.freezeClock {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(REMINDER_LAST, now.minusDays(1)),
with(CREATION_TIME, now.minusDays(30)),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_WEEK))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, now.minusDays(1).millis + 584206592, ReminderService.TYPE_RANDOM))
}
}
@Test
fun scheduleOverdueRandomReminder() {
random.seed = 0.3865f
Freeze.freezeClock {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(REMINDER_LAST, now.minusDays(14)),
with(CREATION_TIME, now.minusDays(30)),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_WEEK))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, now.millis + 10148400, ReminderService.TYPE_RANDOM))
}
}
@Test
fun scheduleOverdueNoLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, null as DateTime?),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 23, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverduePastLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 24, 12, 0)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 24, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverdueBeforeLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 12, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 24, 15, 0)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 25, 12, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverdueWithNoDueTime() {
preferences.setInt(R.string.p_rmd_time, TimeUnit.HOURS.toMillis(15).toInt())
val task = newTask(
with(ID, 1L),
with(DUE_DATE, DateTime(2017, 9, 22)),
with(REMINDER_LAST, DateTime(2017, 9, 23, 12, 17, 59, 999)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 23, 15, 0, 0, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleSubsequentOverdueReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 23, 15, 30, 59, 999)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 24, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun scheduleOverdueAfterLastReminder() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 9, 22, 15, 30)),
with(REMINDER_LAST, DateTime(2017, 9, 23, 12, 17, 59, 999)),
with(REMINDERS, Task.NOTIFY_AFTER_DEADLINE))
service.scheduleAlarm(task)
verify(ReminderEntry(1L, DateTime(2017, 9, 23, 15, 30, 1, 0).millis, ReminderService.TYPE_OVERDUE))
}
@Test
fun snoozeOverridesAll() {
val now = DateTimeUtils.newDateTime()
val task = newTask(
with(ID, 1L),
with(DUE_TIME, now),
with(SNOOZE_TIME, now.plusMonths(12)),
with(REMINDERS, Task.NOTIFY_AT_DEADLINE or Task.NOTIFY_AFTER_DEADLINE),
with(RANDOM_REMINDER_PERIOD, DateUtilities.ONE_HOUR))
service.scheduleAlarm(task)
verify(ReminderEntry(1, now.plusMonths(12).millis, ReminderService.TYPE_SNOOZE))
}
private fun verify(vararg reminders: ReminderEntry) = assertEquals(reminders.toList(), jobs.getJobs())
internal class RandomStub : Random() {
var seed = 1.0f
override fun nextFloat() = seed
}
}

@ -0,0 +1,193 @@
package com.todoroo.astrid.repeats
import android.annotation.SuppressLint
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.ical.values.RRule
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.gcal.GCalHelper
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.InOrder
import org.mockito.Mockito
import org.tasks.LocalBroadcastManager
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.makers.TaskMaker.AFTER_COMPLETE
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.RRULE
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import java.text.ParseException
import javax.inject.Inject
@SuppressLint("NewApi")
@RunWith(AndroidJUnit4::class)
class RepeatTaskHelperTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
private lateinit var localBroadcastManager: LocalBroadcastManager
private lateinit var alarmService: AlarmService
private lateinit var gCalHelper: GCalHelper
private lateinit var helper: RepeatTaskHelper
private lateinit var mocks: InOrder
@Before
fun before() {
alarmService = Mockito.mock(AlarmService::class.java)
gCalHelper = Mockito.mock(GCalHelper::class.java)
localBroadcastManager = Mockito.mock(LocalBroadcastManager::class.java)
mocks = Mockito.inOrder(alarmService, gCalHelper, localBroadcastManager)
helper = RepeatTaskHelper(gCalHelper, alarmService, taskDao, localBroadcastManager)
}
@After
fun after() {
Mockito.verifyNoMoreInteractions(localBroadcastManager, gCalHelper, alarmService)
}
@Test
fun noRepeat() {
helper.handleRepeat(newTask(with(DUE_TIME, DateTime(2017, 10, 4, 13, 30))))
}
@Test
@Throws(ParseException::class)
fun testMinutelyRepeat() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(RRULE, RRule("RRULE:FREQ=MINUTELY;INTERVAL=30")))
repeatAndVerify(
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 14, 0, 1))
}
@Test
@Throws(ParseException::class)
fun testMinutelyRepeatAfterCompletion() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(COMPLETION_TIME, DateTime(2017, 10, 4, 13, 17, 45, 340)),
with(RRULE, RRule("RRULE:FREQ=MINUTELY;INTERVAL=30")),
with(AFTER_COMPLETE, true))
repeatAndVerify(
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 13, 47, 1))
}
@Test
@Throws(ParseException::class)
fun testMinutelyDecrementCount() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(RRULE, RRule("RRULE:FREQ=MINUTELY;COUNT=2;INTERVAL=30")))
repeatAndVerify(
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 14, 0, 1))
assertEquals(1, RRule(task.getRecurrenceWithoutFrom()).count)
}
@Test
@Throws(ParseException::class)
fun testMinutelyLastOccurrence() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(RRULE, RRule("RRULE:FREQ=MINUTELY;COUNT=1;INTERVAL=30")))
helper.handleRepeat(task)
}
@Test
@Throws(ParseException::class)
fun testHourlyRepeat() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(RRULE, RRule("RRULE:FREQ=HOURLY;INTERVAL=6")))
repeatAndVerify(
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 19, 30, 1))
}
@Test
@Throws(ParseException::class)
fun testHourlyRepeatAfterCompletion() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(COMPLETION_TIME, DateTime(2017, 10, 4, 13, 17, 45, 340)),
with(RRULE, RRule("RRULE:FREQ=HOURLY;INTERVAL=6")),
with(AFTER_COMPLETE, true))
repeatAndVerify(
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 4, 19, 17, 1))
}
@Test
@Throws(ParseException::class)
fun testDailyRepeat() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(RRULE, RRule("RRULE:FREQ=DAILY;INTERVAL=6")))
repeatAndVerify(
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 10, 13, 30, 1))
}
@Test
@Throws(ParseException::class)
fun testRepeatWeeklyNoDays() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(RRULE, RRule("RRULE:FREQ=WEEKLY;INTERVAL=2")))
repeatAndVerify(
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2017, 10, 18, 13, 30, 1))
}
@Test
@Throws(ParseException::class)
fun testYearly() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(RRULE, RRule("RRULE:FREQ=YEARLY;INTERVAL=3")))
repeatAndVerify(
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2020, 10, 4, 13, 30, 1))
}
@Test
@Throws(ParseException::class)
fun testMonthlyRepeat() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
with(RRULE, RRule("RRULE:FREQ=MONTHLY;INTERVAL=3")))
repeatAndVerify(
task, DateTime(2017, 10, 4, 13, 30, 1), DateTime(2018, 1, 4, 13, 30, 1))
}
@Test
@Throws(ParseException::class)
fun testMonthlyRepeatAtEndOfMonth() {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 1, 31, 13, 30)),
with(RRULE, RRule("RRULE:FREQ=MONTHLY;INTERVAL=1")))
repeatAndVerify(
task, DateTime(2017, 1, 31, 13, 30, 1), DateTime(2017, 2, 28, 13, 30, 1))
}
private fun repeatAndVerify(task: Task, oldDueDate: DateTime, newDueDate: DateTime) {
helper.handleRepeat(task)
mocks.verify(gCalHelper).rescheduleRepeatingTask(task)
mocks.verify(alarmService).rescheduleAlarms(1, oldDueDate.millis, newDueDate.millis)
mocks.verify(localBroadcastManager).broadcastRepeat(1, oldDueDate.millis, newDueDate.millis)
}
override fun inject(component: TestComponent) = component.inject(this)
}

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

@ -5,23 +5,26 @@
*/ */
package com.todoroo.astrid.service package com.todoroo.astrid.service
import org.tasks.data.entity.Task import androidx.test.ext.junit.runners.AndroidJUnit4
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.utility.TitleParser import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.TagDataDao import org.junit.runner.RunWith
import org.tasks.data.TagDataDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class QuickAddMarkupTest : InjectingTestCase() { class QuickAddMarkupTest : InjectingTestCase() {
private val tags = ArrayList<String>() private val tags = ArrayList<String>()
@Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var tagDataDao: TagDataDao
private var task: Task? = null private var task: Task? = null
override fun inject(component: TestComponent) = component.inject(this)
@Test @Test
fun testTags() { fun testTags() {
@ -79,11 +82,11 @@ class QuickAddMarkupTest : InjectingTestCase() {
assertEquals(title, task!!.title) assertEquals(title, task!!.title)
} }
private fun whenTitleIs(title: String) = runBlocking { private fun whenTitleIs(title: String) {
task = Task() task = Task()
task!!.title = title task!!.title = title
tags.clear() tags.clear()
TitleParser.parse(tagDataDao, task!!, tags) TitleParser.parse(tagDataDao, task, tags)
} }
private fun assertPriority(priority: Int) { private fun assertPriority(priority: Int) {

@ -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,123 +1,123 @@
package com.todoroo.astrid.service package com.todoroo.astrid.service
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Assert.*
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.CaldavDao import org.junit.runner.RunWith
import org.tasks.data.dao.GoogleTaskDao import org.tasks.data.CaldavCalendar
import org.tasks.data.entity.CaldavAccount import org.tasks.data.CaldavDao
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV import org.tasks.data.GoogleTaskDao
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.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.jobs.WorkManager
import org.tasks.makers.CaldavTaskMaker
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.GoogleTaskMaker.LIST
import org.tasks.makers.GoogleTaskMaker.PARENT
import org.tasks.makers.GoogleTaskMaker.TASK
import org.tasks.makers.GoogleTaskMaker.newGoogleTask
import org.tasks.makers.GtaskListMaker
import org.tasks.makers.GtaskListMaker.newGtaskList
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.ID import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class TaskMoverTest : InjectingTestCase() { class TaskMoverTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var workManager: WorkManager
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskMover: TaskMover @Inject lateinit var taskMover: TaskMover
@Before @Before
fun setup() { override fun setUp() {
runBlocking { super.setUp()
caldavDao.insert(CaldavCalendar(uuid = "1", account = "account1")) taskDao.initialize(workManager)
caldavDao.insert(CaldavCalendar(uuid = "2", account = "account2"))
}
} }
@Test @Test
fun moveBetweenGoogleTaskLists() = runBlocking { fun moveBetweenGoogleTaskLists() {
setAccountType("account1", TYPE_GOOGLE_TASKS)
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1) createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1"))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
moveToGoogleTasks("2", 1) moveToGoogleTasks("2", 1)
assertEquals("2", googleTaskDao.getByTaskId(1)?.calendar) assertEquals("2", googleTaskDao.getByTaskId(1)!!.listId)
} }
@Test @Test
fun deleteGoogleTaskAfterMove() = runBlocking { fun deleteGoogleTaskAfterMove() {
createTasks(1) createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1"))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
moveToGoogleTasks("2", 1) moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(1, "account1") val deleted = googleTaskDao.getDeletedByTaskId(1)
assertEquals(1, deleted.size.toLong()) assertEquals(1, deleted.size.toLong())
assertEquals(1, deleted[0].task) assertEquals(1, deleted[0].task)
assertTrue(deleted[0].deleted > 0) assertTrue(deleted[0].deleted > 0)
} }
@Test @Test
fun moveChildrenBetweenGoogleTaskLists() = runBlocking { fun moveChildrenBetweenGoogleTaskLists() {
setAccountType("account1", TYPE_GOOGLE_TASKS) createTasks(1, 2)
setAccountType("account2", TYPE_GOOGLE_TASKS) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
createTasks(1) googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(PARENT, 1L)))
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1) moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(2, "account1") val deleted = googleTaskDao.getDeletedByTaskId(2)
assertEquals(1, deleted.size.toLong()) assertEquals(1, deleted.size.toLong())
assertEquals(2, deleted[0].task) assertEquals(2, deleted[0].task)
assertTrue(deleted[0].deleted > 0) assertTrue(deleted[0].deleted > 0)
assertEquals(1L, taskDao.fetch(2)?.parent) val task = googleTaskDao.getByTaskId(2)!!
assertEquals("2", googleTaskDao.getByTaskId(2)?.calendar) assertEquals(1, task.parent)
assertEquals("2", task.listId)
} }
@Test @Test
fun moveBetweenCaldavList() = runBlocking { fun moveBetweenCaldavList() {
createTasks(1) createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1"))) caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("2", 1) moveToCaldavList("2", 1)
assertEquals("2", caldavDao.getTask(1)!!.calendar) assertEquals("2", caldavDao.getTask(1)!!.calendar)
} }
@Test @Test
fun deleteCaldavTaskAfterMove() = runBlocking { fun deleteCaldavTaskAfterMove() {
createTasks(1) createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1"))) caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("2", 1) moveToCaldavList("2", 1)
val deleted = caldavDao.getMoved("1") val deleted = caldavDao.getDeleted("1")
assertEquals(1, deleted.size.toLong()) assertEquals(1, deleted.size.toLong())
assertEquals(1, deleted[0].task) assertEquals(1, deleted[0].task)
assertTrue(deleted[0].deleted > 0) assertTrue(deleted[0].deleted > 0)
} }
@Test @Test
fun moveRecursiveCaldavChildren() = runBlocking { fun moveRecursiveCaldavChildren() {
createTasks(1) createTasks(1)
createSubtask(2, 1) createSubtask(2, 1)
createSubtask(3, 2) createSubtask(3, 2)
caldavDao.insert( caldavDao.insert(
listOf( listOf(
newCaldavTask( newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")), with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask( newCaldavTask(
with(TASK, 2L), with(CaldavTaskMaker.TASK, 2L),
with(CALENDAR, "1"), with(CALENDAR, "1"),
with(REMOTE_ID, "b"), with(REMOTE_ID, "b"),
with(REMOTE_PARENT, "a")), with(REMOTE_PARENT, "a")),
newCaldavTask( newCaldavTask(
with(TASK, 3L), with(CaldavTaskMaker.TASK, 3L),
with(CALENDAR, "1"), with(CALENDAR, "1"),
with(REMOTE_PARENT, "b")))) with(REMOTE_PARENT, "b"))))
moveToCaldavList("2", 1) moveToCaldavList("2", 1)
val deleted = caldavDao.getMoved("1") val deleted = caldavDao.getDeleted("1")
assertEquals(3, deleted.size.toLong()) assertEquals(3, deleted.size.toLong())
val task = caldavDao.getTask(3) val task = caldavDao.getTask(3)
assertEquals("2", task!!.calendar) assertEquals("2", task!!.calendar)
@ -125,38 +125,36 @@ class TaskMoverTest : InjectingTestCase() {
} }
@Test @Test
fun moveGoogleTaskChildrenToCaldav() = runBlocking { fun moveGoogleTaskChildrenToCaldav() {
setAccountType("account1", TYPE_GOOGLE_TASKS) createTasks(1, 2)
setAccountType("account2", TYPE_CALDAV) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
createTasks(1) googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(PARENT, 1L)))
createSubtask(2, 1) moveToCaldavList("1", 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) val task = caldavDao.getTask(2)
assertEquals("2", task!!.calendar) assertEquals("1", task!!.calendar)
assertEquals(1L, taskDao.fetch(2)?.parent) assertEquals(1, taskDao.fetch(2)!!.parent)
} }
@Test @Test
fun flattenLocalSubtasksWhenMovingToGoogleTasks() = runBlocking { fun flattenLocalSubtasksWhenMovingToGoogleTasks() {
createTasks(1) createTasks(1)
createSubtask(2, 1) createSubtask(2, 1)
createSubtask(3, 2) createSubtask(3, 2)
moveToGoogleTasks("1", 1) moveToGoogleTasks("1", 1)
assertEquals(1L, taskDao.fetch(3)?.parent) assertEquals(1, googleTaskDao.getByTaskId(3)!!.parent)
assertEquals(0, taskDao.fetch(3)!!.parent)
} }
@Test @Test
fun moveLocalChildToGoogleTasks() = runBlocking { fun moveLocalChildToGoogleTasks() {
createTasks(1) createTasks(1)
createSubtask(2, 1) createSubtask(2, 1)
moveToGoogleTasks("1", 2) moveToGoogleTasks("1", 2)
assertEquals(0L, taskDao.fetch(2)?.parent) assertEquals(0, taskDao.fetch(2)!!.parent)
} }
@Test @Test
fun moveLocalChildToCaldav() = runBlocking { fun moveLocalChildToCaldav() {
createTasks(1) createTasks(1)
createSubtask(2, 1) createSubtask(2, 1)
moveToCaldavList("1", 2) moveToCaldavList("1", 2)
@ -164,50 +162,49 @@ class TaskMoverTest : InjectingTestCase() {
} }
@Test @Test
fun flattenCaldavSubtasksWhenMovingToGoogleTasks() = runBlocking { fun flattenCaldavSubtasksWhenMovingToGoogleTasks() {
createTasks(1) createTasks(1)
createSubtask(2, 1) createSubtask(2, 1)
createSubtask(3, 2) createSubtask(3, 2)
caldavDao.insert( caldavDao.insert(
listOf( listOf(
newCaldavTask( newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")), with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask( newCaldavTask(
with(TASK, 2L), with(CaldavTaskMaker.TASK, 2L),
with(CALENDAR, "1"), with(CALENDAR, "1"),
with(REMOTE_ID, "b"), with(REMOTE_ID, "b"),
with(REMOTE_PARENT, "a")), with(REMOTE_PARENT, "a")),
newCaldavTask( newCaldavTask(
with(TASK, 3L), with(CaldavTaskMaker.TASK, 3L),
with(CALENDAR, "1"), with(CALENDAR, "1"),
with(REMOTE_PARENT, "b")))) with(REMOTE_PARENT, "b"))))
moveToGoogleTasks("2", 1) moveToGoogleTasks("1", 1)
val task = taskDao.fetch(3L) val task = googleTaskDao.getByTaskId(3L)!!
assertEquals(1L, task?.parent) assertEquals(1, task.parent)
} }
@Test @Test
fun moveGoogleTaskChildWithoutParent() = runBlocking { fun moveGoogleTaskChildWithoutParent() {
setAccountType("account2", TYPE_GOOGLE_TASKS) createTasks(1, 2)
createTasks(1) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
createSubtask(2, 1) googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(PARENT, 1L)))
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToGoogleTasks("2", 2) moveToGoogleTasks("2", 2)
assertEquals(0L, taskDao.fetch(2)?.parent) val task = googleTaskDao.getByTaskId(2)!!
assertEquals("2", googleTaskDao.getByTaskId(2)?.calendar) assertEquals(0L, task.parent)
assertEquals("2", task.listId)
} }
@Test @Test
fun moveCaldavChildWithoutParent() = runBlocking { fun moveCaldavChildWithoutParent() {
createTasks(1) createTasks(1)
createSubtask(2, 1) createSubtask(2, 1)
caldavDao.insert( caldavDao.insert(
listOf( listOf(
newCaldavTask( newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")), with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask( newCaldavTask(
with(TASK, 2L), with(CaldavTaskMaker.TASK, 2L),
with(CALENDAR, "1"), with(CALENDAR, "1"),
with(REMOTE_PARENT, "a")))) with(REMOTE_PARENT, "a"))))
moveToCaldavList("2", 2) moveToCaldavList("2", 2)
@ -216,114 +213,91 @@ class TaskMoverTest : InjectingTestCase() {
} }
@Test @Test
fun moveGoogleTaskToCaldav() = runBlocking { fun moveGoogleTaskToCaldav() {
createTasks(1) createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1"))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
moveToCaldavList("2", 1) moveToCaldavList("2", 1)
assertEquals("2", caldavDao.getTask(1)!!.calendar) assertEquals("2", caldavDao.getTask(1)!!.calendar)
} }
@Test @Test
fun moveCaldavToGoogleTask() = runBlocking { fun moveCaldavToGoogleTask() {
setAccountType("account1", TYPE_CALDAV)
setAccountType("account2", TYPE_GOOGLE_TASKS)
createTasks(1) createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1"))) caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1) moveToGoogleTasks("2", 1)
assertEquals("2", googleTaskDao.getByTaskId(1L)?.calendar) assertEquals("2", googleTaskDao.getByTaskId(1L)!!.listId)
} }
@Test @Test
fun moveLocalToCaldav() = runBlocking { fun moveLocalToCaldav() {
createTasks(1) createTasks(1)
createSubtask(2, 1) createSubtask(2, 1)
createSubtask(3, 2) createSubtask(3, 2)
moveToCaldavList("1", 1) moveToCaldavList("1", 1)
assertEquals("1", caldavDao.getTask(3)?.calendar) assertEquals("1", caldavDao.getTask(3)!!.calendar)
assertEquals(2L, taskDao.fetch(3)?.parent) assertEquals(2, taskDao.fetch(3)!!.parent)
} }
@Test @Test
fun moveToSameGoogleTaskListIsNoop() = runBlocking { fun moveToSameGoogleTaskListIsNoop() {
setAccountType("account1", TYPE_GOOGLE_TASKS)
createTasks(1) createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1"))) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
moveToGoogleTasks("1", 1) moveToGoogleTasks("1", 1)
assertTrue(googleTaskDao.getDeletedByTaskId(1, "account1").isEmpty()) assertTrue(googleTaskDao.getDeletedByTaskId(1).isEmpty())
assertEquals(1, googleTaskDao.getAllByTaskId(1).size.toLong()) assertEquals(1, googleTaskDao.getAllByTaskId(1).size.toLong())
} }
@Test @Test
fun moveToSameCaldavListIsNoop() = runBlocking { fun moveToSameCaldavListIsNoop() {
createTasks(1) createTasks(1)
caldavDao.insert(newCaldavTask(with(TASK, 1L), with(CALENDAR, "1"))) caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("1", 1) moveToCaldavList("1", 1)
assertTrue(caldavDao.getMoved("1").isEmpty()) assertTrue(caldavDao.getDeleted("1").isEmpty())
assertEquals(1, caldavDao.getTasks(1).size.toLong()) assertEquals(1, caldavDao.getTasks(1).size.toLong())
} }
@Test @Test
fun dontDuplicateWhenParentAndChildGoogleTaskMoved() = runBlocking { fun dontDuplicateWhenParentAndChildGoogleTaskMoved() {
createTasks(1) createTasks(1, 2)
createSubtask(2, 1) googleTaskDao.insert(newGoogleTask(with(TASK, 1), with(LIST, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1"))) googleTaskDao.insert(newGoogleTask(with(TASK, 2), with(LIST, "1"), with(PARENT, 1L)))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1, 2) moveToGoogleTasks("2", 1, 2)
assertEquals(1, googleTaskDao.getAllByTaskId(2).filter { it.deleted == 0L }.size) assertEquals(1, googleTaskDao.getAllByTaskId(2).filter { it.deleted == 0L }.size)
} }
@Test @Test
fun dontDuplicateWhenParentAndChildCaldavMoved() = runBlocking { fun dontDuplicateWhenParentAndChildCaldavMoved() {
createTasks(1) createTasks(1)
createSubtask(2, 1) createSubtask(2, 1)
caldavDao.insert( caldavDao.insert(
listOf( listOf(
newCaldavTask( newCaldavTask(
with(TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")), with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1"), with(REMOTE_ID, "a")),
newCaldavTask( newCaldavTask(
with(TASK, 2L), with(CaldavTaskMaker.TASK, 2L),
with(CALENDAR, "1"), with(CALENDAR, "1"),
with(REMOTE_PARENT, "a")))) with(REMOTE_PARENT, "a"))))
moveToCaldavList("2", 1, 2) moveToCaldavList("2", 1, 2)
assertEquals(1, caldavDao.getTasks(2).filter { it.deleted == 0L }.size) assertEquals(1, caldavDao.getTasks(2).filter { it.deleted == 0L }.size)
} }
private suspend fun createTasks(vararg ids: Long) { private fun createTasks(vararg ids: Long) {
for (id in ids) { for (id in ids) {
taskDao.createNew(newTask(with(ID, id))) taskDao.createNew(newTask(with(ID, id)))
} }
} }
private suspend fun createSubtask(id: Long, parent: Long) { private fun createSubtask(id: Long, parent: Long) {
taskDao.createNew(newTask(with(ID, id), with(PARENT, parent))) taskDao.createNew(newTask(with(ID, id), with(TaskMaker.PARENT, parent)))
} }
private suspend fun moveToGoogleTasks(list: String, vararg tasks: Long) { private fun moveToGoogleTasks(list: String, vararg tasks: Long) {
taskMover.move( taskMover.move(tasks.toList(), GtasksFilter(newGtaskList(with(GtaskListMaker.REMOTE_ID, list))))
tasks.toList(),
CaldavFilter(
calendar = CaldavCalendar(uuid = list),
account = CaldavAccount(accountType = TYPE_GOOGLE_TASKS)
)
)
} }
private suspend fun moveToCaldavList(calendar: String, vararg tasks: Long) { private fun moveToCaldavList(calendar: String, vararg tasks: Long) {
taskMover.move( taskMover.move(tasks.toList(), CaldavFilter(CaldavCalendar("", calendar)))
tasks.toList(),
CaldavFilter(
CaldavCalendar(name = "", uuid = calendar),
account = CaldavAccount(accountType = TYPE_CALDAV)
)
)
} }
private suspend fun setAccountType(account: String, type: Int) { override fun inject(component: TestComponent) = component.inject(this)
caldavDao.insert(
CaldavAccount(
uuid = account,
accountType = type,
)
)
}
} }

@ -5,53 +5,42 @@
*/ */
package com.todoroo.astrid.service package com.todoroo.astrid.service
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.ical.values.Frequency
import com.google.ical.values.RRule
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.utility.TitleParser import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Assert.*
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.junit.Test
import org.junit.runner.RunWith
import org.tasks.R import org.tasks.R
import org.tasks.data.dao.CaldavDao import org.tasks.data.TagDataDao
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.date.DateTimeUtils
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.TestComponent
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.repeats.RecurrenceUtils.newRecur import java.util.*
import java.util.Calendar
import javax.inject.Inject import javax.inject.Inject
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class TitleParserTest : InjectingTestCase() { class TitleParserTest : InjectingTestCase() {
@Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var taskCreator: TaskCreator @Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var caldavDao: CaldavDao
@Before
override fun setUp() { override fun setUp() {
runBlocking { super.setUp()
super.setUp() preferences.setStringFromInteger(R.string.p_default_urgency_key, 0)
preferences.setStringFromInteger(R.string.p_default_urgency_key, 0)
caldavDao.newLocalAccount()
}
} }
override fun inject(component: TestComponent) = component.inject(this)
/** /**
* test that completing a task w/ no regular expressions creates a simple task with no date, no * test that completing a task w/ no regular expressions creates a simple task with no date, no
* repeat, no lists * repeat, no lists
*/ */
@Test @Test
fun testNoRegexes() = runBlocking { fun testNoRegexes() {
val task = taskCreator.basicQuickAddTask("Jog") val task = taskCreator.basicQuickAddTask("Jog")
val nothing = Task() val nothing = Task()
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
@ -157,14 +146,13 @@ class TitleParserTest : InjectingTestCase() {
} }
} }
private fun insertTitleAddTask(title: String): Task = runBlocking { private fun insertTitleAddTask(title: String): Task {
taskCreator.createWithValues(title) return taskCreator.createWithValues(title)
} }
// ----------------Days begin----------------// // ----------------Days begin----------------//
@Test @Test
@Ignore("Flaky test") fun testDays() {
fun testDays() = runBlocking {
val today = Calendar.getInstance() val today = Calendar.getInstance()
var title = "Jog today" var title = "Jog today"
var task = taskCreator.createWithValues(title) var task = taskCreator.createWithValues(title)
@ -193,7 +181,7 @@ class TitleParserTest : InjectingTestCase() {
// ----------------Priority begin----------------// // ----------------Priority begin----------------//
/** tests all words using priority 0 */ /** tests all words using priority 0 */
@Test @Test
fun testPriority0() = runBlocking { fun testPriority0() {
val acceptedStrings = arrayOf("priority 0", "least priority", "lowest priority", "bang 0") val acceptedStrings = arrayOf("priority 0", "least priority", "lowest priority", "bang 0")
for (acceptedString in acceptedStrings) { for (acceptedString in acceptedStrings) {
val title = "Jog $acceptedString" val title = "Jog $acceptedString"
@ -208,7 +196,7 @@ class TitleParserTest : InjectingTestCase() {
} }
@Test @Test
fun testPriority1() = runBlocking { fun testPriority1() {
val acceptedStringsAtEnd = arrayOf("priority 1", "low priority", "bang", "bang 1") val acceptedStringsAtEnd = arrayOf("priority 1", "low priority", "bang", "bang 1")
val acceptedStringsAnywhere = arrayOf("!1", "!") val acceptedStringsAnywhere = arrayOf("!1", "!")
var task: Task var task: Task
@ -233,7 +221,7 @@ class TitleParserTest : InjectingTestCase() {
} }
@Test @Test
fun testPriority2() = runBlocking { fun testPriority2() {
val acceptedStringsAtEnd = arrayOf("priority 2", "high priority", "bang bang", "bang 2") val acceptedStringsAtEnd = arrayOf("priority 2", "high priority", "bang bang", "bang 2")
val acceptedStringsAnywhere = arrayOf("!2", "!!") val acceptedStringsAnywhere = arrayOf("!2", "!!")
for (acceptedStringAtEnd in acceptedStringsAtEnd) { for (acceptedStringAtEnd in acceptedStringsAtEnd) {
@ -255,7 +243,7 @@ class TitleParserTest : InjectingTestCase() {
} }
@Test @Test
fun testPriority3() = runBlocking { fun testPriority3() {
val acceptedStringsAtEnd = arrayOf( val acceptedStringsAtEnd = arrayOf(
"priority 3", "priority 3",
"highest priority", "highest priority",
@ -285,25 +273,25 @@ class TitleParserTest : InjectingTestCase() {
// ----------------Repeats begin----------------// // ----------------Repeats begin----------------//
/** test daily repeat from due date, but with no due date set */ /** test daily repeat from due date, but with no due date set */
@Test @Test
fun testDailyWithNoDueDate() = runBlocking { fun testDailyWithNoDueDate() {
var title = "Jog daily" var title = "Jog daily"
var task = taskCreator.createWithValues(title) var task = taskCreator.createWithValues(title)
val recur = newRecur() val rrule = RRule()
recur.setFrequency(DAILY.name) rrule.freq = Frequency.DAILY
recur.interval = 1 rrule.interval = 1
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
title = "Jog every day" title = "Jog every day"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i days." title = "Jog every $i days."
recur.interval = i rrule.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
} }
@ -311,25 +299,25 @@ class TitleParserTest : InjectingTestCase() {
/** test weekly repeat from due date, with no due date & time set */ /** test weekly repeat from due date, with no due date & time set */
@Test @Test
fun testWeeklyWithNoDueDate() = runBlocking { fun testWeeklyWithNoDueDate() {
var title = "Jog weekly" var title = "Jog weekly"
var task = taskCreator.createWithValues(title) var task = taskCreator.createWithValues(title)
val recur = newRecur() val rrule = RRule()
recur.setFrequency(WEEKLY.name) rrule.freq = Frequency.WEEKLY
recur.interval = 1 rrule.interval = 1
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
title = "Jog every week" title = "Jog every week"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i weeks" title = "Jog every $i weeks"
recur.interval = i rrule.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
} }
@ -337,70 +325,70 @@ class TitleParserTest : InjectingTestCase() {
/** test hourly repeat from due date, with no due date but no time */ /** test hourly repeat from due date, with no due date but no time */
@Test @Test
fun testMonthlyFromNoDueDate() = runBlocking { fun testMonthlyFromNoDueDate() {
var title = "Jog monthly" var title = "Jog monthly"
var task = taskCreator.createWithValues(title) var task = taskCreator.createWithValues(title)
val recur = newRecur() val rrule = RRule()
recur.setFrequency(MONTHLY.name) rrule.freq = Frequency.MONTHLY
recur.interval = 1 rrule.interval = 1
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
title = "Jog every month" title = "Jog every month"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i months" title = "Jog every $i months"
recur.interval = i rrule.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertFalse(task.hasDueTime()) assertFalse(task.hasDueTime())
assertFalse(task.hasDueDate()) assertFalse(task.hasDueDate())
} }
} }
@Test @Test
fun testDailyFromDueDate() = runBlocking { fun testDailyFromDueDate() {
var title = "Jog daily starting from today" var title = "Jog daily starting from today"
var task = taskCreator.createWithValues(title) var task = taskCreator.createWithValues(title)
val recur = newRecur() val rrule = RRule()
recur.setFrequency(DAILY.name) rrule.freq = Frequency.DAILY
recur.interval = 1 rrule.interval = 1
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
title = "Jog every day starting from today" title = "Jog every day starting from today"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i days starting from today" title = "Jog every $i days starting from today"
recur.interval = i rrule.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
} }
} }
@Test @Test
fun testWeeklyFromDueDate() = runBlocking { fun testWeeklyFromDueDate() {
var title = "Jog weekly starting from today" var title = "Jog weekly starting from today"
var task = taskCreator.createWithValues(title) var task = taskCreator.createWithValues(title)
val recur = newRecur() val rrule = RRule()
recur.setFrequency(WEEKLY.name) rrule.freq = Frequency.WEEKLY
recur.interval = 1 rrule.interval = 1
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
title = "Jog every week starting from today" title = "Jog every week starting from today"
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
for (i in 1..12) { for (i in 1..12) {
title = "Jog every $i weeks starting from today" title = "Jog every $i weeks starting from today"
recur.interval = i rrule.interval = i
task = taskCreator.createWithValues(title) task = taskCreator.createWithValues(title)
assertEquals(task.recurrence, recur.toString()) assertEquals(task.recurrence, rrule.toIcal())
assertTrue(task.hasDueDate()) assertTrue(task.hasDueDate())
} }
} }
@ -408,7 +396,7 @@ class TitleParserTest : InjectingTestCase() {
// ----------------Tags begin----------------// // ----------------Tags begin----------------//
/** tests all words using priority 0 */ /** tests all words using priority 0 */
@Test @Test
fun testTagsPound() = runBlocking { fun testTagsPound() {
val acceptedStrings = arrayOf("#tag", "#a", "#(a cool tag)", "#(cool)") val acceptedStrings = arrayOf("#tag", "#a", "#(a cool tag)", "#(cool)")
var task: Task var task: Task
for (acceptedString in acceptedStrings) { for (acceptedString in acceptedStrings) {
@ -425,7 +413,7 @@ class TitleParserTest : InjectingTestCase() {
/** tests all words using priority 0 */ /** tests all words using priority 0 */
@Test @Test
fun testTagsAt() = runBlocking { fun testTagsAt() {
val acceptedStrings = arrayOf("@tag", "@a", "@(a cool tag)", "@(cool)") val acceptedStrings = arrayOf("@tag", "@a", "@(a cool tag)", "@(cool)")
var task: Task var task: Task
for (acceptedString in acceptedStrings) { for (acceptedString in acceptedStrings) {

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

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

@ -1,28 +1,26 @@
package com.todoroo.astrid.subtasks package com.todoroo.astrid.subtasks
import org.tasks.data.entity.Task import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidTest import com.todoroo.astrid.data.Task
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.data.entity.TaskListMetadata import org.junit.runner.RunWith
import org.tasks.data.TaskListMetadata
import org.tasks.injection.TestComponent
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class SubtasksHelperTest : SubtasksTestCase() { class SubtasksHelperTest : SubtasksTestCase() {
@Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
createTasks() createTasks()
val m = TaskListMetadata() val m = TaskListMetadata()
m.filter = TaskListMetadata.FILTER_ID_ALL m.filter = TaskListMetadata.FILTER_ID_ALL
runBlocking { updater.initializeFromSerializedTree(
updater.initializeFromSerializedTree( m, filter, SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE))
m, filter, SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE))
}
} }
private fun createTask(title: String, uuid: String) = runBlocking { private fun createTask(title: String, uuid: String) {
val t = Task() val t = Task()
t.title = title t.title = title
t.uuid = uuid t.uuid = uuid
@ -49,14 +47,16 @@ class SubtasksHelperTest : SubtasksTestCase() {
} }
@Test @Test
fun testLocalToRemoteIdMapping() = runBlocking { fun testLocalToRemoteIdMapping() {
val mapped = SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE) val mapped = SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE)
.replace("\\s".toRegex(), "") .replace("\\s".toRegex(), "")
assertEquals(EXPECTED_REMOTE, mapped) assertEquals(EXPECTED_REMOTE, mapped)
} }
override fun inject(component: TestComponent) = component.inject(this)
companion object { companion object {
private val EXPECTED_ORDER = arrayOf("-1", "1", "2", "3", "4", "5", "6") 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(), "") private val EXPECTED_REMOTE = "[\"-1\", [\"6\", \"4\", [\"3\", \"1\"]], \"2\", \"5\"]".replace("\\s".toRegex(), "")
} }
} }

@ -1,13 +1,13 @@
package com.todoroo.astrid.subtasks package com.todoroo.astrid.subtasks
import org.tasks.data.entity.Task import androidx.test.ext.junit.runners.AndroidJUnit4
import dagger.hilt.android.testing.HiltAndroidTest import com.todoroo.astrid.data.Task
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.data.entity.TaskListMetadata import org.junit.runner.RunWith
import org.tasks.data.TaskListMetadata
import org.tasks.injection.TestComponent
@HiltAndroidTest @RunWith(AndroidJUnit4::class)
class SubtasksMovingTest : SubtasksTestCase() { class SubtasksMovingTest : SubtasksTestCase() {
private lateinit var A: Task private lateinit var A: Task
private lateinit var B: Task private lateinit var B: Task
@ -16,16 +16,13 @@ class SubtasksMovingTest : SubtasksTestCase() {
private lateinit var E: Task private lateinit var E: Task
private lateinit var F: Task private lateinit var F: Task
@Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
createTasks() createTasks()
val m = TaskListMetadata() val m = TaskListMetadata()
m.filter = TaskListMetadata.FILTER_ID_ALL m.filter = TaskListMetadata.FILTER_ID_ALL
runBlocking { updater.initializeFromSerializedTree(
updater.initializeFromSerializedTree( m, filter, SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE))
m, filter, SubtasksHelper.convertTreeToRemoteIds(taskDao, DEFAULT_SERIALIZED_TREE))
}
// Assert initial state is correct // Assert initial state is correct
expectParentAndPosition(A, null, 0) expectParentAndPosition(A, null, 0)
@ -36,6 +33,8 @@ class SubtasksMovingTest : SubtasksTestCase() {
expectParentAndPosition(F, null, 2) expectParentAndPosition(F, null, 2)
} }
override fun inject(component: TestComponent) = component.inject(this)
private fun createTasks() { private fun createTasks() {
A = createTask("A") A = createTask("A")
B = createTask("B") B = createTask("B")
@ -45,16 +44,16 @@ class SubtasksMovingTest : SubtasksTestCase() {
F = createTask("F") F = createTask("F")
} }
private fun createTask(title: String): Task = runBlocking { private fun createTask(title: String): Task {
val task = Task() val task = Task()
task.title = title task.title = title
taskDao.createNew(task) taskDao.createNew(task)
task return task
} }
private fun whenTriggerMoveBefore(target: Task?, before: Task?) = runBlocking { private fun whenTriggerMoveBefore(target: Task?, before: Task?) {
val beforeId = before?.uuid ?: "-1" val beforeId = if (before == null) "-1" else before.uuid
updater.moveTo(TaskListMetadata(), filter, target!!.uuid, beforeId) updater.moveTo(null, filter, target!!.uuid, beforeId)
} }
/* Starting State (see SubtasksTestCase): /* Starting State (see SubtasksTestCase):

@ -1,37 +1,37 @@
package com.todoroo.astrid.subtasks package com.todoroo.astrid.subtasks
import androidx.test.InstrumentationRegistry
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import kotlinx.coroutines.runBlocking import com.todoroo.astrid.data.Task
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.tasks.data.dao.TaskListMetadataDao import org.tasks.data.TaskListMetadataDao
import org.tasks.data.entity.Task
import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
abstract class SubtasksTestCase : InjectingTestCase() { abstract class SubtasksTestCase : InjectingTestCase() {
lateinit var updater: SubtasksFilterUpdater lateinit var updater: SubtasksFilterUpdater
lateinit var filter: AstridOrderingFilter lateinit var filter: Filter
@Inject lateinit var taskListMetadataDao: TaskListMetadataDao @Inject lateinit var taskListMetadataDao: TaskListMetadataDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
filter = runBlocking { MyTasksFilter.create() } filter = BuiltInFilterExposer.getMyTasksFilter(InstrumentationRegistry.getTargetContext().resources)
preferences.clear(SubtasksFilterUpdater.ACTIVE_TASKS_ORDER) preferences.clear(SubtasksFilterUpdater.ACTIVE_TASKS_ORDER)
updater = SubtasksFilterUpdater(taskListMetadataDao, taskDao) updater = SubtasksFilterUpdater(taskListMetadataDao, taskDao)
} }
fun expectParentAndPosition(task: Task, parent: Task?, positionInParent: Int) { fun expectParentAndPosition(task: Task, parent: Task?, positionInParent: Int) {
val parentId = parent?.uuid ?: "-1" val parentId = if (parent == null) "-1" else parent.uuid
val n = updater.findNodeForTask(task.uuid) val n = updater.findNodeForTask(task.uuid)
assertNotNull("No node found for task " + task.title, n) assertNotNull("No node found for task " + task.title, n)
assertEquals("Parent mismatch", parentId, n!!.parent!!.uuid) assertEquals("Parent mismatch", parentId, n.parent.uuid)
assertEquals("Position mismatch", positionInParent, n.parent!!.children.indexOf(n)) assertEquals("Position mismatch", positionInParent, n.parent.children.indexOf(n))
} }
companion object { companion object {

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

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

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

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

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

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

@ -1,18 +1,20 @@
package org.tasks.caldav package org.tasks.caldav
import dagger.hilt.android.testing.HiltAndroidTest import android.content.Context
import kotlinx.coroutines.runBlocking import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test import org.junit.Test
import org.tasks.injection.InjectingTestCase import org.junit.runner.RunWith
import javax.inject.Inject import org.tasks.TestUtilities.newPreferences
import java.security.KeyManagementException
@HiltAndroidTest import java.security.NoSuchAlgorithmException
class CaldavClientTest : InjectingTestCase() {
@Inject lateinit var clientProvider: CaldavClientProvider
@RunWith(AndroidJUnit4::class)
class CaldavClientTest {
@Test @Test
fun dontCrashOnSpaceInUrl(): Unit = runBlocking { fun dontCrashOnSpaceInUrl() {
clientProvider.forUrl("https://example.com/remote.php/a space/", "username", "password") val context = ApplicationProvider.getApplicationContext<Context>()
CaldavClient(context, null, newPreferences(context), null)
.forUrl("https://example.com/remote.php/a space/", "username", "password")
} }
} }

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

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

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

Loading…
Cancel
Save