Compare commits

..

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

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

@ -5,7 +5,6 @@ on:
branches: branches:
- main - main
workflow_dispatch: workflow_dispatch:
workflow_call:
permissions: permissions:
contents: read contents: read
@ -20,20 +19,18 @@ jobs:
- name: Decode Keystore - name: Decode Keystore
run: | run: |
echo ${{ secrets.KEY_STORE }} | base64 -di > "${RUNNER_TEMP}"/keystore.jks echo ${{ secrets.KEY_STORE }} | base64 -di > "${RUNNER_TEMP}"/keystore.jks
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
bundler-cache: true bundler-cache: true
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '17'
cache: 'gradle'
- name: Setup Gradle - name: Grant execute permission for gradlew
uses: gradle/actions/setup-gradle@v4 run: chmod +x gradlew
- name: Bundle - name: Bundle
env: env:
KEY_PATH: ${{ runner.temp }}/keystore.jks KEY_PATH: ${{ runner.temp }}/keystore.jks
@ -44,9 +41,7 @@ jobs:
GOOGLE_KEY: ${{ secrets.GOOGLE_KEY }} GOOGLE_KEY: ${{ secrets.GOOGLE_KEY }}
run: bundle exec fastlane bundle run: bundle exec fastlane bundle
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: release name: release
path: | path: app/build/outputs/**
app/build/outputs/**
wear/build/outputs/**

@ -11,25 +11,22 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
bundler-cache: true bundler-cache: true
- name: Set up JDK 17
- name: Set up JDK 21 uses: actions/setup-java@v4
uses: actions/setup-java@v5
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '17'
cache: 'gradle'
- name: Setup Gradle - name: Grant execute permission for gradlew
uses: gradle/actions/setup-gradle@v4 run: chmod +x gradlew
- name: Lint checks - name: Lint checks
run: bundle exec fastlane lint run: bundle exec fastlane lint
- name: Archive lint reports - name: Archive lint reports
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
if: ${{ always() }} if: ${{ always() }}
with: with:
name: lint-reports name: lint-reports
@ -43,16 +40,14 @@ jobs:
api-level: [29] api-level: [29]
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v6 uses: actions/checkout@v4
- name: Set up JDK 21 - name: Set up JDK 17
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '21' java-version: '17'
cache: 'gradle'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Enable KVM - name: Enable KVM
run: | run: |
@ -89,7 +84,7 @@ jobs:
script: ./gradlew -Pcoverage app:test${{ matrix.flavor }}DebugUnitTest app:connected${{ matrix.flavor }}DebugAndroidTest script: ./gradlew -Pcoverage app:test${{ matrix.flavor }}DebugUnitTest app:connected${{ matrix.flavor }}DebugAndroidTest
- name: Upload test reports - name: Upload test reports
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
if: ${{ always() }} if: ${{ always() }}
with: with:
name: test-reports-${{ matrix.flavor }} name: test-reports-${{ matrix.flavor }}

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

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

1
.gitignore vendored

@ -11,4 +11,3 @@ Thumbs.db
/captures/ /captures/
/fastlane/report.xml /fastlane/report.xml
/compose-metrics/ /compose-metrics/
.DS_Store

@ -1 +1 @@
3.4.8 3.3.4

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

@ -1,534 +1,3 @@
### 14.8.4 (2025-12-20)
* Fix flashing widgets [#3902](https://github.com/tasks/tasks/issues/3902)
* Fix random reminder scheduling
* Fix random reminders firing immediately on recurring tasks [#3904](https://github.com/tasks/tasks/issues/3904)
* Fix deadlock when adding new task
* Fix crash in settings when backup location unavailable [#3989](https://github.com/tasks/tasks/issues/3989)
* Fix Hebrew and Indonesian support [#3928](https://github.com/tasks/tasks/issues/3928)
* Update translations
* Asturian - Xana
* Bosnian - @hasak
* Finnish - @pHamala
* Indonesian - @erigmac
* Japanese - @array, Norara
* Persian - @theuser17
* Romanian - @ygorigor
### 14.8.3 (2025-09-16)
* Fix crash on Android 10 and below
### 14.8.2 (2025-09-14)
* Fix blank widgets on Android 16 QPR1 [#3847](https://github.com/tasks/tasks/issues/3847)
* Fix all-day calendar events [#1534](https://github.com/tasks/tasks/issues/1534)
* Fix alarm synchronization [#3859](https://github.com/tasks/tasks/issues/3859)
* Fix sync failure when migrating data from EteSync to CalDAV [#3869](https://github.com/tasks/tasks/issues/3869)
* Fix removing values from Microsoft To Do [#3862](https://github.com/tasks/tasks/issues/3862)
* Fix share invites for Nextcloud [#2386](https://github.com/tasks/tasks/issues/2386)
* Fix failure to delete source data when moving to Google Tasks [#3867](https://github.com/tasks/tasks/issues/3867)
* Fix crash when clearing completed while grouping by lists
* Update translations
* Croatian - @milotype
* Dutch - @fvbommel
* German - @MisterTechnik
* Italian - @glemco
* Serbian - @vale-decem
### 14.8.1 (2025-08-24)
* System bar scrim improvements
* Recover from Google Task 'Bad request' errors
* Improve layout on Z Folds
* Crash fixes
* Update translations
* Brazilian Portuguese - odnankenobi
* Catalan - @Crashillo, @ferranpujolcamins
* Danish - ERYpTION
* Esperanto - Don Zouras
* Galician - @Crashillo, @delthia
* Hungarian - @Antmajgra, @gthrepwood
* Italian - @ppasserini
* Korean - Jiho Min
* Polish - @Antmajgra
* Portuguese - @Crashillo
* Russian - Алексей Ежков
* Spanish - @Crashillo
### 14.8 (2025-08-02)
* Synchronize **list** icons for Tasks.org and CalDAV accounts
* Does not apply to Microsoft To Do, Google Tasks, DAVx5, EteSync, or DecSync
CC accounts
* Does not apply to tags or filters
* CalDAV server must support extensible properties, e.g. Nextcloud or sabre/dav
* Target Android 15
* Return to previous view after searching
* Remove shadow from date picker sheet
* Fix updating list names and colors for Tasks.org and CalDAV accounts
* Update translations
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Czech - @Fjuro
* Dutch - @fvbommel
* Estonian - Priit Jõerüüt
* French - @FlorianLeChat
* German - @Colorful Rhino
* Hebrew - Xo
* Italian - @ppasserini
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 14.7.4 (2025-07-12)
* @devn1x: Fix escaping quotes in iCalendar [#3645](https://github.com/tasks/tasks/pull/3645)
* Limit widget to 25 items on Android 16+
* Android 16 nerfed widget performance 😢
* Fix bug when reconfiguring widget
* Fix default widget group sort order
* Update translations
* Catalan - pitroig
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* German - @Kachelkaiser
* Serbian - @vale-decem
* Swedish - Nick Wick
* Tamil - @TamilNeram
### 14.7.3 (2025-06-13)
* Fix dynamic color
* Fix Microsoft To Do sync failure
* Fix crash after deleting last list
* Fix notifications when 'Alarms & reminders' not allowed
* Update translations
* Bulgarian - 109247019824
* Dutch - @fvbommel
* Esperanto - Don Zouras
* French - @FlorianLeChat
* Hebrew - Xo
* Japanese - M_Haruki
* Persian - @theuser17
* Portuguese - @nero-bratti
* Romanian - @ygorigor
* Russian - @yurtpage
* Spanish - @orionn333
* Swedish - @Nicklasfox
* Turkish - @emintufan
### 14.7.2 (2025-05-23)
* Remove Microsoft Authentication Library from F-Droid builds [#3581](https://github.com/tasks/tasks/issues/3581)
* Remove contacts permission added by Microsoft Authentication Library
* Enable video attachments
* Fix wallpaper theme
* Fix handling multiple attachments
* Update translations
* Arabic - abdelbasset jabrane
* Bulgarian - 109247019824
* Catalan - @Crashillo
* Czech - @Fjuro
* Danish - @catsnote
* Dutch - @fvbommel
* Esperanto - Don Zouras
* Estonian - Priit Jõerüüt
* Hungarian - Kaci
* Italian - @ppasserini
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 14.7.1 (2025-05-04)
* Fix app closing itself automatically [#3366](https://github.com/tasks/tasks/issues/3366)
* Automatically set default list when connecting Microsoft To Do account
* Update translations
* Arabic - abdelbasset jabrane, @kemo-1
* Brazilian Portuguese - Jose Delvani
* Chinese (Simplified) - Sketch6580
* French - @FlorianLeChat
* German - @Kachelkaiser
### 14.7 (2025-05-03)
* Add support for Microsoft To Do work & school accounts [#3267](https://github.com/tasks/tasks/issues/3267)
* Add ability to rename or delete local account
* Prompt to sign in or import backup on first launch
* @BeaterGhalio: Fix back button closing app after search [#3426](https://github.com/tasks/tasks/issues/3426)
* @codokie: Automirrored icons fix [#3499](https://github.com/tasks/tasks/pull/3499)
* @codokie: Fix ltr-rtl alignment for text input [#3489](https://github.com/tasks/tasks/pull/3489)
* Use system language picker on Android 33+
* Don't show 'due date' as a start date option for DAVx5, EteSync, DecSync CC [#1558](https://github.com/tasks/tasks/issues/1558)
* Prevent attempts to delete or rename Microsoft To Do default list
* Don't handle system 'Clear storage' button
* Update minimum Android version to 8
* Fix backup import dropping tags [#3556](https://github.com/tasks/tasks/issues/3556)
* Fix start date chip when grouping by start date [#3509](https://github.com/tasks/tasks/issues/3509)
* Update translations
* Brazilian Portuguese - @sobeitnow0, dedakir923
* Czech - @Fjuro
* Dutch - Jay Tromp
* German - min7-i
* Hebrew - Xo
* Portuguese - @wm-pucrs
* Russian - @hady-exc, Maksim_220 Кабанов
* Slovak - @jose1711
* Spanish - Nucl3arSnake, @diamondtipdr
* Tamil - @TamilNeram
### 14.6.2 (2025-04-06)
* Show error indicators if 'When started' or 'When due' reminders are used
without start or due times
* Fix delay when saving tasks
* Fix populating clock picker with initial value instead of 00:00
* Fix displaying selected calendar month
* Fix grouping by start date in descending order
* Update translations
* Arabic - abdelbasset jabrane
* Danish - @catsnote
* Esperanto - Don Zouras
* German - @Kachelkaiser
* Hebrew - @elid34
* Italian - @Fs00
* Slovak - @jose1711
* Turkish - @emintufan
### 14.6.1 (2025-03-30)
* Restore default sort mode for existing installs
* Fix grouping by due date descending
* Remove shadow from launcher icons
### 14.6 (2025-03-25)
* Add dynamic theme color - requires pro subscription
* Update translation
* Brazilian Portuguese - dedakir923
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Estonian - Priit Jõerüüt
* Italian - @ppasserini
* Japanese - YuzuMikan
* Swedish - @Ziron
* Ukrainian - @IhorHordiichuk
### 14.5.4 (2025-03-24)
* Updated remaining date and time pickers to Material 3
* App will remember if you change calendar or clock to text input
* Text input now supported on start and due date pickers
* Remove calendar and clock mode settings
* Open date picker to currently selected month
* Replaced upgrade pop-up with a banner [#1429](https://github.com/tasks/tasks/issues/1429)
* @hady-exc: Fix date picker time zone issues [#3248](https://github.com/tasks/tasks/pull/3248)
* Fix date time picker font scaling issues [#3437](https://github.com/tasks/tasks/issues/3437)
* Fix save task on keyboard done [#3288](https://github.com/tasks/tasks/issues/3288)
* Fix applying date time when dismissing date time pickers
* Fix 3 button navigation bar padding in landscape mode
* Fix out of memory errors in backup import/export
* Update translations
* Brazilian Portuguese - dedakir923
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Dutch - @fvbommel
* Estonian - Priit Jõerüüt
* French - @FlorianLeChat
* German - @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Romanian - @ygorigor
* Tamil - @TamilNeram
* Turkish - @emintufan
### 14.5.3 (2025-03-20)
* Updated date and time pickers to Material 3
* Remove 'Start of week' preference
* This feature can't be supported with Material 3 calendars
### 14.5.2 (2025-03-15)
* Fix items hidden under menu search bar [#3406](https://github.com/tasks/tasks/issues/3406)
* Attempt to fix layout on some foldables
* Fix checking for tasks.org account [#3397](https://github.com/tasks/tasks/issues/3397)
* Slightly reduce donation nagging frequency [#3397](https://github.com/tasks/tasks/issues/3397)
* Update translations
* Danish - Øjvind Fritjof Arnfred
* Hungarian - Kaci
* Malayalam - Clouds Liberty
* Russian - @GREAT-DNG
* Swedish - @bittin
* Tamil - @TamilNeram
### 14.5.1 (2025-03-11)
* Fix performance issue when opening search
* Fix Microsoft To Do authentication crash
* Fix crash on task list screen
* Update translation
* Brazilian Portuguese - dedakir923
* Bulgarian - 109247019824
* Chinese (Simplified) - 大王叫我来巡山
* Dutch - @fvbommel
* Esperanto - Don Zouras
* Estonian - Priit Jõerüüt
* French - @FlorianLeChat
* German - Colorful Rhino
* Italian - @ppasserini
* Kannada - Abilash S
* Persian - @mamad-zahiri
* Ukrainian - @IhorHordiichuk
### 14.5 (2025-03-04)
* Material 3 - work in progress
* Side navigation drawer
* Improve support for foldables
* Improve edge-to-edge support
* Remove options for top app bar and disabling collapsing app bar
* Some features are being removed in order to make development easier for the
upcoming desktop app. The features may return again in a future release.
* Save backup files and attachments to Nextcloud [#1289](https://github.com/tasks/tasks/issues/1289)
* Dismiss notification dialog when pressing cancel [#2116](https://github.com/tasks/tasks/issues/2116)
* Performance improvements
* Fix Microsoft To Do sync failure
* Fix missing list chips for subtasks in custom filters
* Fix for database timeouts
* Fix infinite subtask recursion
* Update translations
* Belarusian - @fobo66
* Estonian - Priit Jõerüüt
* German - Colorful Rhino
* Japanese - M_Haruki
* Nahuatl - Benjamin Bruce
* Slovak - @jose1711
* Ukrainian - @IhorHordiichuk
### 14.4.8 (2025-02-04)
* Performance improvements
* Update translations
* German - Colorful Rhino, @Kachelkaiser
* Nepali - Sagun Khatri
### 14.4.7 (2025-02-01)
* Database improvements
* Update translations
* Estonian - Priit Jõerüüt
* German - @Kachelkaiser
### 14.4.6 (2025-01-29)
* Database performance improvements
* Additional debug logging
* Update translations
* Danish - ERYpTION
* Estonian - Priit Jõerüüt
* German - @franconian, Colorful Rhino, @Kachelkaiser
* Italian - @ppasserini
* Korean - Sunjae Choi
* Nepali - Sagun Khatri
* Slovak - @jose1711
* Swedish - Nick Wick
### 14.4.5 (2025-01-22)
* Performance improvements
* DAVx5 sync performance improvements
* Update translations
* Bosnian - @hasak
* Esperanto - Don Zouras
* Estonian - Priit Jõerüüt, @dermezl
* Italian - @ppasserini
* Nepali - @sagunkhatri
### 14.4.4 (2025-01-19)
* Fix list pickers [#3269](https://github.com/tasks/tasks/issues/3269)
### 14.4.3 (2025-01-18)
* Preserve reminder recurrence when copying tasks
* Refresh task list after changing settings
* Fix missing chips for local lists
* Fix changes being lost when completing task from edit screen
* Update translations
* German - @franconian
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 14.4.2 (2025-01-16)
* Fix crash on missing account
* Update translations
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Croatian - @milotype
* Dutch - @fvbommel
* Esperanto - Don Zouras
* French - @FlorianLeChat, @CennoxX
* German - @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Russian - @hady-exc
* Slovak - @jose1711
* Ukrainian - @IhorHordiichuk
### 14.4.1 (2025-01-11)
* Microsoft To Do support [#2011](https://github.com/tasks/tasks/issues/2011)
* This feature is in early access, please report any bugs!
* Enable under 'Advanced' settings
* Add configuration option for new lines in titles
* @TonSilver - Copy comments to clipboard with long press [#3212](https://github.com/tasks/tasks/pull/3212)
* @jheld - Attempt to fix F-Droid build with colorpicker fork [#2028](https://github.com/tasks/tasks/issues/2028)
* Subscription changes
* Multiple Google Task accounts are now free to use
* Tasker plugins are now free to use
* Fix crash on empty shortcut labels
* Fix missing settings button on Android 10 and below
* Update translations
* Bulgarian - 109247019824
* Chinese (Simplified) - 大王叫我来巡山, Sketch6580
* Czech - @AtmosphericIgnition
* Dutch - @fvbommel
* Esperanto - Don Zouras
* French - @FlorianLeChat, @lfavole
* German - @franconian, Colorful Rhino
* Hungarian - Kaci
* Italian - @ppasserini
* Slovak - @jose1711
* Swedish - @Ziron, @bittin
* Turkish - @emintufan
### 14.3.1 (2025-01-02)
* Fix edit screen disappearing on rotation
* Fix notification bundling issue
* Fix scrolling in custom filter settings
* Remove map theme and desaturation options
* Update translations
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - 大王叫我来巡山
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @p-rogalski
* Italian - @ppasserini
* Korean - Sunjae Choi
* Swedish - @bittin
### 14.3 (2024-12-24)
* "Add widget to home screen" shortcut in list settings
* "Add shortcut to home screen" shortcut in list settings
* Shortcuts use list icon and color
* Fix long running sync indicators [#3045](https://github.com/tasks/tasks/issues/3045)
* @hady-exc: Migrate list setting screens to Compose [#3163](https://github.com/tasks/tasks/pull/3163)
* Update translations
* Bosnian - @hasak
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Esperanto - Don Zouras
* Finnish - @pHamala, @Ricky-Tigg
* German - @p-rogalski, @franconian, @Atalanttore
* Hungarian - Kaci
* Italian - @ppasserini
* Korean - Sunjae Choi
* Spanish - gallegonovato
* Swedish - Nick Wick
### 14.2.1 (2024-12-03)
* Fix save button when 'Back button saves task' is enabled [#3149](https://github.com/tasks/tasks/issues/3149)
* Fix customizing edit screen order screen
### 14.2 (2024-12-02)
* Updated edit screen task title
* Show full title
* Removed collapse on scroll
* Removed floating action button
* Add separate alarms and reminders warning
* Capitalize tag picker text field
* Update translations
* Bulgarian - @StoyanDimitrov
* Catalan - raulmagdalena
* Chinese (Simplified) - 大王叫我来巡山
* Dutch - @fvbommel
* French - @FlorianLeChat
* Italian - @ppasserini
* Spanish - gallegonovato
* Ukrainian - @nathalier
### 14.1.1 (2024-11-26)
* Show warning when quiet hours are in effect
* Fix escape character in some localizations [#3046](https://github.com/tasks/tasks/issues/3046)
* Fix comment delete button color [#3102](https://github.com/tasks/tasks/issues/3102)
* Update translations
* Bosnian - @hasak
* Bulgarian - @StoyanDimitrov
* Catalan - raulmagdalena
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Dutch - @fvbommel
* Esperanto - Don Zouras
* French - @FlorianLeChat
* Hungarian - Kaci
* Italian - @ppasserini
* Polish - @rom4nik
* Spanish - gallegonovato
* Swedish - Nick Wick
### 14.1 (2024-11-20)
* Add 'Help & Feedback > Send application logs'
* Delete snoozed reminders when completing tasks
* Fix duplicated tasks when using 'Share' [#2404](https://github.com/tasks/tasks/issues/2404)
* Don't show sync indicator on startup when sync is not used
* Update translations
* Bosnian - @hasak
* Brazilian Portuguese - kowih83264
* Croatian - @milotype
* German - min7-i
### 14.0.1 (2024-11-10)
* Fix widget crash
* Fix EteSync sync failure [#3092](https://github.com/tasks/tasks/issues/3092)
* Minor Wear OS improvements
* Update translations
* Hungarian - Kaci
* Italian - @ppasserini
* Kannada - @historicattle
* Marathi - @historicattle
* Spanish - gallegonovato
* Swedish - @bittin
### 14.0 (2024-11-05)
* Wear OS support (Google Play only)
* Move drawer items to top unless searching
* Fix drawer item layout issues
* Update translations
* Brazilian Portuguese - Nicolas Suzuki, pogoyar888
* Bulgarian - @StoyanDimitrov
* Chinese (Simplified) - 大王叫我来巡山
* Chinese (Traditional) - hugoalh
* Dutch - Luna, @fvbommel
* French - @FlorianLeChat
* German - @p-rogalski, @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Spanish - gallegonovato
* Swedish - @bittin
* Turkish - @oersen
* Ukrainian - @IhorHordiichuk
### 13.11.2 (2024-09-29) ### 13.11.2 (2024-09-29)
* Target Android 14 * Target Android 14

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

@ -5,46 +5,41 @@ GEM
base64 base64
nkf nkf
rexml rexml
abbrev (0.1.2)
addressable (2.8.7) addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17) artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.4.0) aws-eventstream (1.3.0)
aws-partitions (1.1196.0) aws-partitions (1.958.0)
aws-sdk-core (3.240.0) aws-sdk-core (3.201.3)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.8)
base64
bigdecimal
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
logger aws-sdk-kms (1.88.0)
aws-sdk-kms (1.118.0) aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-core (~> 3, >= 3.239.1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.208.0) aws-sdk-s3 (1.156.0)
aws-sdk-core (~> 3, >= 3.234.0) aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1) aws-sigv4 (1.9.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
base64 (0.3.0) base64 (0.2.0)
bigdecimal (4.0.1)
claide (1.1.0) claide (1.1.0)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
commander (4.6.0) commander (4.6.0)
highline (~> 2.0.0) highline (~> 2.0.0)
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.7.0) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.6.20240107) domain_name (0.6.20240107)
dotenv (2.8.1) dotenv (2.8.1)
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.112.0) excon (0.111.0)
faraday (1.10.4) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)
@ -60,20 +55,20 @@ GEM
faraday (>= 0.8.0) faraday (>= 0.8.0)
http-cookie (~> 1.0.0) http-cookie (~> 1.0.0)
faraday-em_http (1.0.0) faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.1) faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0) faraday-excon (1.1.0)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.1.1) faraday-multipart (1.0.4)
multipart-post (~> 2.0) multipart-post (~> 2)
faraday-net_http (1.0.2) faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
faraday-rack (1.0.0) faraday-rack (1.0.0)
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.1) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.4.0) fastimage (2.3.1)
fastlane (2.228.0) fastlane (2.222.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
@ -89,7 +84,6 @@ GEM
faraday-cookie_jar (~> 0.0.6) faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0) faraday_middleware (~> 1.0)
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-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1) google-apis-playcustomapp_v1 (~> 0.1)
@ -113,10 +107,8 @@ GEM
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, < 2.0.0)
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-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
@ -134,12 +126,12 @@ GEM
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.31.0) google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.8.0) google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a) 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.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.5.0) google-cloud-errors (1.4.0)
google-cloud-storage (1.47.0) google-cloud-storage (1.47.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
@ -155,39 +147,37 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
highline (2.0.3) highline (2.0.3)
http-cookie (1.0.8) http-cookie (1.0.6)
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.9.0) httpclient (2.8.3)
mutex_m
jmespath (1.6.2) jmespath (1.6.2)
json (2.12.2) json (2.7.2)
jwt (2.10.2) jwt (2.8.2)
base64 base64
logger (1.7.0)
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.1) multipart-post (2.4.1)
mutex_m (0.3.0) nanaimo (0.3.0)
nanaimo (0.4.0) naturally (2.2.1)
naturally (2.3.0)
nkf (0.2.0) nkf (0.2.0)
optparse (0.6.0) optparse (0.5.0)
os (1.1.4) os (1.1.4)
plist (3.7.2) plist (3.7.1)
public_suffix (6.0.2) public_suffix (5.1.1)
rake (13.3.0) rake (13.2.1)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.4.2) rexml (3.3.6)
rouge (3.28.0) strscan
rouge (2.0.7)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.4.1) rubyzip (2.3.2)
security (0.1.5) security (0.1.5)
signet (0.20.0) signet (0.19.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
@ -195,7 +185,7 @@ GEM
simctl (1.6.10) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
sysrandom (1.0.5) strscan (3.1.0)
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
@ -205,17 +195,16 @@ GEM
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) unicode-display_width (2.5.0)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.27.0) xcodeproj (1.19.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.3.0)
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.1) xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7) xcpretty (~> 0.2, >= 0.0.7)
@ -223,8 +212,7 @@ PLATFORMS
ruby ruby
DEPENDENCIES DEPENDENCIES
abbrev
fastlane fastlane
BUNDLED WITH BUNDLED WITH
2.6.9 2.2.32

@ -15,7 +15,7 @@ Please visit [tasks.org](https://tasks.org) for end user documentation and suppo
[![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](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) [![codebeat badge](https://codebeat.co/badges/07924fca-2f18-4eff-99a3-120ec5ac2d5f)](https://codebeat.co/projects/github-com-tasks-tasks-main)
### Contributing ### Contributing
@ -23,6 +23,8 @@ Contributions are always welcome! Whether translations, code changes, bug report
### Communication ### Communication
You can submit questions to [GitHub Discussions](https://github.com/tasks/tasks/discussions). Join the #tasks channel on Libera Chat to chat with the Tasks team and other people. [Link to webchat](https://web.libera.chat/#tasks)
You can also use [GitHub Discussions](https://github.com/tasks/tasks/discussions).
If you have a suggestion or want to report a bug, please see [CONTRIBUTING.md](CONTRIBUTING.md). If you have a suggestion or want to report a bug, please see [CONTRIBUTING.md](CONTRIBUTING.md).

@ -51,8 +51,8 @@ android {
defaultConfig { defaultConfig {
testApplicationId = "org.tasks.test" testApplicationId = "org.tasks.test"
applicationId = "org.tasks" applicationId = "org.tasks"
versionCode = libs.versions.versionCode.get().toInt() versionCode = 131107
versionName = libs.versions.versionName.get() versionName = "13.11.2"
targetSdk = libs.versions.android.targetSdk.get().toInt() targetSdk = libs.versions.android.targetSdk.get().toInt()
minSdk = libs.versions.android.minSdk.get().toInt() minSdk = libs.versions.android.minSdk.get().toInt()
testInstrumentationRunner = "org.tasks.TestRunner" testInstrumentationRunner = "org.tasks.TestRunner"
@ -154,8 +154,6 @@ dependencies {
implementation(projects.data) implementation(projects.data)
implementation(projects.kmp) implementation(projects.kmp)
implementation(projects.icons) implementation(projects.icons)
implementation(libs.androidx.navigation)
implementation(libs.androidx.adaptive.navigation.android)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bitfire.dav4jvm) { implementation(libs.bitfire.dav4jvm) {
exclude(group = "junit") exclude(group = "junit")
@ -178,17 +176,14 @@ dependencies {
implementation(libs.dagger.hilt) implementation(libs.dagger.hilt)
ksp(libs.dagger.hilt.compiler) ksp(libs.dagger.hilt.compiler)
ksp(libs.androidx.hilt.compiler) ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.navigation)
implementation(libs.androidx.hilt.work) implementation(libs.androidx.hilt.work)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.datastore) implementation(libs.androidx.datastore)
implementation(libs.androidx.fragment.compose) implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.room) implementation(libs.androidx.room)
implementation(libs.androidx.sqlite)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.iconics) implementation(libs.iconics)
implementation(libs.markwon) implementation(libs.markwon)
@ -198,6 +193,9 @@ dependencies {
implementation(libs.markwon.tables) implementation(libs.markwon.tables)
implementation(libs.markwon.tasklist) implementation(libs.markwon.tasklist)
debugImplementation(libs.facebook.flipper)
debugImplementation(libs.facebook.flipper.network)
debugImplementation(libs.facebook.soloader)
debugImplementation(libs.leakcanary) debugImplementation(libs.leakcanary)
debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation(libs.kotlin.reflect) debugImplementation(libs.kotlin.reflect)
@ -209,7 +207,6 @@ dependencies {
implementation(libs.persistent.cookiejar) implementation(libs.persistent.cookiejar)
implementation(libs.material) implementation(libs.material)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.swiperefreshlayout) implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.preference) implementation(libs.androidx.preference)
@ -230,6 +227,8 @@ dependencies {
implementation(libs.colorpicker) implementation(libs.colorpicker)
implementation(libs.appauth) implementation(libs.appauth)
implementation(libs.osmdroid) implementation(libs.osmdroid)
implementation(libs.retrofit)
implementation(libs.retrofit.moshi)
implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerview)
implementation(platform(libs.androidx.compose)) implementation(platform(libs.androidx.compose))
@ -238,40 +237,30 @@ dependencies {
implementation("androidx.compose.material:material") implementation("androidx.compose.material:material")
implementation("androidx.compose.runtime:runtime-livedata") implementation("androidx.compose.runtime:runtime-livedata")
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.material.icons.extended) implementation("androidx.compose.material:material-icons-extended")
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation("androidx.compose.ui:ui-viewbinding")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.coil.video) implementation(libs.coil.video)
implementation(libs.coil.svg) implementation(libs.coil.svg)
implementation(libs.coil.gif) implementation(libs.coil.gif)
implementation(libs.ktor) implementation(libs.accompanist.flowlayout)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.content.negotiation)
implementation(libs.ktor.serialization)
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
implementation(libs.accompanist.systemuicontroller)
googleplayImplementation(platform(libs.firebase)) googleplayImplementation(platform(libs.firebase))
googleplayImplementation(libs.firebase.crashlytics) googleplayImplementation("com.google.firebase:firebase-crashlytics")
googleplayImplementation(libs.firebase.analytics) { googleplayImplementation("com.google.firebase:firebase-analytics") {
exclude("com.google.android.gms", "play-services-ads-identifier") exclude("com.google.android.gms", "play-services-ads-identifier")
} }
googleplayImplementation(libs.firebase.config.ktx) googleplayImplementation("com.google.firebase:firebase-config-ktx")
googleplayImplementation(libs.play.services.location) googleplayImplementation(libs.play.services.location)
googleplayImplementation(libs.play.services.maps) googleplayImplementation(libs.play.services.maps)
googleplayImplementation(libs.play.billing.ktx) googleplayImplementation(libs.play.billing.ktx)
googleplayImplementation(libs.play.review) googleplayImplementation(libs.play.review)
googleplayImplementation(libs.play.services.oss.licenses) googleplayImplementation(libs.play.services.oss.licenses)
googleplayImplementation(libs.horologist.datalayer.phone)
googleplayImplementation(libs.horologist.datalayer.grpc)
googleplayImplementation(libs.horologist.datalayer.core)
googleplayImplementation(libs.play.services.wearable)
googleplayImplementation(libs.microsoft.authentication) {
exclude("com.microsoft.device.display", "display-mask")
}
googleplayImplementation(projects.wearDatalayer)
androidTestImplementation(libs.dagger.hilt.testing) androidTestImplementation(libs.dagger.hilt.testing)
kspAndroidTest(libs.dagger.hilt.compiler) kspAndroidTest(libs.dagger.hilt.compiler)

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

19
app/proguard.pro vendored

@ -2,6 +2,13 @@
-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(...);
}
# guava # guava
-dontwarn sun.misc.Unsafe -dontwarn sun.misc.Unsafe
-dontwarn java.lang.ClassValue -dontwarn java.lang.ClassValue
@ -26,8 +33,6 @@
-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 at.bitfire.** { *; } # all DAVdroid code is required -keep class at.bitfire.** { *; } # all DAVdroid code is required
@ -52,13 +57,3 @@
# material icons # material icons
-keep class androidx.compose.material.icons.outlined.** { *; } -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>; }

@ -349,7 +349,7 @@ class DateUtilitiesTest {
} }
@Test @Test
fun hebrewDateTimeNoYear() = withLocale(Locale.forLanguageTag("he")) { fun hebrewDateTimeNoYear() = withLocale(Locale.forLanguageTag("iw")) {
freezeAt(DateTime(2018, 12, 12)) { freezeAt(DateTime(2018, 12, 12)) {
assertMatches( assertMatches(
"יום ראשון, 14 בינואר( בשעה)? 13:45", "יום ראשון, 14 בינואר( בשעה)? 13:45",
@ -359,7 +359,7 @@ class DateUtilitiesTest {
} }
@Test @Test
fun hebrewDateTimeWithYear() = withLocale(Locale.forLanguageTag("he")) { fun hebrewDateTimeWithYear() = withLocale(Locale.forLanguageTag("iw")) {
freezeAt(DateTime(2017, 12, 12)) { freezeAt(DateTime(2017, 12, 12)) {
assertMatches( assertMatches(
"יום ראשון, 14 בינואר 2018( בשעה)? 13:45", "יום ראשון, 14 בינואר 2018( בשעה)? 13:45",

@ -4,11 +4,11 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.todoroo.astrid.activity.MainActivity.Companion.isFromHistory
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.tasks.extensions.isFromHistory
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MainActivityTest { class MainActivityTest {

@ -2,9 +2,12 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import org.tasks.filters.CaldavFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -12,16 +15,13 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.data.TaskContainer import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount import org.tasks.data.TaskContainer
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV import org.tasks.data.TaskListQuery.getQuery
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.ProductionModule
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,6 +33,7 @@ import org.tasks.preferences.Preferences
import org.tasks.time.DateTime import org.tasks.time.DateTime
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class CaldavManualSortTaskAdapterTest : InjectingTestCase() { class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@ -44,10 +45,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
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(name = "calendar", uuid = "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]
@ -220,7 +218,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
} }
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking { private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking {
tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter))) tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) })
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)
} }

@ -5,6 +5,7 @@ import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Before import org.junit.Before
@ -15,10 +16,12 @@ import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
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
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class CaldavTaskAdapterTest : InjectingTestCase() { class CaldavTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao

@ -2,9 +2,11 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import org.tasks.filters.GtasksFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
@ -15,12 +17,10 @@ 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.CaldavDao
import org.tasks.data.dao.GoogleTaskDao 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.CaldavCalendar
import org.tasks.data.entity.Task 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.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.CaldavTaskMaker.newCaldavTask
@ -29,6 +29,7 @@ import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class GoogleTaskManualSortAdapterTest : InjectingTestCase() { class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@ -40,10 +41,7 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
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(CaldavCalendar(uuid = "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]
@ -423,7 +421,7 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
} }
private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking { private fun move(from: Int, to: Int, indent: Int = 0) = runBlocking {
tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter))) tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) })
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)
} }

@ -3,32 +3,53 @@ package com.todoroo.astrid.adapter
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 dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.tasks.LocalBroadcastManager
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.Task import org.tasks.data.entity.Task
import org.tasks.filters.MyTasksFilter import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
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
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
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
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = runBlocking { MyTasksFilter.create() } private val filter = runBlocking { MyTasksFilter.create() }
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
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, taskMover)
adapter.setDataSource(dataSource)
} }
@Test @Test
@ -36,7 +57,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 +70,20 @@ 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
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 { 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 task.id
} }
private fun query(): List<TaskContainer> = runBlocking { private fun query() = runBlocking {
taskDao.fetchTasks(getQuery(preferences, filter)) tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) })
} }
} }

@ -4,21 +4,25 @@ 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 dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.Ignore
import org.junit.Test import org.junit.Test
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.filters.TodayFilter import org.tasks.filters.TodayFilter
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.DUE_DATE import org.tasks.makers.TaskMaker.DUE_DATE
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
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class RecursiveLoopTest : InjectingTestCase() { class RecursiveLoopTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@ -31,6 +35,7 @@ class RecursiveLoopTest : InjectingTestCase() {
} }
@Test @Test
@Ignore("infinite loop")
fun handleSelfLoop() = runBlocking { fun handleSelfLoop() = runBlocking {
addTask(with(DUE_DATE, newDateTime()), with(PARENT, 1L)) addTask(with(DUE_DATE, newDateTime()), with(PARENT, 1L))
@ -41,6 +46,7 @@ class RecursiveLoopTest : InjectingTestCase() {
} }
@Test @Test
@Ignore("infinite loop")
fun handleSingleLevelLoop() = runBlocking { fun handleSingleLevelLoop() = runBlocking {
val parent = addTask(with(DUE_DATE, newDateTime())) val parent = addTask(with(DUE_DATE, newDateTime()))
val child = addTask(with(PARENT, parent)) val child = addTask(with(PARENT, parent))
@ -54,6 +60,7 @@ class RecursiveLoopTest : InjectingTestCase() {
} }
@Test @Test
@Ignore("infinite loop")
fun handleMultiLevelLoop() = runBlocking { fun handleMultiLevelLoop() = runBlocking {
val parent = addTask(with(DUE_DATE, newDateTime())) val parent = addTask(with(DUE_DATE, newDateTime()))
val child = addTask(with(PARENT, parent)) val child = addTask(with(PARENT, parent))
@ -68,20 +75,9 @@ class RecursiveLoopTest : InjectingTestCase() {
assertEquals(grandchild, tasks[2].id) assertEquals(grandchild, tasks[2].id)
} }
@Test private suspend fun getTasks() = taskDao.fetchTasks {
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()) getQuery(preferences, TodayFilter.create())
) }
private suspend fun addTask(vararg properties: PropertyValue<in Task?, *>): Long { private suspend fun addTask(vararg properties: PropertyValue<in Task?, *>): Long {
val task = newTask(*properties) val task = newTask(*properties)

@ -1,6 +1,7 @@
package com.todoroo.astrid.alarms package com.todoroo.astrid.alarms
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@ -11,11 +12,13 @@ import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Notification import org.tasks.data.entity.Notification
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2 import org.tasks.time.DateTimeUtils2
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class AlarmJobServiceTest : InjectingTestCase() { class AlarmJobServiceTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao

@ -8,13 +8,16 @@ package com.todoroo.astrid.dao
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TaskDaoTests : InjectingTestCase() { class TaskDaoTests : InjectingTestCase() {

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

@ -4,6 +4,7 @@ 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 dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -11,17 +12,21 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
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
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
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 googleTaskListDao: GoogleTaskListDao
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
private lateinit var gtasksListService: GtasksListService private lateinit var gtasksListService: GtasksListService
@ -29,7 +34,7 @@ class GtasksListServiceTest : InjectingTestCase() {
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
gtasksListService = GtasksListService(caldavDao, taskDeleter, localBroadcastManager) gtasksListService = GtasksListService(googleTaskListDao, taskDeleter, localBroadcastManager)
} }
@Test @Test
@ -39,47 +44,48 @@ class GtasksListServiceTest : InjectingTestCase() {
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"), CaldavCalendar(id = 1, account = "account", uuid = "1", name = "Default"),
caldavDao.getCalendarById(1L) googleTaskListDao.getById(1L)
) )
} }
@Test @Test
fun testGetListByRemoteId() = runBlocking { fun testGetListByRemoteId() = runBlocking {
val list = CaldavCalendar(uuid = "1") val list = CaldavCalendar(uuid = "1")
caldavDao.insert(list) list.id = googleTaskListDao.insertOrReplace(list)
assertEquals(list, caldavDao.getCalendarByUuid("1")) assertEquals(list, googleTaskListDao.getByRemoteId("1"))
} }
@Test @Test
fun testGetListReturnsNullWhenNotFound() = runBlocking { fun testGetListReturnsNullWhenNotFound() = runBlocking {
assertNull(caldavDao.getCalendarByUuid("1")) assertNull(googleTaskListDao.getByRemoteId("1"))
} }
@Test @Test
fun testDeleteMissingList() = runBlocking { fun testDeleteMissingList() = runBlocking {
caldavDao.insert(CaldavCalendar(account = "account", uuid = "1")) googleTaskListDao.insertOrReplace(CaldavCalendar(id = 1, account = "account", uuid = "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(CaldavCalendar(id = 2, account = "account", uuid = "2", name = "Default")),
caldavDao.getCalendarsByAccount("account") googleTaskListDao.getLists("account")
) )
} }
@Test @Test
fun testUpdateListName() = runBlocking { fun testUpdateListName() = runBlocking {
val calendar = CaldavCalendar(uuid = "1", name = "oldName", account = "account") googleTaskListDao.insertOrReplace(
caldavDao.insert(calendar) CaldavCalendar(id = 1, uuid = "1", name = "oldName", account = "account")
)
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)!!.name)
} }
@Test @Test
fun testNewListLastSyncIsZero() = runBlocking { fun testNewListLastSyncIsZero() = runBlocking {
setLists(TaskList().setId("1")) setLists(TaskList().setId("1"))
assertEquals(0L, caldavDao.getCalendarByUuid("1")!!.lastSync) assertEquals(0L, googleTaskListDao.getByRemoteId("1")!!.lastSync)
} }
private suspend fun setLists(vararg list: TaskList) { private suspend fun setLists(vararg list: TaskList) {

@ -3,14 +3,17 @@ package com.todoroo.astrid.model
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.tasks.SuspendFreeze.Companion.freezeClock
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TaskTest : InjectingTestCase() { class TaskTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao

@ -3,15 +3,18 @@ package com.todoroo.astrid.repeats
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.TaskDao import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class RepeatWithSubtasksTests : InjectingTestCase() { class RepeatWithSubtasksTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao

@ -8,14 +8,17 @@ package com.todoroo.astrid.service
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.utility.TitleParser import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.tasks.data.dao.TagDataDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class QuickAddMarkupTest : InjectingTestCase() { class QuickAddMarkupTest : InjectingTestCase() {
private val tags = ArrayList<String>() private val tags = ArrayList<String>()

@ -8,6 +8,7 @@ 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.HIDE_UNTIL
import org.tasks.data.entity.Task.Companion.URGENCY_SPECIFIC_DAY import org.tasks.data.entity.Task.Companion.URGENCY_SPECIFIC_DAY
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
@ -15,10 +16,12 @@ import org.tasks.R
import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.createDueDate import org.tasks.data.createDueDate
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTime import org.tasks.time.DateTime
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TaskCreatorTest : InjectingTestCase() { class TaskCreatorTest : InjectingTestCase() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences

@ -2,14 +2,17 @@ package com.todoroo.astrid.service
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.TaskDao import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TaskDeleterTest : InjectingTestCase() { class TaskDeleterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao

@ -1,8 +1,11 @@
package com.todoroo.astrid.service package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.filters.CaldavFilter
import org.tasks.filters.GtasksFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -14,8 +17,9 @@ import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar 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.ProductionModule
import org.tasks.jobs.WorkManager
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
@ -26,10 +30,13 @@ 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
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TaskMoverTest : InjectingTestCase() { class TaskMoverTest : InjectingTestCase() {
@Inject lateinit var taskDaoAsync: TaskDao
@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
@ -56,7 +63,7 @@ class TaskMoverTest : InjectingTestCase() {
createTasks(1) createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1"))) googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "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)
@ -71,7 +78,7 @@ class TaskMoverTest : InjectingTestCase() {
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1"))) googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), 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)
@ -132,9 +139,9 @@ class TaskMoverTest : InjectingTestCase() {
createSubtask(2, 1) createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1"))) googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1"))) googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToCaldavList("2", 1) moveToCaldavList("1", 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(1L, taskDao.fetch(2)?.parent)
} }
@ -181,7 +188,7 @@ class TaskMoverTest : InjectingTestCase() {
with(TASK, 3L), with(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 = taskDao.fetch(3L)
assertEquals(1L, task?.parent) assertEquals(1L, task?.parent)
} }
@ -249,7 +256,7 @@ class TaskMoverTest : InjectingTestCase() {
createTasks(1) createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1"))) googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "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())
} }
@ -299,23 +306,11 @@ class TaskMoverTest : InjectingTestCase() {
} }
private suspend fun moveToGoogleTasks(list: String, vararg tasks: Long) { private suspend fun moveToGoogleTasks(list: String, vararg tasks: Long) {
taskMover.move( taskMover.move(tasks.toList(), GtasksFilter(CaldavCalendar(uuid = list)))
tasks.toList(),
CaldavFilter(
calendar = CaldavCalendar(uuid = list),
account = CaldavAccount(accountType = TYPE_GOOGLE_TASKS)
)
)
} }
private suspend fun moveToCaldavList(calendar: String, vararg tasks: Long) { private suspend fun moveToCaldavList(calendar: String, vararg tasks: Long) {
taskMover.move( taskMover.move(tasks.toList(), CaldavFilter(CaldavCalendar(name = "", uuid = calendar)))
tasks.toList(),
CaldavFilter(
CaldavCalendar(name = "", uuid = calendar),
account = CaldavAccount(accountType = TYPE_CALDAV)
)
)
} }
private suspend fun setAccountType(account: String, type: Int) { private suspend fun setAccountType(account: String, type: Int) {

@ -5,45 +5,37 @@
*/ */
package com.todoroo.astrid.service package com.todoroo.astrid.service
import org.tasks.data.entity.Task
import com.todoroo.astrid.utility.TitleParser import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import net.fortuna.ical4j.model.Recur.Frequency.DAILY import net.fortuna.ical4j.model.Recur.Frequency.*
import net.fortuna.ical4j.model.Recur.Frequency.MONTHLY import org.junit.Assert.*
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.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.tasks.R import org.tasks.R
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.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.ProductionModule
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.repeats.RecurrenceUtils.newRecur
import java.util.Calendar import java.util.*
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
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 @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()
}
} }
/** /**

@ -4,6 +4,7 @@ package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -15,6 +16,7 @@ import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
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.TASK import org.tasks.makers.CaldavTaskMaker.TASK
@ -27,6 +29,7 @@ import org.tasks.opentasks.TestOpenTaskDao
import org.tasks.time.DateTime import org.tasks.time.DateTime
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class Upgrade_11_3_Test : InjectingTestCase() { class Upgrade_11_3_Test : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao

@ -2,12 +2,15 @@ package com.todoroo.astrid.subtasks
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.tasks.data.entity.TaskListMetadata import org.tasks.data.entity.TaskListMetadata
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class SubtasksHelperTest : SubtasksTestCase() { class SubtasksHelperTest : SubtasksTestCase() {
@Before @Before

@ -2,11 +2,14 @@ package com.todoroo.astrid.subtasks
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.data.entity.TaskListMetadata import org.tasks.data.entity.TaskListMetadata
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class SubtasksMovingTest : SubtasksTestCase() { class SubtasksMovingTest : SubtasksTestCase() {
private lateinit var A: Task private lateinit var A: Task

@ -2,10 +2,13 @@ package com.todoroo.astrid.sync
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotEquals
import org.junit.Test import org.junit.Test
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class SyncModelTest : NewSyncTestCase() { class SyncModelTest : NewSyncTestCase() {

@ -1,11 +1,14 @@
package org.tasks.caldav package org.tasks.caldav
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Test import org.junit.Test
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class CaldavClientTest : InjectingTestCase() { class CaldavClientTest : InjectingTestCase() {

@ -3,12 +3,14 @@ package org.tasks.caldav
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.ETAG import org.tasks.makers.CaldavTaskMaker.ETAG
import org.tasks.makers.CaldavTaskMaker.OBJECT import org.tasks.makers.CaldavTaskMaker.OBJECT
@ -16,6 +18,7 @@ import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class CaldavSynchronizerTest : CaldavTest() { class CaldavSynchronizerTest : CaldavTest() {

@ -1,19 +1,22 @@
package org.tasks.caldav package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.UUIDHelper
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_NEXTCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OPEN_XCHANGE import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_SABREDAV import org.tasks.data.entity.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_TASKS import org.tasks.data.entity.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class ServerDetectionTest : CaldavTest() { class ServerDetectionTest : CaldavTest() {
@ -36,7 +39,7 @@ class ServerDetectionTest : CaldavTest() {
sync() sync()
assertEquals(SERVER_NEXTCLOUD, loadAccount().serverType) assertEquals(SERVER_OWNCLOUD, loadAccount().serverType)
} }
@Test @Test

@ -2,6 +2,7 @@ package org.tasks.caldav
import org.tasks.data.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -10,8 +11,10 @@ import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.dao.PrincipalDao import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class SharingMailboxDotOrgTest : CaldavTest() { class SharingMailboxDotOrgTest : CaldavTest() {

@ -2,6 +2,7 @@ package org.tasks.caldav
import org.tasks.data.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
@ -10,8 +11,10 @@ import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.dao.PrincipalDao import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class SharingOwncloudTest : CaldavTest() { class SharingOwncloudTest : CaldavTest() {

@ -2,6 +2,7 @@ package org.tasks.caldav
import org.tasks.data.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -12,8 +13,10 @@ import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.entity.CaldavCalendar.Companion.INVITE_ACCEPTED import org.tasks.data.entity.CaldavCalendar.Companion.INVITE_ACCEPTED
import org.tasks.data.dao.PrincipalDao import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class SharingSabredavTest : CaldavTest() { class SharingSabredavTest : CaldavTest() {

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

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -12,12 +13,14 @@ import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TaskDao import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskContainerMaker import org.tasks.makers.TaskContainerMaker
import org.tasks.makers.TaskContainerMaker.CREATED import org.tasks.makers.TaskContainerMaker.CREATED
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class CaldavDaoShiftTests : InjectingTestCase() { class CaldavDaoShiftTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -14,11 +15,13 @@ import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.CREATION_TIME import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime import org.tasks.time.DateTime
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class CaldavDaoTests : InjectingTestCase() { class CaldavDaoTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao

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

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -10,11 +11,13 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
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
@ -23,8 +26,10 @@ import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class GoogleTaskDaoTests : InjectingTestCase() { class GoogleTaskDaoTests : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@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
@ -180,21 +185,6 @@ class GoogleTaskDaoTests : InjectingTestCase() {
assertEquals("abcd", googleTaskDao.getByTaskId(1)!!.remoteParent) assertEquals("abcd", googleTaskDao.getByTaskId(1)!!.remoteParent)
} }
@Test
fun ignoreSelfParent() = runBlocking {
insert(
newCaldavTask(
with(TASK, 1),
with(REMOTE_ID, "123"),
with(REMOTE_PARENT, "123")
)
)
caldavDao.updateParents()
assertEquals(0, taskDao.fetch(1)!!.parent)
}
@Test @Test
fun updateParents() = runBlocking { fun updateParents() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123"))) insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))

@ -1,16 +1,21 @@
package org.tasks.data package org.tasks.data
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class GoogleTaskListDaoTest : InjectingTestCase() { class GoogleTaskListDaoTest : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Test @Test
@ -21,6 +26,6 @@ class GoogleTaskListDaoTest : InjectingTestCase() {
) )
caldavDao.insert(account) caldavDao.insert(account)
assertTrue(caldavDao.getCaldavFilters(account.username!!).isEmpty()) assertTrue(googleTaskListDao.getGoogleTaskFilters(account.username!!).isEmpty())
} }
} }

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
@ -20,6 +21,7 @@ import org.tasks.data.entity.Place
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.COMPLETION_TIME import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_TIME import org.tasks.makers.TaskMaker.DUE_TIME
@ -29,6 +31,7 @@ import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class LocationDaoTest : InjectingTestCase() { class LocationDaoTest : InjectingTestCase() {
@Inject lateinit var locationDao: LocationDao @Inject lateinit var locationDao: LocationDao

@ -1,8 +1,10 @@
package org.tasks.data package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.filters.GtasksFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
@ -11,10 +13,9 @@ import org.tasks.R
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount 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.CaldavCalendar
import org.tasks.filters.CaldavFilter
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.CaldavTaskMaker.newCaldavTask
@ -25,13 +26,14 @@ import org.tasks.makers.TaskMaker.PARENT
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class ManualGoogleTaskQueryTest : InjectingTestCase() { class ManualGoogleTaskQueryTest : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
private lateinit var filter: CaldavFilter private lateinit var filter: GtasksFilter
@Before @Before
override fun setUp() { override fun setUp() {
@ -43,7 +45,7 @@ class ManualGoogleTaskQueryTest : InjectingTestCase() {
caldavDao.insert(CaldavAccount()) caldavDao.insert(CaldavAccount())
caldavDao.insert(calendar) caldavDao.insert(calendar)
} }
filter = CaldavFilter(calendar, account = CaldavAccount(accountType = TYPE_GOOGLE_TASKS)) filter = GtasksFilter(calendar)
} }
@Test @Test
@ -99,10 +101,10 @@ class ManualGoogleTaskQueryTest : InjectingTestCase() {
with(ORDER, order), with(ORDER, order),
with(PARENT, parent), with(PARENT, parent),
)) ))
googleTaskDao.insert(newCaldavTask(with(CALENDAR, filter.uuid), with(TASK, id))) googleTaskDao.insert(newCaldavTask(with(CALENDAR, filter.list.uuid), with(TASK, id)))
} }
private suspend fun query(): List<TaskContainer> = taskDao.fetchTasks( private suspend fun query(): List<TaskContainer> = taskDao.fetchTasks {
TaskListQuery.getQuery(preferences, filter) TaskListQuery.getQuery(preferences, filter)
) }
} }

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -12,10 +13,12 @@ import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Tag import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData import org.tasks.data.entity.TagData
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.ID import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TagDataDaoTest : InjectingTestCase() { class TagDataDaoTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao

@ -9,17 +9,20 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.Test import org.junit.Test
import org.tasks.data.dao.TaskDao import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
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.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TaskDaoTests : InjectingTestCase() { class TaskDaoTests : InjectingTestCase() {

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy import com.natpryce.makeiteasy.MakeItEasy
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -15,9 +16,11 @@ import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Tag import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData import org.tasks.data.entity.TagData
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker import org.tasks.makers.TaskMaker
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class UpgraderDaoTests : InjectingTestCase() { class UpgraderDaoTests : InjectingTestCase() {

@ -4,9 +4,9 @@ import android.content.Context
import androidx.room.Room import androidx.room.Room
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.tasks.TestUtilities import org.tasks.TestUtilities
import org.tasks.data.db.Database import org.tasks.data.db.Database
@ -19,19 +19,15 @@ import org.tasks.preferences.Preferences
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@TestInstallIn( @InstallIn(SingletonComponent::class)
components = [SingletonComponent::class],
replaces = [ProductionModule::class]
)
class TestModule { class TestModule {
@Provides @Provides
@Singleton @Singleton
fun getDatabase(@ApplicationContext context: Context): Database = fun getDatabase(@ApplicationContext context: Context): Database {
Room return Room.inMemoryDatabaseBuilder(context, Database::class.java)
.inMemoryDatabaseBuilder(context, Database::class.java) .fallbackToDestructiveMigration(dropAllTables = true)
.fallbackToDestructiveMigration(dropAllTables = true) .build()
.setDriver() }
.build()
@Provides @Provides
fun getPermissionChecker(@ApplicationContext context: Context): PermissionChecker { fun getPermissionChecker(@ApplicationContext context: Context): PermissionChecker {

@ -10,6 +10,7 @@ import androidx.test.InstrumentationRegistry
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -21,11 +22,13 @@ import org.tasks.backup.BackupConstants.BACKUP_CLEANUP_MATCHER
import org.tasks.backup.TasksJsonExporter import org.tasks.backup.TasksJsonExporter
import org.tasks.backup.TasksJsonExporter.ExportType import org.tasks.backup.TasksJsonExporter.ExportType
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class BackupServiceTests : InjectingTestCase() { class BackupServiceTests : InjectingTestCase() {
@Inject lateinit var jsonExporter: TasksJsonExporter @Inject lateinit var jsonExporter: TasksJsonExporter

@ -4,14 +4,17 @@ import android.location.Location
import android.location.LocationManager.GPS_PROVIDER import android.location.LocationManager.GPS_PROVIDER
import android.location.LocationManager.NETWORK_PROVIDER import android.location.LocationManager.NETWORK_PROVIDER
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.Test import org.junit.Test
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTime import org.tasks.time.DateTime
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class LocationServiceAndroidTest : InjectingTestCase() { class LocationServiceAndroidTest : InjectingTestCase() {
@Inject lateinit var service: LocationServiceAndroid @Inject lateinit var service: LocationServiceAndroid

@ -3,10 +3,12 @@ package org.tasks.opentasks
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.TestUtilities.withTZ import org.tasks.TestUtilities.withTZ
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker import org.tasks.makers.CaldavTaskMaker
import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker import org.tasks.makers.TaskMaker
@ -14,6 +16,7 @@ import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.* import java.util.*
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class OpenTasksDueDateTests : OpenTasksTest() { class OpenTasksDueDateTests : OpenTasksTest() {

@ -2,6 +2,7 @@ package org.tasks.opentasks
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
@ -22,6 +23,7 @@ import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.Tag import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData import org.tasks.data.entity.TagData
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker 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
@ -34,6 +36,7 @@ import org.tasks.time.DateTime
import java.util.TimeZone import java.util.TimeZone
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class OpenTasksPropertiesTests : OpenTasksTest() { class OpenTasksPropertiesTests : OpenTasksTest() {

@ -2,6 +2,7 @@ package org.tasks.opentasks
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
@ -10,6 +11,7 @@ import org.junit.Test
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.ProductionModule
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.TASK import org.tasks.makers.CaldavTaskMaker.TASK
@ -17,6 +19,7 @@ import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.RECUR import org.tasks.makers.TaskMaker.RECUR
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class OpenTasksSynchronizerTest : OpenTasksTest() { class OpenTasksSynchronizerTest : OpenTasksTest() {

@ -6,6 +6,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
class PermissivePermissionChecker(@ApplicationContext context: Context) : PermissionChecker(context) { class PermissivePermissionChecker(@ApplicationContext context: Context) : PermissionChecker(context) {
override fun canAccessCalendars() = true override fun canAccessCalendars() = true
override fun canAccessAccounts() = true
override fun canAccessForegroundLocation() = true override fun canAccessForegroundLocation() = true
override fun canAccessBackgroundLocation() = true override fun canAccessBackgroundLocation() = true

@ -1,18 +1,21 @@
package org.tasks.repeats package org.tasks.repeats
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.tasks.Freeze import org.tasks.Freeze
import org.tasks.TestUtilities.withTZ import org.tasks.TestUtilities.withTZ
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.text.ParseException import java.text.ParseException
import java.util.Locale import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class RepeatRuleToStringTest : InjectingTestCase() { class RepeatRuleToStringTest : InjectingTestCase() {
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase

@ -3,7 +3,9 @@ package org.tasks.ui.editviewmodel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import com.todoroo.astrid.activity.TaskEditFragment import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.alarms.AlarmService
import org.tasks.data.db.Database
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import com.todoroo.astrid.gcal.GCalHelper import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
@ -12,16 +14,12 @@ import com.todoroo.astrid.timers.TimerPlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.tasks.calendars.CalendarEventProvider import org.tasks.calendars.CalendarEventProvider
import org.tasks.data.dao.AlarmDao import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.LocationDao import org.tasks.data.dao.LocationDao
import org.tasks.data.dao.TagDataDao import org.tasks.data.dao.TagDataDao
import org.tasks.data.dao.UserActivityDao import org.tasks.data.dao.UserActivityDao
import org.tasks.data.db.Database import org.tasks.data.getLocation
import org.tasks.data.entity.Task
import org.tasks.data.newLocalAccount
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.location.GeofenceApi import org.tasks.location.GeofenceApi
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
@ -47,23 +45,18 @@ open class BaseTaskEditViewModelTest : InjectingTestCase() {
@Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao @Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var userActivityDao: UserActivityDao @Inject lateinit var userActivityDao: UserActivityDao
@Inject lateinit var caldavDao: CaldavDao
protected lateinit var viewModel: TaskEditViewModel protected lateinit var viewModel: TaskEditViewModel
@Before
override fun setUp() {
runBlocking {
super.setUp()
caldavDao.newLocalAccount()
}
}
protected fun setup(task: Task) = runBlocking { protected fun setup(task: Task) = runBlocking {
viewModel = TaskEditViewModel( viewModel = TaskEditViewModel(
context, context,
SavedStateHandle().apply { SavedStateHandle().apply {
set(TaskEditFragment.EXTRA_TASK, task) set(TaskEditFragment.EXTRA_TASK, task)
set(TaskEditFragment.EXTRA_LIST, defaultFilterProvider.getList(task))
set(TaskEditFragment.EXTRA_LOCATION, locationDao.getLocation(task, preferences))
set(TaskEditFragment.EXTRA_TAGS, tagDataDao.getTags(task))
set(TaskEditFragment.EXTRA_ALARMS, alarmDao.getAlarms(task))
}, },
taskDao, taskDao,
taskDeleter, taskDeleter,
@ -82,10 +75,10 @@ open class BaseTaskEditViewModelTest : InjectingTestCase() {
taskCompleter, taskCompleter,
alarmService, alarmService,
MutableSharedFlow(), MutableSharedFlow(),
MutableSharedFlow(),
userActivityDao = userActivityDao, userActivityDao = userActivityDao,
taskAttachmentDao = db.taskAttachmentDao(), taskAttachmentDao = db.taskAttachmentDao(),
alarmDao = db.alarmDao(), alarmDao = db.alarmDao(),
defaultFilterProvider = defaultFilterProvider,
) )
} }

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

@ -3,7 +3,7 @@ package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.service.TaskCreator.Companion.setDefaultReminders import com.todoroo.astrid.service.TaskCreator.Companion.setDefaultReminders
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.collections.immutable.persistentSetOf import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
@ -16,12 +16,14 @@ import org.tasks.data.entity.Alarm.Companion.whenDue
import org.tasks.data.entity.Alarm.Companion.whenOverdue import org.tasks.data.entity.Alarm.Companion.whenOverdue
import org.tasks.data.entity.Alarm.Companion.whenStarted import org.tasks.data.entity.Alarm.Companion.whenStarted
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.DUE_TIME import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.START_DATE import org.tasks.makers.TaskMaker.START_DATE
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class ReminderTests : BaseTaskEditViewModelTest() { class ReminderTests : BaseTaskEditViewModelTest() {
@Test @Test
@ -36,8 +38,8 @@ class ReminderTests : BaseTaskEditViewModelTest() {
setup(task) setup(task)
assertEquals( assertEquals(
persistentSetOf(Alarm(type = Alarm.TYPE_REL_START)), listOf(Alarm(type = Alarm.TYPE_REL_START)),
viewModel.viewState.value.alarms viewModel.selectedAlarms.value
) )
} }
@ -53,8 +55,8 @@ class ReminderTests : BaseTaskEditViewModelTest() {
setup(task) setup(task)
assertEquals( assertEquals(
persistentSetOf(Alarm(type = Alarm.TYPE_REL_END)), listOf(Alarm(type = Alarm.TYPE_REL_END)),
viewModel.viewState.value.alarms viewModel.selectedAlarms.value
) )
} }
@ -70,8 +72,8 @@ class ReminderTests : BaseTaskEditViewModelTest() {
setup(task) setup(task)
assertEquals( assertEquals(
persistentSetOf(whenOverdue(0)), listOf(whenOverdue(0)),
viewModel.viewState.value.alarms viewModel.selectedAlarms.value
) )
} }

@ -1,13 +1,16 @@
package org.tasks.ui.editviewmodel package org.tasks.ui.editviewmodel
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.entity.Task import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TaskEditViewModelTest : BaseTaskEditViewModelTest() { class TaskEditViewModelTest : BaseTaskEditViewModelTest() {
@Test @Test
@ -30,7 +33,7 @@ class TaskEditViewModelTest : BaseTaskEditViewModelTest() {
fun dontSaveTaskTwice() = runBlocking { fun dontSaveTaskTwice() = runBlocking {
setup(newTask()) setup(newTask())
viewModel.setPriority(Task.Priority.HIGH) viewModel.priority.value = Task.Priority.HIGH
assertTrue(save()) assertTrue(save())

@ -2,6 +2,7 @@ package org.tasks.ui.editviewmodel
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -10,18 +11,18 @@ import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.DeletionDao import org.tasks.data.dao.DeletionDao
import org.tasks.data.dao.TaskDao import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.filters.MyTasksFilter import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.PermissivePermissionChecker import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.ui.TaskListViewModel import org.tasks.ui.TaskListViewModel
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TaskListViewModelTest : InjectingTestCase() { class TaskListViewModelTest : InjectingTestCase() {
private lateinit var viewModel: TaskListViewModel private lateinit var viewModel: TaskListViewModel
@ -32,13 +33,12 @@ class TaskListViewModelTest : InjectingTestCase() {
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var inventory: Inventory @Inject lateinit var inventory: Inventory
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
@Inject lateinit var caldavDao: CaldavDao
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
viewModel = TaskListViewModel( viewModel = TaskListViewModel(
applicationContext = context, context = context,
preferences = preferences, preferences = preferences,
taskDao = taskDao, taskDao = taskDao,
deletionDao = deletionDao, deletionDao = deletionDao,
@ -46,8 +46,6 @@ class TaskListViewModelTest : InjectingTestCase() {
localBroadcastManager = localBroadcastManager, localBroadcastManager = localBroadcastManager,
inventory = inventory, inventory = inventory,
firebase = firebase, firebase = firebase,
permissionChecker = PermissivePermissionChecker(context),
caldavDao = caldavDao,
) )
viewModel.setFilter(runBlocking { MyTasksFilter.create() }) viewModel.setFilter(runBlocking { MyTasksFilter.create() })
} }

@ -1,22 +1,25 @@
package org.tasks.ui.editviewmodel package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.entity.Task.Priority.Companion.HIGH
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.entity.Task.Priority.Companion.HIGH import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class TitleTests : BaseTaskEditViewModelTest() { class TitleTests : BaseTaskEditViewModelTest() {
@Test @Test
fun changeTitleCausesChange() { fun changeTitleCausesChange() {
setup(newTask()) setup(newTask())
viewModel.setTitle("Test") viewModel.title = "Test"
assertTrue(viewModel.hasChanges()) assertTrue(viewModel.hasChanges())
} }
@ -26,7 +29,7 @@ class TitleTests : BaseTaskEditViewModelTest() {
val task = newTask() val task = newTask()
setup(task) setup(task)
viewModel.setPriority(HIGH) viewModel.priority.value = HIGH
save() save()

@ -1,18 +1,19 @@
package org.tasks.billing package org.tasks.billing
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking import dagger.hilt.android.testing.UninstallModules
import org.json.JSONObject import org.json.JSONObject
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class InventoryTest : InjectingTestCase() { class InventoryTest : InjectingTestCase() {
@ -23,24 +24,6 @@ class InventoryTest : InjectingTestCase() {
lateinit var inventory: Inventory lateinit var inventory: Inventory
@Test
fun hasTasksAccount() = runBlocking {
caldavDao.insert(CaldavAccount(accountType = CaldavAccount.TYPE_TASKS, url = "https://caldav.tasks.org/calendars/"))
initInventory()
inventory.updateTasksAccount()
assertTrue(inventory.hasTasksAccount)
}
@Test
fun hasTasksAccountWithCaldav() = runBlocking {
caldavDao.insert(CaldavAccount(accountType = CaldavAccount.TYPE_CALDAV, url = "https://caldav.tasks.org/calendars/"))
initInventory()
inventory.updateTasksAccount()
assertTrue(inventory.hasTasksAccount)
}
@Test @Test
fun monthlyIsPro() { fun monthlyIsPro() {
withPurchases(monthly01) withPurchases(monthly01)
@ -89,17 +72,13 @@ class InventoryTest : InjectingTestCase() {
} }
.map(::Purchase) .map(::Purchase)
preferences.setPurchases(asPurchases) preferences.setPurchases(asPurchases)
initInventory()
}
private fun initInventory() {
runOnMainSync { runOnMainSync {
inventory = Inventory( inventory = Inventory(
context, context,
preferences, preferences,
signatureVerifier, signatureVerifier,
localBroadcastManager, localBroadcastManager,
caldavDao caldavDao
) )
} }
} }

@ -3,14 +3,17 @@ package org.tasks.caldav
import androidx.test.annotation.UiThreadTest import androidx.test.annotation.UiThreadTest
import org.tasks.data.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.R import org.tasks.R
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class CaldavSubscriptionTest : CaldavTest() { class CaldavSubscriptionTest : CaldavTest() {
@Inject lateinit var inventory: Inventory @Inject lateinit var inventory: Inventory

@ -1,11 +1,14 @@
package org.tasks.opentasks package org.tasks.opentasks
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking 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.R import org.tasks.R
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class OpenTasksSubscriptionTest : OpenTasksTest() { class OpenTasksSubscriptionTest : OpenTasksTest() {
@Test @Test

@ -7,6 +7,10 @@
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
tools:ignore="GoogleAppIndexingWarning"> tools:ignore="GoogleAppIndexingWarning">
<activity
android:exported="true"
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"/>
</application> </application>
</manifest> </manifest>

@ -3,24 +3,40 @@ package org.tasks
import android.app.Application import android.app.Application
import android.os.StrictMode import android.os.StrictMode
import android.os.StrictMode.VmPolicy import android.os.StrictMode.VmPolicy
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.android.utils.FlipperUtils
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
import com.facebook.soloader.SoLoader
import com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo
import com.todoroo.andlib.utility.AndroidUtilities.atLeastQ import com.todoroo.andlib.utility.AndroidUtilities.atLeastQ
import leakcanary.AppWatcher import leakcanary.AppWatcher
import org.tasks.logging.FileLogger
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import timber.log.Timber import timber.log.Timber
import timber.log.Timber.DebugTree
import javax.inject.Inject import javax.inject.Inject
class BuildSetup @Inject constructor( class BuildSetup @Inject constructor(
private val context: Application, private val context: Application,
private val preferences: Preferences, private val preferences: Preferences) {
private val fileLogger: FileLogger,
) {
fun setup() { fun setup() {
Timber.plant(Timber.DebugTree()) Timber.plant(DebugTree())
Timber.plant(fileLogger) SoLoader.init(context, false)
if (preferences.getBoolean(R.string.p_leakcanary, false)) { if (preferences.getBoolean(R.string.p_leakcanary, false)) {
AppWatcher.manualInstall(context) AppWatcher.manualInstall(context)
} }
if (preferences.getBoolean(R.string.p_flipper, false) && FlipperUtils.shouldEnableFlipper(context)) {
val client = AndroidFlipperClient.getInstance(context)
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
client.addPlugin(DatabasesFlipperPlugin(context))
client.addPlugin(NetworkFlipperPlugin())
client.addPlugin(SharedPreferencesFlipperPlugin(context))
client.start()
}
if (preferences.getBoolean(R.string.p_strict_mode_thread, false)) { if (preferences.getBoolean(R.string.p_strict_mode_thread, false)) {
val builder = StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog() val builder = StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog()
if (preferences.getBoolean(R.string.p_crash_main_queries, false)) { if (preferences.getBoolean(R.string.p_crash_main_queries, false)) {
@ -36,7 +52,9 @@ class BuildSetup @Inject constructor(
.detectLeakedClosableObjects() .detectLeakedClosableObjects()
.detectFileUriExposure() .detectFileUriExposure()
.penaltyLog() .penaltyLog()
.detectContentUriWithoutPermission() if (atLeastOreo()) {
builder.detectContentUriWithoutPermission()
}
if (atLeastQ()) { if (atLeastQ()) {
builder builder
.detectCredentialProtectedWhileLocked() .detectCredentialProtectedWhileLocked()
@ -45,4 +63,4 @@ class BuildSetup @Inject constructor(
StrictMode.setVmPolicy(builder.build()) StrictMode.setVmPolicy(builder.build())
} }
} }
} }

@ -0,0 +1,39 @@
package org.tasks
import android.content.Context
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.google.api.client.http.HttpRequest
import com.google.api.client.http.HttpResponse
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.OkHttpClient
import java.io.IOException
import javax.inject.Inject
class DebugNetworkInterceptor @Inject constructor(@param:ApplicationContext private val context: Context) {
fun apply(builder: OkHttpClient.Builder?) {
builder?.addNetworkInterceptor(FlipperOkhttpInterceptor(getNetworkPlugin(context)))
}
@Throws(IOException::class)
fun <T> execute(request: HttpRequest, responseClass: Class<T>): T? {
val interceptor = FlipperHttpInterceptor(getNetworkPlugin(context), responseClass)
request
.setInterceptor(interceptor)
.setResponseInterceptor(interceptor)
.execute()
return interceptor.response
}
@Throws(IOException::class)
fun <T> report(httpResponse: HttpResponse, responseClass: Class<T>, start: Long, finish: Long): T? {
val interceptor = FlipperHttpInterceptor(getNetworkPlugin(context), responseClass)
interceptor.report(httpResponse, start, finish)
return interceptor.response
}
private fun getNetworkPlugin(context: Context): NetworkFlipperPlugin {
return AndroidFlipperClient.getInstance(context).getPlugin(NetworkFlipperPlugin.ID)!!
}
}

@ -0,0 +1,82 @@
package org.tasks
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.flipper.plugins.network.NetworkReporter
import com.facebook.flipper.plugins.network.NetworkReporter.ResponseInfo
import com.google.api.client.http.*
import com.google.api.client.json.GenericJson
import org.tasks.data.UUIDHelper
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.IOException
internal class FlipperHttpInterceptor<T>(private val plugin: NetworkFlipperPlugin, private val responseClass: Class<T>) : HttpExecuteInterceptor, HttpResponseInterceptor {
private val requestId = UUIDHelper.newUUID()
var response: T? = null
private set
override fun intercept(request: HttpRequest) {
plugin.reportRequest(toRequestInfo(request, currentTimeMillis()))
}
@Throws(IOException::class)
override fun interceptResponse(response: HttpResponse) {
plugin.reportResponse(toResponseInfo(response, currentTimeMillis()))
}
@Throws(IOException::class)
fun report(response: HttpResponse, start: Long, end: Long) {
plugin.reportRequest(toRequestInfo(response.request, start))
plugin.reportResponse(toResponseInfo(response, end))
}
private fun toRequestInfo(request: HttpRequest, timestamp: Long): NetworkReporter.RequestInfo {
val requestInfo = NetworkReporter.RequestInfo()
requestInfo.method = request.requestMethod
requestInfo.body = bodyToByteArray(request.content)
requestInfo.headers = getHeaders(request.headers)
requestInfo.requestId = requestId
requestInfo.timeStamp = timestamp
requestInfo.uri = request.url.toString()
return requestInfo
}
@Throws(IOException::class)
private fun toResponseInfo(response: HttpResponse, timestamp: Long): ResponseInfo {
val responseInfo = ResponseInfo()
responseInfo.timeStamp = timestamp
responseInfo.headers = getHeaders(response.headers)
responseInfo.requestId = requestId
responseInfo.statusCode = response.statusCode
responseInfo.statusReason = response.statusMessage
this.response = response.parseAs(responseClass)
if (this.response is GenericJson) {
try {
responseInfo.body = (this.response as GenericJson).toPrettyString().toByteArray()
} catch (e: IOException) {
Timber.e(e)
}
}
return responseInfo
}
private fun getHeaders(headers: HttpHeaders): List<NetworkReporter.Header> {
return headers.map { (name, value) -> NetworkReporter.Header(name, value.toString()) }
}
private fun bodyToByteArray(content: HttpContent?): ByteArray? {
if (content == null) {
return null
}
val output = ByteArrayOutputStream()
try {
content.writeTo(output)
} catch (e: IOException) {
Timber.e(e)
return null
}
return output.toByteArray()
}
}

@ -1,19 +1,15 @@
package org.tasks.preferences.fragments package org.tasks.preferences.fragments
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import at.bitfire.cert4android.CustomCertManager.Companion.resetCertificates import at.bitfire.cert4android.CustomCertManager.Companion.resetCertificates
import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.billing.BillingClient import org.tasks.billing.BillingClient
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.data.createDueDate
import org.tasks.data.entity.Task
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.toast
import org.tasks.injection.InjectingPreferenceFragment import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
@ -28,14 +24,13 @@ class Debug : InjectingPreferenceFragment() {
@Inject lateinit var inventory: Inventory @Inject lateinit var inventory: Inventory
@Inject lateinit var billingClient: BillingClient @Inject lateinit var billingClient: BillingClient
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var taskDao: com.todoroo.astrid.dao.TaskDao
override fun getPreferenceXml() = R.xml.preferences_debug override fun getPreferenceXml() = R.xml.preferences_debug
override suspend fun setupPreferences(savedInstanceState: Bundle?) { override suspend fun setupPreferences(savedInstanceState: Bundle?) {
for (pref in listOf( for (pref in listOf(
R.string.p_leakcanary, R.string.p_leakcanary,
R.string.p_flipper,
R.string.p_strict_mode_vm, R.string.p_strict_mode_vm,
R.string.p_strict_mode_thread, R.string.p_strict_mode_thread,
R.string.p_crash_main_queries R.string.p_crash_main_queries
@ -59,6 +54,7 @@ class Debug : InjectingPreferenceFragment() {
} }
setupIap(R.string.debug_themes, Inventory.SKU_THEMES) setupIap(R.string.debug_themes, Inventory.SKU_THEMES)
setupIap(R.string.debug_tasker, Inventory.SKU_TASKER)
findPreference(R.string.debug_crash_app).setOnPreferenceClickListener { findPreference(R.string.debug_crash_app).setOnPreferenceClickListener {
throw RuntimeException("Crashed app from debug preferences") throw RuntimeException("Crashed app from debug preferences")
@ -70,25 +66,7 @@ class Debug : InjectingPreferenceFragment() {
preferences.lastSubscribeRequest = 0L preferences.lastSubscribeRequest = 0L
preferences.lastReviewRequest = 0L preferences.lastReviewRequest = 0L
preferences.shownBeastModeHint = false preferences.shownBeastModeHint = false
preferences.warnMicrosoft = true
preferences.warnGoogleTasks = true
preferences.warnQuietHoursDisabled = true
preferences.setBoolean(R.string.p_just_updated, true)
true true
}
findPreference(R.string.debug_create_tasks).setOnPreferenceClickListener {
lifecycleScope.launch {
val count = 5000
for (i in 1..count) {
val task = taskCreator.createWithValues("")
taskDao.createNew(task)
task.title = "Task ${task.id}"
task.dueDate = createDueDate(Task.URGENCY_SPECIFIC_DAY, currentTimeMillis())
taskDao.save(task)
}
Toast.makeText(context, "Created $count tasks", Toast.LENGTH_SHORT).show()
}
false
} }
} }

@ -1,20 +0,0 @@
{
"client_id" : "9d4babd5-e7ba-4286-ba4b-17274495a901",
"authorization_user_agent" : "DEFAULT",
"redirect_uri" : "msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D",
"account_mode" : "MULTIPLE",
"authorities" : [
{
"type": "AAD",
"audience": {
"type": "AzureADandPersonalMicrosoftAccount",
"tenant_id": "common"
}
}
],
"logging": {
"level": "verbose",
"logcat_enabled": true,
"pii_enabled": true
}
}

@ -4,16 +4,16 @@
<string name="debug_strict_mode_thread">Strict mode - Thread</string> <string name="debug_strict_mode_thread">Strict mode - Thread</string>
<string name="debug_strict_mode_vm">Strict mode - VM</string> <string name="debug_strict_mode_vm">Strict mode - VM</string>
<string name="debug_leakcanary">LeakCanary</string> <string name="debug_leakcanary">LeakCanary</string>
<string name="debug_flipper">Flipper</string>
<string name="debug_pro">Unlock pro</string> <string name="debug_pro">Unlock pro</string>
<string name="debug_purchase">Purchase %s</string> <string name="debug_purchase">Purchase %s</string>
<string name="debug_consume">Consume %s</string> <string name="debug_consume">Consume %s</string>
<string name="debug_themes">debug_themes</string> <string name="debug_themes">debug_themes</string>
<string name="debug_tasker">debug_tasker</string>
<string name="debug_reset_ssl">Reset SSL certificates</string> <string name="debug_reset_ssl">Reset SSL certificates</string>
<string name="debug_crash_app">Crash app now</string> <string name="debug_crash_app">Crash app now</string>
<string name="debug_create_tasks">Create tasks</string>
<string name="debug_main_queries">Crash on violation</string> <string name="debug_main_queries">Crash on violation</string>
<string name="debug_force_restart">Restart app</string> <string name="debug_force_restart">Restart app</string>
<string name="debug_clear_hints">Clear hints</string> <string name="debug_clear_hints">Clear hints</string>
<string name="google_oauth_scheme">com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf</string> <string name="google_oauth_scheme">com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf</string>
<string name="microsoft_oauth_path">/8wnYBRqh5nnQgFzbIXfxXSs41xE=</string>
</resources> </resources>

@ -6,6 +6,11 @@
android:key="@string/p_leakcanary" android:key="@string/p_leakcanary"
android:title="@string/debug_leakcanary" /> android:title="@string/debug_leakcanary" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/p_flipper"
android:title="@string/debug_flipper"/>
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="@string/p_strict_mode_vm" android:key="@string/p_strict_mode_vm"
@ -42,11 +47,10 @@
android:key="@string/debug_themes"/> android:key="@string/debug_themes"/>
<Preference <Preference
android:key="@string/debug_clear_hints" android:key="@string/debug_tasker"/>
android:title="@string/debug_clear_hints" />
<Preference <Preference
android:key="@string/debug_create_tasks" android:key="@string/debug_clear_hints"
android:title="@string/debug_create_tasks"/> android:title="@string/debug_clear_hints" />
</PreferenceScreen> </PreferenceScreen>

@ -1,24 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools" <manifest>
xmlns:android="http://schemas.android.com/apk/res/android">
<application tools:ignore="MissingApplicationIcon"> <application/>
<activity
android:name=".auth.MicrosoftAuthenticationActivity"
android:theme="@style/TranslucentDialog"/>
<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
android:exported="true"
tools:node="merge">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:host="${applicationId}"
android:scheme="msauth" />
</intent-filter>
</activity>
</application>
</manifest> </manifest>

@ -1,40 +1,19 @@
package org.tasks.analytics package org.tasks.analytics
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
class Firebase @Inject constructor( class Firebase @Inject constructor() {
@param:ApplicationContext val context: Context,
private val preferences: Preferences
) {
fun reportException(t: Throwable) = Timber.e(t) fun reportException(t: Throwable) = Timber.e(t)
fun updateRemoteConfig() {} fun updateRemoteConfig() {}
fun logEvent(event: Int, vararg params: Pair<Int, Any>) { fun logEvent(event: Int, vararg params: Pair<Int, Any>) {}
Timber.d("${context.getString(event)} -> $params")
}
fun addTask(source: String) = fun addTask(source: String) {}
logEvent(R.string.event_add_task, R.string.param_type to source)
fun completeTask(source: String) = val subscribeCooldown = false
logEvent(R.string.event_complete_task, R.string.param_type to source) val moreOptionsBadge = false
val moreOptionsSolid = false
val subscribeCooldown: Boolean }
get() = installCooldown
|| preferences.lastSubscribeRequest + days(28L) > currentTimeMillis()
private val installCooldown: Boolean
get() = preferences.installDate + days(7L) > currentTimeMillis()
private fun days(default: Long): Long =
TimeUnit.DAYS.toMillis(default)
}

@ -1,38 +0,0 @@
package org.tasks.auth
import android.net.Uri
import androidx.core.net.toUri
import net.openid.appauth.AuthorizationServiceConfiguration
import kotlin.coroutines.suspendCoroutine
data class IdentityProvider(
val name: String,
val discoveryEndpoint: Uri,
val clientId: String,
val redirectUri: Uri,
val scope: String
) {
suspend fun retrieveConfig(): AuthorizationServiceConfiguration {
return suspendCoroutine { cont ->
AuthorizationServiceConfiguration.fetchFromUrl(discoveryEndpoint) { serviceConfiguration, ex ->
cont.resumeWith(
when {
ex != null -> Result.failure(ex)
serviceConfiguration != null -> Result.success(serviceConfiguration)
else -> Result.failure(IllegalStateException())
}
)
}
}
}
companion object {
val MICROSOFT = IdentityProvider(
"Microsoft",
"https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration".toUri(),
"9d4babd5-e7ba-4286-ba4b-17274495a901",
"msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D".toUri(),
"user.read Tasks.ReadWrite openid offline_access email"
)
}
}

@ -21,7 +21,6 @@ class BillingClientImpl(
) {} ) {}
override suspend fun acknowledge(purchase: Purchase) {} override suspend fun acknowledge(purchase: Purchase) {}
override suspend fun getSkus(skus: List<String>): List<Sku> = emptyList()
override suspend fun consume(sku: String) {} override suspend fun consume(sku: String) {}

@ -4,13 +4,7 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.tasks.location.Geocoder import org.tasks.location.*
import org.tasks.location.GeocoderNominatim
import org.tasks.location.LocationService
import org.tasks.location.LocationServiceAndroid
import org.tasks.location.MapFragment
import org.tasks.location.OsmMapFragment
import org.tasks.wear.WearRefresher
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -23,9 +17,4 @@ class FlavorModule {
@Provides @Provides
fun getGeocoder(nominatim: GeocoderNominatim): Geocoder = nominatim fun getGeocoder(nominatim: GeocoderNominatim): Geocoder = nominatim
@Provides
fun getWearRefresher(): WearRefresher = object : WearRefresher {
override suspend fun refresh() = Unit
}
} }

@ -1,29 +0,0 @@
package org.tasks.sync.microsoft
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import net.openid.appauth.AuthState
import org.tasks.data.entity.CaldavAccount
import org.tasks.security.KeyStoreEncryption
import javax.inject.Inject
class MicrosoftTokenProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val encryption: KeyStoreEncryption,
) {
suspend fun getToken(account: CaldavAccount): String {
val authState = encryption.decrypt(account.password)?.let { AuthState.jsonDeserialize(it) }
?: throw RuntimeException("Missing credentials")
if (authState.needsTokenRefresh) {
val (token, ex) = context.requestTokenRefresh(authState)
authState.update(token, ex)
if (authState.isAuthorized) {
account.password = encryption.encrypt(authState.jsonSerializeString())
}
}
if (!authState.isAuthorized) {
throw RuntimeException("Needs authentication")
}
return authState.accessToken!!
}
}

@ -40,33 +40,6 @@
android:name=".location.GoogleGeofenceTransitionIntentService" android:name=".location.GoogleGeofenceTransitionIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"/> android:permission="android.permission.BIND_JOB_SERVICE"/>
<service
android:name=".wear.WearDataService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="com.google.android.gms.wearable.REQUEST_RECEIVED" />
<data
android:host="*"
android:pathPrefix="/grpc/"
android:scheme="wear" />
</intent-filter>
</service>
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${applicationId}"
android:path="@string/microsoft_oauth_path"
android:scheme="msauth" />
</intent-filter>
</activity>
</application> </application>
</manifest> </manifest>

@ -19,42 +19,13 @@ import javax.inject.Singleton
@Singleton @Singleton
class Firebase @Inject constructor( class Firebase @Inject constructor(
@ApplicationContext private val context: Context, @param:ApplicationContext val context: Context,
private val preferences: Preferences private val preferences: Preferences
) { ) {
private val crashlytics by lazy {
if (preferences.isTrackingEnabled) { private var crashlytics: FirebaseCrashlytics? = null
FirebaseCrashlytics.getInstance().apply { private var analytics: FirebaseAnalytics? = null
setCrashlyticsCollectionEnabled(true) private var remoteConfig: FirebaseRemoteConfig? = null
}
} else {
null
}
}
private val analytics by lazy {
if (preferences.isTrackingEnabled) {
FirebaseAnalytics.getInstance(context).apply {
setAnalyticsCollectionEnabled(true)
}
} else {
null
}
}
private val remoteConfig by lazy {
if (preferences.isTrackingEnabled) {
FirebaseRemoteConfig.getInstance().apply {
setConfigSettingsAsync(remoteConfigSettings {
minimumFetchIntervalInSeconds =
TimeUnit.HOURS.toSeconds(WorkManager.REMOTE_CONFIG_INTERVAL_HOURS)
})
setDefaultsAsync(R.xml.remote_config_defaults)
}
} else {
null
}
}
fun reportException(t: Throwable) { fun reportException(t: Throwable) {
Timber.e(t) Timber.e(t)
@ -79,13 +50,8 @@ class Firebase @Inject constructor(
fun addTask(source: String) = fun addTask(source: String) =
logEvent(R.string.event_add_task, R.string.param_type to source) logEvent(R.string.event_add_task, R.string.param_type to source)
fun completeTask(source: String) =
logEvent(R.string.event_complete_task, R.string.param_type to source)
fun logEvent(@StringRes event: Int, vararg p: Pair<Int, Any>) { fun logEvent(@StringRes event: Int, vararg p: Pair<Int, Any>) {
val eventName = context.getString(event) analytics?.logEvent(context.getString(event), Bundle().apply {
Timber.d("$eventName -> $p")
analytics?.logEvent(eventName, Bundle().apply {
p.forEach { p.forEach {
val key = context.getString(it.first) val key = context.getString(it.first)
when (it.second::class) { when (it.second::class) {
@ -107,6 +73,30 @@ class Firebase @Inject constructor(
get() = installCooldown get() = installCooldown
|| preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > currentTimeMillis() || preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > currentTimeMillis()
val moreOptionsBadge: Boolean
get() = remoteConfig?.getBoolean("more_options_badge") ?: false
val moreOptionsSolid: Boolean
get() = remoteConfig?.getBoolean("more_options_solid") ?: false
private fun days(key: String, default: Long): Long = private fun days(key: String, default: Long): Long =
TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default) TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default)
init {
if (preferences.isTrackingEnabled) {
analytics = FirebaseAnalytics.getInstance(context).apply {
setAnalyticsCollectionEnabled(true)
}
crashlytics = FirebaseCrashlytics.getInstance().apply {
setCrashlyticsCollectionEnabled(true)
}
remoteConfig = FirebaseRemoteConfig.getInstance().apply {
setConfigSettingsAsync(remoteConfigSettings {
minimumFetchIntervalInSeconds =
TimeUnit.HOURS.toSeconds(WorkManager.REMOTE_CONFIG_INTERVAL_HOURS)
})
setDefaultsAsync(R.xml.remote_config_defaults)
}
}
}
} }

@ -4,19 +4,21 @@ import android.app.Activity
import android.content.Context import android.content.Context
import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingClient.ProductType import com.android.billingclient.api.BillingClient.SkuType
import com.android.billingclient.api.BillingClient.newBuilder import com.android.billingclient.api.BillingClient.newBuilder
import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams import com.android.billingclient.api.BillingFlowParams.ProrationMode
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams
import com.android.billingclient.api.BillingResult import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ConsumeParams import com.android.billingclient.api.ConsumeParams
import com.android.billingclient.api.Purchase.PurchaseState import com.android.billingclient.api.Purchase.PurchaseState
import com.android.billingclient.api.PurchasesResult
import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams import com.android.billingclient.api.SkuDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.consumePurchase import com.android.billingclient.api.consumePurchase
import com.android.billingclient.api.queryPurchasesAsync
import com.android.billingclient.api.querySkuDetails
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
@ -44,63 +46,11 @@ class BillingClientImpl(
private var connected = false private var connected = false
private var onPurchasesUpdated: OnPurchasesUpdated? = null private var onPurchasesUpdated: OnPurchasesUpdated? = null
override suspend fun getSkus(skus: List<String>): List<Sku> =
executeServiceRequest {
val productList = skus.map {
QueryProductDetailsParams.Product.newBuilder()
.setProductId(it)
.setProductType(ProductType.SUBS)
.build()
}
val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
val productDetailsResult = withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
billingClient.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
cont.resume(billingResult to productDetailsList)
}
}
}
productDetailsResult.first.let {
if (!it.success) {
throw IllegalStateException(it.responseCodeString)
}
}
productDetailsResult.second?.map { productDetails ->
Sku(
productId = productDetails.productId,
price = productDetails.subscriptionOfferDetails?.firstOrNull()?.pricingPhases?.pricingPhaseList?.firstOrNull()?.formattedPrice
?: productDetails.oneTimePurchaseOfferDetails?.formattedPrice
?: ""
)
} ?: emptyList()
}
override suspend fun queryPurchases(throwError: Boolean) = try { override suspend fun queryPurchases(throwError: Boolean) = try {
executeServiceRequest { executeServiceRequest {
withContext(Dispatchers.IO + NonCancellable) { withContext(Dispatchers.IO + NonCancellable) {
val subsParams = QueryPurchasesParams.newBuilder() val subs = billingClient.queryPurchasesAsync(SkuType.SUBS)
.setProductType(ProductType.SUBS) val iaps = billingClient.queryPurchasesAsync(SkuType.INAPP)
.build()
val iapsParams = QueryPurchasesParams.newBuilder()
.setProductType(ProductType.INAPP)
.build()
val subs = suspendCoroutine { cont ->
billingClient.queryPurchasesAsync(subsParams) { billingResult, purchases ->
cont.resume(PurchasesResult(billingResult, purchases))
}
}
val iaps = suspendCoroutine { cont ->
billingClient.queryPurchasesAsync(iapsParams) { billingResult, purchases ->
cont.resume(PurchasesResult(billingResult, purchases))
}
}
if (subs.success || iaps.success) { if (subs.success || iaps.success) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
inventory.clear() inventory.clear()
@ -131,7 +81,7 @@ class BillingClientImpl(
purchases?.forEach { purchases?.forEach {
firebase.reportIabResult( firebase.reportIabResult(
result.responseCodeString, result.responseCodeString,
it.products.joinToString(","), it.skus.joinToString(","),
it.purchaseState.purchaseStateString it.purchaseState.purchaseStateString
) )
} }
@ -148,57 +98,31 @@ class BillingClientImpl(
oldPurchase: Purchase? oldPurchase: Purchase?
) { ) {
executeServiceRequest { executeServiceRequest {
val productList = listOf( val skuDetailsResult = withContext(Dispatchers.IO) {
QueryProductDetailsParams.Product.newBuilder() billingClient.querySkuDetails(
.setProductId(sku) SkuDetailsParams.newBuilder().setSkusList(listOf(sku)).setType(skuType)
.setProductType(skuType) .build()
.build() )
)
val queryParams = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
val productDetailsResult = withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
billingClient.queryProductDetailsAsync(queryParams) { billingResult, productDetailsList ->
cont.resume(billingResult to productDetailsList)
}
}
} }
skuDetailsResult.billingResult.let {
productDetailsResult.first.let {
if (!it.success) { if (!it.success) {
throw IllegalStateException(it.responseCodeString) throw IllegalStateException(it.responseCodeString)
} }
} }
val skuDetails =
val productDetails = productDetailsResult.second?.firstOrNull() skuDetailsResult
?: throw IllegalStateException("Product $sku not found") .skuDetailsList
?.firstOrNull()
val productDetailsParamsBuilder = ProductDetailsParams.newBuilder() ?: throw IllegalStateException("Sku $sku not found")
.setProductDetails(productDetails) val params = BillingFlowParams.newBuilder().setSkuDetails(skuDetails)
// For subscriptions (including legacy subscriptions), we need to provide an offer token
if (skuType == ProductType.SUBS) {
val offerToken = productDetails.subscriptionOfferDetails?.firstOrNull()?.offerToken
?: throw IllegalStateException("No offer token found for subscription $sku")
productDetailsParamsBuilder.setOfferToken(offerToken)
}
val productDetailsParams = productDetailsParamsBuilder.build()
val params = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productDetailsParams))
oldPurchase?.let { oldPurchase?.let {
params.setSubscriptionUpdateParams( params.setSubscriptionUpdateParams(
SubscriptionUpdateParams.newBuilder() SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(it.purchaseToken) .setOldSkuPurchaseToken(it.purchaseToken)
.setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION) .setReplaceSkusProrationMode(ProrationMode.IMMEDIATE_WITH_TIME_PRORATION)
.build() .build()
) )
} }
if (activity is OnPurchasesUpdated) { if (activity is OnPurchasesUpdated) {
onPurchasesUpdated = activity onPurchasesUpdated = activity
} }
@ -250,11 +174,11 @@ class BillingClientImpl(
) )
} }
private suspend fun <T> executeServiceRequest(runnable: suspend () -> T): T { private suspend fun executeServiceRequest(runnable: suspend () -> Unit) {
if (!connected) { if (!connected) {
connect() connect()
} }
return runnable() runnable()
} }
override suspend fun consume(sku: String) { override suspend fun consume(sku: String) {
@ -266,28 +190,17 @@ class BillingClientImpl(
ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build(), ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build(),
) )
Timber.d("consume purchase: ${result.billingResult.responseCodeString}") Timber.d("consume purchase: ${result.billingResult.responseCodeString}")
queryPurchases(throwError = false) queryPurchases()
} }
} }
private data class PurchasesResult(
val billingResult: BillingResult,
val purchasesList: List<com.android.billingclient.api.Purchase>
) {
val success: Boolean
get() = billingResult.responseCode == BillingResponseCode.OK
val responseCodeString: String
get() = billingResult.responseCodeString
val purchases: List<com.android.billingclient.api.Purchase>
get() = purchasesList
}
companion object { companion object {
const val TYPE_SUBS = ProductType.SUBS const val TYPE_SUBS = SkuType.SUBS
const val STATE_PURCHASED = PurchaseState.PURCHASED const val STATE_PURCHASED = PurchaseState.PURCHASED
private val PurchasesResult.success: Boolean
get() = billingResult.responseCode == BillingResponseCode.OK
private val BillingResult.success: Boolean private val BillingResult.success: Boolean
get() = responseCode == BillingResponseCode.OK get() = responseCode == BillingResponseCode.OK
@ -314,5 +227,11 @@ class BillingClientImpl(
PurchaseState.PENDING -> "PENDING" PurchaseState.PENDING -> "PENDING"
else -> this.toString() else -> this.toString()
} }
private val PurchasesResult.responseCodeString: String
get() = billingResult.responseCodeString
private val PurchasesResult.purchases: List<com.android.billingclient.api.Purchase>
get() = purchasesList
} }
} }

@ -31,7 +31,7 @@ class Purchase(private val purchase: Purchase) {
get() = purchase.signature get() = purchase.signature
val sku: String val sku: String
get() = purchase.products.first() get() = purchase.skus.first()
val purchaseToken: String val purchaseToken: String
get() = purchase.purchaseToken get() = purchase.purchaseToken
@ -55,7 +55,7 @@ class Purchase(private val purchase: Purchase) {
get() { get() {
val matcher = PATTERN.matcher(sku) val matcher = PATTERN.matcher(sku)
if (matcher.matches()) { if (matcher.matches()) {
val price = matcher.group(2)?.toInt() val price = matcher.group(2).toInt()
return if (price == 499) 5 else price return if (price == 499) 5 else price
} }
return null return null

@ -1,17 +1,10 @@
package org.tasks.injection package org.tasks.injection
import android.content.Context
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.WearDataLayerRegistry
import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper
import dagger.Lazy import dagger.Lazy
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import org.tasks.extensions.wearDataLayerRegistry
import org.tasks.location.Geocoder import org.tasks.location.Geocoder
import org.tasks.location.GeocoderMapbox import org.tasks.location.GeocoderMapbox
import org.tasks.location.GoogleMapFragment import org.tasks.location.GoogleMapFragment
@ -21,9 +14,6 @@ import org.tasks.location.LocationServiceGooglePlay
import org.tasks.location.MapFragment import org.tasks.location.MapFragment
import org.tasks.location.OsmMapFragment import org.tasks.location.OsmMapFragment
import org.tasks.play.PlayServices import org.tasks.play.PlayServices
import org.tasks.wear.WearRefresher
import org.tasks.wear.WearRefresherImpl
import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@ -44,30 +34,4 @@ class FlavorModule {
@Provides @Provides
fun getGeocoder(mapbox: GeocoderMapbox): Geocoder = mapbox fun getGeocoder(mapbox: GeocoderMapbox): Geocoder = mapbox
@OptIn(ExperimentalHorologistApi::class)
@Provides
fun wearDataLayerRegistry(
@ApplicationContext applicationContext: Context,
@ApplicationScope coroutineScope: CoroutineScope,
) = applicationContext.wearDataLayerRegistry(coroutineScope)
@OptIn(ExperimentalHorologistApi::class)
@Provides
fun phoneDataLayerAppHelper(
@ApplicationContext applicationContext: Context,
wearDataLayerRegistry: WearDataLayerRegistry,
) = PhoneDataLayerAppHelper(
context = applicationContext,
registry = wearDataLayerRegistry,
)
@OptIn(ExperimentalHorologistApi::class)
@Provides
@Singleton
fun getWearRefresher(
phoneDataLayerAppHelper: PhoneDataLayerAppHelper,
wearDataLayerRegistry: WearDataLayerRegistry,
@ApplicationScope scope: CoroutineScope,
): WearRefresher = WearRefresherImpl(phoneDataLayerAppHelper, wearDataLayerRegistry, scope)
} }

@ -2,7 +2,6 @@ package org.tasks.location
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.GoogleMap
@ -24,18 +23,14 @@ class GoogleMapFragment @Inject constructor(
private var map: GoogleMap? = null private var map: GoogleMap? = null
private var circle: Circle? = null private var circle: Circle? = null
override fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean, parent: ViewGroup?) { override fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean) {
this.callback = callback this.callback = callback
this.dark = dark this.dark = dark
val fragmentManager = activity.supportFragmentManager val fragmentManager = activity.supportFragmentManager
var mapFragment = fragmentManager.findFragmentByTag(FRAG_TAG_MAP) as SupportMapFragment? var mapFragment = fragmentManager.findFragmentByTag(FRAG_TAG_MAP) as SupportMapFragment?
if (mapFragment == null) { if (mapFragment == null) {
mapFragment = SupportMapFragment() mapFragment = SupportMapFragment()
if (parent == null) { fragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit()
fragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit()
} else {
fragmentManager.beginTransaction().add(parent, mapFragment, null).commit()
}
} }
mapFragment.getMapAsync(this) mapFragment.getMapAsync(this)
} }

@ -1,100 +0,0 @@
package org.tasks.sync.microsoft
import android.app.Activity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.microsoft.identity.client.AcquireTokenParameters
import com.microsoft.identity.client.AuthenticationCallback
import com.microsoft.identity.client.IAuthenticationResult
import com.microsoft.identity.client.Prompt
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.exception.MsalException
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.analytics.Constants
import org.tasks.analytics.Firebase
import org.tasks.data.UUIDHelper
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_MICROSOFT
import org.tasks.extensions.Context.toast
import org.tasks.jobs.WorkManager
import org.tasks.sync.SyncAdapters
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class MicrosoftSignInViewModel @Inject constructor(
private val caldavDao: CaldavDao,
private val firebase: Firebase,
private val syncAdapters: SyncAdapters,
private val workManager: WorkManager,
) : ViewModel() {
fun signIn(activity: Activity) {
viewModelScope.launch(Dispatchers.IO) {
val app = PublicClientApplication.createMultipleAccountPublicClientApplication(
activity,
R.raw.microsoft_config
)
val parameters = AcquireTokenParameters.Builder()
.startAuthorizationFromActivity(activity)
.withScopes(scopes)
.withPrompt(Prompt.SELECT_ACCOUNT)
.withCallback(object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult) {
val email = authenticationResult.account.claims?.get("preferred_username") as? String
if (email == null) {
Timber.e("No email found")
return
}
Timber.d("Successfully signed in")
viewModelScope.launch {
caldavDao
.getAccount(TYPE_MICROSOFT, email)
?.let {
caldavDao.update(
it.copy(error = null)
)
}
?: caldavDao
.insert(
CaldavAccount(
uuid = UUIDHelper.newUUID(),
name = email,
username = email,
accountType = TYPE_MICROSOFT,
)
)
.also {
firebase.logEvent(
R.string.event_sync_add_account,
R.string.param_type to Constants.SYNC_TYPE_MICROSOFT
)
}
syncAdapters.sync(true)
workManager.updateBackgroundSync()
}
}
override fun onError(exception: MsalException?) {
Timber.e(exception)
activity.toast(exception?.message ?: exception?.javaClass?.simpleName ?: "Sign in failed")
}
override fun onCancel() {
Timber.d("onCancel")
}
})
.build()
app.acquireToken(parameters)
}
}
companion object {
val scopes = listOf("https://graph.microsoft.com/.default")
}
}

@ -1,39 +0,0 @@
package org.tasks.sync.microsoft
import android.content.Context
import com.microsoft.identity.client.AcquireTokenSilentParameters
import com.microsoft.identity.client.PublicClientApplication
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R
import org.tasks.data.entity.CaldavAccount
import timber.log.Timber
import javax.inject.Inject
class MicrosoftTokenProvider @Inject constructor(
@ApplicationContext private val context: Context,
) {
fun getToken(account: CaldavAccount): String {
val app = PublicClientApplication.createMultipleAccountPublicClientApplication(
context,
R.raw.microsoft_config
)
val result = try {
val msalAccount = app.accounts.firstOrNull { it.username == account.username }
?: throw RuntimeException("No matching account found")
val parameters = AcquireTokenSilentParameters.Builder()
.withScopes(MicrosoftSignInViewModel.scopes)
.forAccount(msalAccount)
.fromAuthority(msalAccount.authority)
.forceRefresh(true)
.build()
app.acquireTokenSilent(parameters)
} catch (e: Exception) {
Timber.e(e)
throw RuntimeException("Authentication failed: ${e.message}")
}
return result.accessToken
}
}

@ -1,65 +0,0 @@
package org.tasks.wear
import android.text.format.DateFormat
import androidx.datastore.core.DataStore
import androidx.lifecycle.lifecycleScope
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.ProtoDataStoreHelper.protoDataStore
import com.google.android.horologist.data.WearDataLayerRegistry
import com.google.android.horologist.datalayer.grpc.server.BaseGrpcDataService
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.GrpcProto.Settings
import org.tasks.WearServiceGrpcKt
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.extensions.wearDataLayerRegistry
import org.tasks.filters.FilterProvider
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import org.tasks.themes.ColorProvider
import javax.inject.Inject
@OptIn(ExperimentalHorologistApi::class)
@AndroidEntryPoint
class WearDataService : BaseGrpcDataService<WearServiceGrpcKt.WearServiceCoroutineImplBase>() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var headerFormatter: HeaderFormatter
@Inject lateinit var firebase: Firebase
@Inject lateinit var filterProvider: FilterProvider
@Inject lateinit var inventory: Inventory
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var taskCreator: TaskCreator
override val registry: WearDataLayerRegistry by lazy {
applicationContext.wearDataLayerRegistry(lifecycleScope)
}
private val settings: DataStore<Settings> by lazy {
registry.protoDataStore(lifecycleScope)
}
override fun buildService(): WearServiceGrpcKt.WearServiceCoroutineImplBase {
return WearService(
taskDao = taskDao,
appPreferences = preferences,
taskCompleter = taskCompleter,
headerFormatter = headerFormatter,
settings = settings,
firebase = firebase,
filterProvider = filterProvider,
inventory = inventory,
colorProvider = colorProvider,
defaultFilterProvider = defaultFilterProvider,
taskCreator = taskCreator,
is24HourTime = DateFormat.is24HourFormat(applicationContext),
)
}
}

@ -1,16 +0,0 @@
package org.tasks.wear
import org.tasks.GrpcProto.Settings
import org.tasks.preferences.Preferences
import org.tasks.preferences.QueryPreferences
class WearPreferences(
preferences: Preferences,
private val settings: Settings,
): QueryPreferences by preferences {
override val showHidden: Boolean
get() = settings.showHidden
override val showCompleted: Boolean
get() = settings.showCompleted
}

@ -1,50 +0,0 @@
package org.tasks.wear
import androidx.datastore.core.DataStore
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.ProtoDataStoreHelper.protoDataStore
import com.google.android.horologist.data.WearDataLayerRegistry
import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.tasks.GrpcProto.LastUpdate
import org.tasks.copy
import timber.log.Timber
@OptIn(ExperimentalHorologistApi::class)
class WearRefresherImpl(
phoneDataLayerAppHelper: PhoneDataLayerAppHelper,
private val registry: WearDataLayerRegistry,
private val scope: CoroutineScope,
) : WearRefresher {
private var watchConnected = false
init {
phoneDataLayerAppHelper
.connectedAndInstalledNodes
.catch { Timber.e("${it.message}") }
.onEach { nodes ->
Timber.d("Connected nodes: ${nodes.joinToString()}")
watchConnected = nodes.isNotEmpty()
lastUpdate.update()
}
.launchIn(scope)
}
private val lastUpdate: DataStore<LastUpdate> by lazy {
registry.protoDataStore<LastUpdate>(scope)
}
override suspend fun refresh() {
if (watchConnected) {
lastUpdate.update()
}
}
}
private suspend fun DataStore<LastUpdate>.update() {
updateData { it.copy { now = System.currentTimeMillis() } }
}

@ -1,264 +0,0 @@
package org.tasks.wear
import androidx.datastore.core.DataStore
import com.todoroo.astrid.core.SortHelper.SORT_DUE
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator
import kotlinx.coroutines.flow.firstOrNull
import org.tasks.GrpcProto
import org.tasks.GrpcProto.CompleteTaskRequest
import org.tasks.GrpcProto.CompleteTaskResponse
import org.tasks.GrpcProto.GetListsResponse
import org.tasks.GrpcProto.GetTaskResponse
import org.tasks.GrpcProto.GetTasksRequest
import org.tasks.GrpcProto.ListItem
import org.tasks.GrpcProto.ListItemType
import org.tasks.GrpcProto.SaveTaskResponse
import org.tasks.GrpcProto.Tasks
import org.tasks.GrpcProto.ToggleGroupRequest
import org.tasks.GrpcProto.ToggleGroupResponse
import org.tasks.WearServiceGrpcKt
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.copy
import org.tasks.data.NO_COUNT
import org.tasks.data.isHidden
import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.MyTasksFilter
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.getIcon
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import org.tasks.tasklist.SectionedDataSource
import org.tasks.tasklist.UiItem
import org.tasks.themes.ColorProvider
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.startOfDay
import timber.log.Timber
class WearService(
private val taskDao: TaskDao,
private val appPreferences: Preferences,
private val taskCompleter: TaskCompleter,
private val headerFormatter: HeaderFormatter,
private val settings: DataStore<GrpcProto.Settings>,
private val firebase: Firebase,
private val filterProvider: FilterProvider,
private val inventory: Inventory,
private val colorProvider: ColorProvider,
private val defaultFilterProvider: DefaultFilterProvider,
private val taskCreator: TaskCreator,
private val is24HourTime: Boolean,
) : WearServiceGrpcKt.WearServiceCoroutineImplBase() {
override suspend fun getTasks(request: GetTasksRequest): Tasks {
val position = request.position
val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE
val settingsData = settings.data.firstOrNull() ?: GrpcProto.Settings.getDefaultInstance()
val filter =
defaultFilterProvider.getFilterFromPreference(settingsData.filter.takeIf { it.isNotBlank() })
val preferences = WearPreferences(appPreferences, settingsData)
val collapsed = settingsData?.collapsedList?.toSet() ?: emptySet()
val payload = SectionedDataSource(
tasks = taskDao.fetchTasks(preferences, filter),
disableHeaders = filter.disableHeaders()
|| (filter.supportsManualSort() && preferences.isManualSort)
|| (filter is AstridOrderingFilter && preferences.isAstridSort),
groupMode = preferences.groupMode,
subtaskMode = preferences.subtaskMode,
completedAtBottom = preferences.completedTasksAtBottom,
collapsed = collapsed,
)
return Tasks.newBuilder()
.setTotalItems(payload.size)
.addAllItems(
payload
.subList(position, position + limit)
.map { item ->
when (item) {
is UiItem.Header ->
GrpcProto.UiItem.newBuilder()
.setId(item.value)
.setType(ListItemType.Header)
.setTitle(headerFormatter.headerString(item.value, style = DateStyle.MEDIUM))
.setCollapsed(item.collapsed)
.build()
is UiItem.Task -> {
val timestamp = if (preferences.groupMode == SORT_DUE &&
(item.task.sortGroup
?: 0) >= currentTimeMillis().startOfDay()
) {
item.task.takeIf { it.hasDueTime() }?.let {
getTimeString(item.task.dueDate, is24HourTime)
}
} else if (item.task.hasDueDate()) {
getRelativeDateTime(
item.task.dueDate,
is24HourTime,
)
} else {
null
}
GrpcProto.UiItem.newBuilder()
.setType(ListItemType.Item)
.setId(item.task.id)
.setPriority(item.task.priority)
.setCompleted(item.task.isCompleted)
.setHidden(item.task.task.isHidden)
.setIndent(item.task.indent)
.setCollapsed(item.task.isCollapsed)
.setNumSubtasks(item.task.children)
.apply {
if (item.task.title != null) {
setTitle(item.task.title)
}
if (timestamp != null) {
setTimestamp(timestamp)
}
}
.setRepeating(item.task.task.isRecurring)
.build()
}
}
}
)
.build()
}
override suspend fun completeTask(request: CompleteTaskRequest): CompleteTaskResponse {
taskCompleter.setComplete(request.id, request.completed)
firebase.completeTask("wearable")
return CompleteTaskResponse.newBuilder().setSuccess(true).build()
}
override suspend fun toggleGroup(request: ToggleGroupRequest): ToggleGroupResponse {
settings.updateData {
it.copy {
if (request.collapsed) {
if (!collapsed.contains(request.value)) {
collapsed.add(request.value)
}
} else {
if (collapsed.contains(request.value)) {
collapsed.clear()
collapsed.addAll(
it.collapsedList.toMutableList().apply { remove(request.value) })
}
}
}
}
return ToggleGroupResponse.getDefaultInstance()
}
override suspend fun updateSettings(request: GrpcProto.UpdateSettingsRequest): GrpcProto.Settings {
return settings.updateData { request.settings }
}
override suspend fun toggleSubtasks(request: ToggleGroupRequest): ToggleGroupResponse {
taskDao.setCollapsed(request.value, request.collapsed)
return ToggleGroupResponse.newBuilder().build()
}
override suspend fun getLists(request: GrpcProto.GetListsRequest): GetListsResponse {
val position = request.position
val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE
val selected = settings.data.firstOrNull()?.filter?.takeIf { it.isNotBlank() }
?: defaultFilterProvider.getFilterPreferenceValue(MyTasksFilter.create())
val filters = filterProvider.wearableFilters()
return GetListsResponse.newBuilder()
.setTotalItems(filters.size)
.addAllItems(
filters
.subList(position, (position + limit).coerceAtMost(filters.size))
.map { item ->
when (item) {
is Filter -> {
ListItem.newBuilder()
.setId(defaultFilterProvider.getFilterPreferenceValue(item))
.setType(ListItemType.Item)
.setTitle(item.title ?: "")
.setIcon(item.getIcon(inventory))
.setColor(getColor(item))
.setTaskCount(item.count.takeIf { it != NO_COUNT } ?: try {
taskDao.count(item)
} catch (e: Exception) {
Timber.e(e)
0
})
.build()
}
is NavigationDrawerSubheader ->
ListItem.newBuilder()
.setType(ListItemType.Header)
.setTitle(item.title ?: "")
.setId("${item.subheaderType}_${item.id}")
.build()
else -> throw IllegalArgumentException()
}
}
)
.build()
}
override suspend fun getTask(request: GrpcProto.GetTaskRequest): GetTaskResponse {
Timber.d("getTask($request)")
val task = taskDao.fetch(request.taskId)
?: throw IllegalArgumentException()
return GetTaskResponse.newBuilder()
.setTitle(task.title ?: "")
.setCompleted(task.isCompleted)
.setPriority(task.priority)
.setRepeating(task.isRecurring)
.build()
}
override suspend fun saveTask(request: GrpcProto.SaveTaskRequest): SaveTaskResponse {
Timber.d("saveTask($request)")
if (request.taskId == 0L) {
val filter = defaultFilterProvider.getFilterFromPreference(
settings.data.firstOrNull()?.filter?.takeIf { it.isNotBlank() }
)
val task = taskCreator.basicQuickAddTask(
title = request.title,
filter = filter,
)
firebase.addTask("wearable")
return SaveTaskResponse.newBuilder().setTaskId(task.id).build()
} else {
taskDao.fetch(request.taskId)?.let { task ->
taskDao.save(
task.copy(
title = request.title,
completionDate = when {
!request.completed -> 0
task.isCompleted -> task.completionDate
else -> System.currentTimeMillis()
},
)
)
}
return SaveTaskResponse.newBuilder().setTaskId(request.taskId).build()
}
}
private fun getColor(filter: Filter): Int {
if (filter.tint != 0) {
val color = colorProvider.getThemeColor(filter.tint, true)
if (color.isFree || inventory.purchasedThemes()) {
return color.primaryColor
}
}
return 0
}
}

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="android_wear_capabilities" tools:ignore="UnusedResources">
<item>data_layer_app_helper_device_phone</item>
<item>horologist_phone</item>
</string-array>
</resources>

@ -1,20 +0,0 @@
{
"client_id" : "9d4babd5-e7ba-4286-ba4b-17274495a901",
"authorization_user_agent" : "DEFAULT",
"redirect_uri" : "msauth://org.tasks/sEe08kX5nGJi4miFX3VkNXICC%2FY%3D",
"account_mode" : "MULTIPLE",
"authorities" : [
{
"type": "AAD",
"audience": {
"type": "AzureADandPersonalMicrosoftAccount",
"tenant_id": "common"
}
}
],
"logging": {
"level": "info",
"logcat_enabled": true,
"pii_enabled": false
}
}

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="google_oauth_scheme">com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif</string> <string name="google_oauth_scheme">com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif</string>
<string name="microsoft_oauth_path">/sEe08kX5nGJi4miFX3VkNXICC/Y=</string>
</resources> </resources>

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

Loading…
Cancel
Save