Compare commits

..

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

@ -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,7 +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: |

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

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

@ -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,36 @@ 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.9)
rouge (3.28.0) 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 +184,6 @@ GEM
simctl (1.6.10) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
sysrandom (1.0.5)
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 +193,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 +210,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).

@ -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,18 +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.remoteviews)
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)
@ -199,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)
@ -210,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)
@ -231,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))
@ -241,18 +239,16 @@ dependencies {
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.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(libs.firebase.crashlytics)
@ -269,9 +265,6 @@ dependencies {
googleplayImplementation(libs.horologist.datalayer.grpc) googleplayImplementation(libs.horologist.datalayer.grpc)
googleplayImplementation(libs.horologist.datalayer.core) googleplayImplementation(libs.horologist.datalayer.core)
googleplayImplementation(libs.play.services.wearable) googleplayImplementation(libs.play.services.wearable)
googleplayImplementation(libs.microsoft.authentication) {
exclude("com.microsoft.device.display", "display-mask")
}
googleplayImplementation(projects.wearDatalayer) googleplayImplementation(projects.wearDatalayer)
androidTestImplementation(libs.dagger.hilt.testing) androidTestImplementation(libs.dagger.hilt.testing)

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

17
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
@ -53,12 +58,4 @@
# 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>; } -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
@ -96,13 +101,13 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Test @Test
fun getTaskFromRemoteId() = runBlocking { fun getTaskFromRemoteId() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234"))) insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals(1L, googleTaskDao.getTask("1234", "calendar")) assertEquals(1L, googleTaskDao.getTask("1234"))
} }
@Test @Test
fun getRemoteIdForTask() = runBlocking { fun getRemoteIdForTask() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234"))) insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals("1234", googleTaskDao.getRemoteId(1L, "calendar")) assertEquals("1234", googleTaskDao.getRemoteId(1L))
} }
@Test @Test
@ -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")))
@ -256,7 +246,7 @@ class GoogleTaskDaoTests : InjectingTestCase() {
} }
private suspend fun getOrder(remoteId: String): Long? { private suspend fun getOrder(remoteId: String): Long? {
return taskDao.fetch(googleTaskDao.getByRemoteId(remoteId, "calendar")!!.task)?.order return taskDao.fetch(googleTaskDao.getByRemoteId(remoteId)!!.task)?.order
} }
private suspend fun insertTop(googleTask: CaldavTask) { private suspend fun insertTop(googleTask: CaldavTask) {
@ -278,6 +268,6 @@ class GoogleTaskDaoTests : InjectingTestCase() {
} }
private suspend fun getByRemoteId(remoteId: String): CaldavTask { private suspend fun getByRemoteId(remoteId: String): CaldavTask {
return googleTaskDao.getByRemoteId(remoteId, "calendar")!! return googleTaskDao.getByRemoteId(remoteId)!!
} }
} }

@ -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,20 @@
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) = fun completeTask(source: String) {}
logEvent(R.string.event_complete_task, R.string.param_type to source)
val subscribeCooldown: Boolean val subscribeCooldown = false
get() = installCooldown val subGroupA = false
|| 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) {}

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

@ -53,20 +53,6 @@
</intent-filter> </intent-filter>
</service> </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,12 @@ 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 { private var crashlytics: FirebaseCrashlytics? = null
if (preferences.isTrackingEnabled) { private var analytics: FirebaseAnalytics? = null
FirebaseCrashlytics.getInstance().apply { private var remoteConfig: FirebaseRemoteConfig? = null
setCrashlyticsCollectionEnabled(true)
}
} 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)
@ -83,9 +53,7 @@ class Firebase @Inject constructor(
logEvent(R.string.event_complete_task, R.string.param_type to source) 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 +75,27 @@ class Firebase @Inject constructor(
get() = installCooldown get() = installCooldown
|| preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > currentTimeMillis() || preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > currentTimeMillis()
val subGroupA: Boolean
get() = remoteConfig?.getBoolean("sub_group_a") ?: 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

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

@ -25,7 +25,7 @@ class WearRefresherImpl(
init { init {
phoneDataLayerAppHelper phoneDataLayerAppHelper
.connectedAndInstalledNodes .connectedAndInstalledNodes
.catch { Timber.e("${it.message}") } .catch { Timber.e(it) }
.onEach { nodes -> .onEach { nodes ->
Timber.d("Connected nodes: ${nodes.joinToString()}") Timber.d("Connected nodes: ${nodes.joinToString()}")
watchConnected = nodes.isNotEmpty() watchConnected = nodes.isNotEmpty()

@ -87,7 +87,7 @@ class WearService(
.setId(item.value) .setId(item.value)
.setType(ListItemType.Header) .setType(ListItemType.Header)
.setTitle(headerFormatter.headerString(item.value, style = DateStyle.MEDIUM)) .setTitle(headerFormatter.headerString(item.value, style = DateStyle.MEDIUM))
.setCollapsed(item.collapsed) .setCollapsed(collapsed.contains(item.value))
.build() .build()
is UiItem.Task -> { is UiItem.Task -> {

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

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

@ -66,24 +66,13 @@
<!-- **************************************** --> <!-- **************************************** -->
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/> <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH"/> <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" android:maxSdkVersion="25"/>
<!-- ****************************** --> <!-- ****************************** -->
<!-- Check DAVx5/EteSync sync state --> <!-- Check DAVx5/EteSync sync state -->
<!-- ****************************** --> <!-- ****************************** -->
<uses-permission android:name="android.permission.READ_SYNC_STATS" /> <uses-permission android:name="android.permission.READ_SYNC_STATS" />
<!-- ******************************** -->
<!-- Microsoft Authentication Library -->
<!-- ******************************** -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" tools:node="remove" />
<uses-feature
android:name="android.hardware.nfc"
android:required="false" />
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />
<!-- ****************************************** --> <!-- ****************************************** -->
<!-- Exclude OpenTasks and jtxBoard permissions --> <!-- Exclude OpenTasks and jtxBoard permissions -->
<!-- ****************************************** --> <!-- ****************************************** -->
@ -159,14 +148,14 @@
</queries> </queries>
<application <application
android:pageSizeCompat="enabled"
android:allowBackup="true" android:allowBackup="true"
android:backupAgent="org.tasks.backup.TasksBackupAgent" android:backupAgent="org.tasks.backup.TasksBackupAgent"
android:backupInForeground="true" android:backupInForeground="true"
android:fullBackupOnly="false" android:fullBackupOnly="false"
android:icon="@mipmap/ic_launcher_blue" android:icon="@mipmap/ic_launcher_blue"
android:label="@string/app_name" android:label="@string/app_name"
android:name=".TasksApplication" android:manageSpaceActivity="org.tasks.preferences.ManageSpaceActivity"
android:name=".Tasks"
android:roundIcon="@mipmap/ic_launcher_blue" android:roundIcon="@mipmap/ic_launcher_blue"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Tasks" android:theme="@style/Tasks"
@ -191,6 +180,14 @@
<category android:name="android.intent.category.BROWSABLE"/> <category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="org.tasks.github.a50fdbf3e289a7fb2fc6" /> <data android:scheme="org.tasks.github.a50fdbf3e289a7fb2fc6" />
</intent-filter> </intent-filter>
<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> </activity>
<activity <activity
@ -210,9 +207,7 @@
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/TranslucentDialog"/> android:theme="@style/TranslucentDialog"/>
<activity <activity android:name=".location.LocationPickerActivity"/>
android:name=".location.LocationPickerActivity"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar" />
<activity <activity
android:exported="true" android:exported="true"
@ -282,7 +277,6 @@
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
<data android:mimeType="audio/*" /> <data android:mimeType="audio/*" />
<data android:mimeType="video/*"/>
<data android:mimeType="application/*" /> <data android:mimeType="application/*" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
@ -329,20 +323,11 @@
android:resource="@xml/samsung_scrollable_flex_window_widget_meta_info"/> android:resource="@xml/samsung_scrollable_flex_window_widget_meta_info"/>
</receiver> </receiver>
<receiver
android:name=".widget.RequestPinWidgetReceiver"
android:exported="false">
<intent-filter>
<action android:name="org.tasks.CONFIGURE_WIDGET" />
</intent-filter>
</receiver>
<!-- ======================================================== Services = --> <!-- ======================================================== Services = -->
<service <service
android:name="androidx.core.widget.RemoteViewsCompatService" android:name=".widget.TasksWidgetAdapter"
android:permission="android.permission.BIND_REMOTEVIEWS" android:permission="android.permission.BIND_REMOTEVIEWS"/>
android:exported="false"/>
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
@ -364,18 +349,6 @@
android:name="androidx.work.WorkManagerInitializer" android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup" android:value="androidx.startup"
tools:node="remove" /> tools:node="remove" />
<meta-data
android:name="com.mikepenz.iconics.animation.SpinProcessor"
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="com.mikepenz.iconics.animation.BlinkAlphaProcessor"
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="com.mikepenz.iconics.animation.BlinkScaleProcessor"
android:value="androidx.startup"
tools:node="remove" />
</provider> </provider>
<provider <provider
@ -400,13 +373,6 @@
android:resource="@xml/file_provider_paths"/> android:resource="@xml/file_provider_paths"/>
</provider> </provider>
<provider
android:name=".widget.WidgetIconProvider"
android:authorities="${applicationId}.widgeticons"
android:exported="true"
android:grantUriPermissions="true"
tools:ignore="ExportedContentProvider" />
<receiver <receiver
android:name="org.dmfs.provider.tasks.TaskProviderBroadcastReceiver" android:name="org.dmfs.provider.tasks.TaskProviderBroadcastReceiver"
tools:node="remove"/> tools:node="remove"/>
@ -452,10 +418,6 @@
android:name=".caldav.CaldavAccountSettingsActivity" android:name=".caldav.CaldavAccountSettingsActivity"
android:theme="@style/Tasks"/> android:theme="@style/Tasks"/>
<activity
android:name=".caldav.LocalAccountSettingsActivity"
android:theme="@style/Tasks" />
<activity <activity
android:name=".etebase.EtebaseAccountSettingsActivity" android:name=".etebase.EtebaseAccountSettingsActivity"
android:theme="@style/Tasks" /> android:theme="@style/Tasks" />
@ -506,11 +468,10 @@
</activity> </activity>
<receiver <receiver
android:name=".receivers.SystemEventReceiver" android:name=".receivers.BootCompletedReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.USER_PRESENT"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
@ -649,11 +610,13 @@
<receiver android:name="org.tasks.jobs.NotificationReceiver" /> <receiver android:name="org.tasks.jobs.NotificationReceiver" />
<activity <activity
android:name="com.todoroo.astrid.activity.MainActivity" android:name=".auth.MicrosoftAuthenticationActivity"
android:exported="true" android:theme="@style/TranslucentDialog"/>
android:launchMode="singleTask"
android:theme="@style/Tasks" <activity
android:windowSoftInputMode="adjustResize"> android:launchMode="singleTask"
android:exported="true"
android:name="com.todoroo.astrid.activity.MainActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
</intent-filter> </intent-filter>
@ -690,6 +653,9 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".preferences.ManageSpaceActivity"
android:theme="@style/Tasks" />
<activity android:name="org.tasks.sync.microsoft.MicrosoftListSettingsActivity" /> <activity android:name="org.tasks.sync.microsoft.MicrosoftListSettingsActivity" />
<activity <activity

@ -0,0 +1 @@
../../../../CHANGELOG.md

@ -39,14 +39,32 @@ object AndroidUtilities {
return (dp * displayMetrics.density + 0.5f).toInt() return (dp * displayMetrics.density + 0.5f).toInt()
} }
fun preOreo(): Boolean {
return !atLeastOreo()
}
fun preS(): Boolean { fun preS(): Boolean {
return !atLeastS() return !atLeastS()
} }
@JvmStatic
fun preTiramisu(): Boolean {
return !atLeastTiramisu()
}
fun preUpsideDownCake(): Boolean { fun preUpsideDownCake(): Boolean {
return Build.VERSION.SDK_INT <= VERSION_CODES.TIRAMISU return Build.VERSION.SDK_INT <= VERSION_CODES.TIRAMISU
} }
fun atLeastNougatMR1(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.N_MR1
}
@JvmStatic
fun atLeastOreo(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.O
}
fun atLeastOreoMR1(): Boolean { fun atLeastOreoMR1(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1 return Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1
} }
@ -75,14 +93,6 @@ object AndroidUtilities {
return Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU return Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU
} }
fun atLeastAndroid15(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.VANILLA_ICE_CREAM
}
fun atLeastAndroid16(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.BAKLAVA
}
fun assertMainThread() { fun assertMainThread() {
check(!(BuildConfig.DEBUG && !isMainThread)) { "Should be called from main thread" } check(!(BuildConfig.DEBUG && !isMainThread)) { "Should be called from main thread" }
} }

@ -12,12 +12,8 @@ import static java.util.Arrays.asList;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -88,24 +84,12 @@ public class BeastModePreferences extends ThemedInjectingAppCompatActivity
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
BeastModePrefActivityBinding binding = BeastModePrefActivityBinding.inflate(getLayoutInflater()); BeastModePrefActivityBinding binding = BeastModePrefActivityBinding.inflate(getLayoutInflater());
Toolbar toolbar = binding.toolbar.toolbar; Toolbar toolbar = binding.toolbar.toolbar;
RecyclerView recyclerView = binding.recyclerView; RecyclerView recyclerView = binding.recyclerView;
setContentView(binding.getRoot()); setContentView(binding.getRoot());
ViewCompat.setOnApplyWindowInsetsListener(
binding.getRoot(),
(v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
ViewGroup.MarginLayoutParams toolbarParams =
(ViewGroup.MarginLayoutParams) toolbar.getLayoutParams();
toolbarParams.topMargin = systemBars.top;
recyclerView.setPadding(0, 0, 0, systemBars.bottom);
return insets;
});
toolbar.setNavigationIcon( toolbar.setNavigationIcon(
getDrawable(R.drawable.ic_outline_arrow_back_24px)); getDrawable(R.drawable.ic_outline_arrow_back_24px));
toolbar.setNavigationOnClickListener(v -> finish()); toolbar.setNavigationOnClickListener(v -> finish());

@ -5,88 +5,93 @@
*/ */
package com.todoroo.astrid.activity package com.todoroo.astrid.activity
import android.app.ActivityOptions
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import androidx.activity.SystemBarStyle import android.view.View
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.compose.material3.DrawerValue import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.IntentCompat.getParcelableExtra import androidx.core.content.IntentCompat.getParcelableExtra
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.compose.composable import com.todoroo.andlib.utility.AndroidUtilities
import androidx.navigation.compose.rememberNavController import com.todoroo.andlib.utility.AndroidUtilities.atLeastR
import androidx.navigation.toRoute import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment
import com.todoroo.astrid.adapter.SubheaderClickHandler import com.todoroo.astrid.adapter.SubheaderClickHandler
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.R import org.tasks.R
import org.tasks.Tasks
import org.tasks.activities.GoogleTaskListSettingsActivity
import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.auth.SignInActivity
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.caldav.CaldavAccountSettingsActivity import org.tasks.billing.PurchaseActivity
import org.tasks.compose.AddAccountDestination import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.HomeDestination import org.tasks.compose.drawer.DrawerAction
import org.tasks.compose.accounts.AddAccountScreen import org.tasks.compose.drawer.DrawerItem
import org.tasks.compose.accounts.AddAccountViewModel import org.tasks.compose.drawer.MenuSearchBar
import org.tasks.compose.home.HomeScreen import org.tasks.compose.drawer.TaskListDrawer
import org.tasks.data.dao.AlarmDao import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.CaldavDao 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.entity.Place
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.dialogs.ImportTasksDialog import org.tasks.data.getLocation
import org.tasks.data.listSettingsClass
import org.tasks.databinding.TaskListActivityBinding
import org.tasks.dialogs.NewFilterDialog import org.tasks.dialogs.NewFilterDialog
import org.tasks.etebase.EtebaseAccountSettingsActivity import org.tasks.dialogs.WhatsNewDialog
import org.tasks.extensions.Context.findActivity
import org.tasks.extensions.Context.nightMode import org.tasks.extensions.Context.nightMode
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.openUri
import org.tasks.extensions.broughtToFront import org.tasks.extensions.hideKeyboard
import org.tasks.extensions.flagsToString
import org.tasks.extensions.isFromHistory
import org.tasks.files.FileHelper
import org.tasks.filters.Filter import org.tasks.filters.Filter
import org.tasks.jobs.WorkManager import org.tasks.filters.FilterProvider
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_LIST
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_PLACE
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_TAGS
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.PlaceFilter
import org.tasks.location.LocationPickerActivity
import org.tasks.location.LocationPickerActivity.Companion.EXTRA_PLACE
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.HelpAndFeedback
import org.tasks.preferences.MainPreferences
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.preferences.fragments.FRAG_TAG_IMPORT_TASKS
import org.tasks.sync.AddAccountDialog
import org.tasks.sync.SyncAdapters
import org.tasks.sync.microsoft.MicrosoftSignInViewModel
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.ui.EmptyTaskEditFragment.Companion.newEmptyTaskEditFragment
import org.tasks.ui.MainActivityEvent
import org.tasks.ui.MainActivityEventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@ -99,209 +104,286 @@ class MainActivity : AppCompatActivity() {
@Inject lateinit var locationDao: LocationDao @Inject lateinit var locationDao: LocationDao
@Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao @Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var eventBus: MainActivityEventBus
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var workManager: WorkManager
private val viewModel: MainActivityViewModel by viewModels() private val viewModel: MainActivityViewModel by viewModels()
private var currentNightMode = 0 private var currentNightMode = 0
private var currentPro = false private var currentPro = false
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private var isReady = false private lateinit var binding: TaskListActivityBinding
/** @see android.app.Activity.onCreate
*/
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
theme.themeBase.set(this) theme.applyTheme(this)
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !isReady }
currentNightMode = nightMode currentNightMode = nightMode
currentPro = inventory.hasPro currentPro = inventory.hasPro
binding = TaskListActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
logIntent("onCreate")
handleIntent()
enableEdgeToEdge( binding.composeView.setContent {
statusBarStyle = SystemBarStyle.auto( if (viewModel.drawerOpen.collectAsStateWithLifecycle().value) {
lightScrim = Color.TRANSPARENT, TasksTheme {
darkScrim = Color.TRANSPARENT val sheetState = rememberModalBottomSheetState(
), skipPartiallyExpanded = true,
navigationBarStyle = if (theme.themeBase.isDarkTheme(this)) { confirmValueChange = { true },
SystemBarStyle.dark(Color.TRANSPARENT) )
} else { ModalBottomSheet(
SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT) sheetState = sheetState,
} containerColor = MaterialTheme.colorScheme.surface,
) onDismissRequest = { viewModel.closeDrawer() },
) {
setContent { val state = viewModel.state.collectAsStateWithLifecycle().value
TasksTheme( val context = LocalContext.current
theme = theme.themeBase.index, val settingsRequest = rememberLauncherForActivityResult(
primary = theme.themeColor.primaryColor, ActivityResultContracts.StartActivityForResult()
) { ) {
val navController = rememberNavController() context.findActivity()?.recreate()
val hasAccount = viewModel
.accountExists
.collectAsStateWithLifecycle(null)
.value
LaunchedEffect(hasAccount) {
Timber.d("hasAccount=$hasAccount")
if (hasAccount == false) {
navController.navigate(AddAccountDestination(showImport = true))
}
isReady = hasAccount != null
}
NavHost(
navController = navController,
startDestination = HomeDestination,
) {
composable<AddAccountDestination> {
val route = it.toRoute<AddAccountDestination>()
LaunchedEffect(hasAccount) {
if (route.showImport && hasAccount == true) {
navController.popBackStack()
}
} }
val addAccountViewModel: AddAccountViewModel = hiltViewModel() val scope = rememberCoroutineScope()
val microsoftVM: MicrosoftSignInViewModel = hiltViewModel() val bottomSearchBar = atLeastR()
val syncLauncher = TaskListDrawer(
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> arrangement = when {
if (result.resultCode == RESULT_OK) { state.menuQuery.isBlank() -> Arrangement.Top
syncAdapters.sync(true) bottomSearchBar -> Arrangement.Bottom
workManager.updateBackgroundSync() else -> Arrangement.Top
} else { },
result.data bottomSearchBar = bottomSearchBar,
?.getStringExtra(GtasksLoginActivity.EXTRA_ERROR) filters = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems,
?.let { toast(it) } onClick = {
} when (it) {
} is DrawerItem.Filter -> {
val importBackupLauncher = viewModel.setFilter(it.filter)
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> scope.launch(Dispatchers.Default) {
if (result.resultCode == RESULT_OK) { sheetState.hide()
val uri = result.data?.data ?: return@rememberLauncherForActivityResult viewModel.closeDrawer()
ImportTasksDialog.newImportTasksDialog(uri) }
.show(supportFragmentManager, FRAG_TAG_IMPORT_TASKS) }
is DrawerItem.Header -> {
viewModel.toggleCollapsed(it.header)
}
} }
} },
AddAccountScreen( onAddClick = {
gettingStarted = route.showImport, scope.launch(Dispatchers.Default) {
hasTasksAccount = inventory.hasTasksAccount, sheetState.hide()
hasPro = inventory.hasPro, viewModel.closeDrawer()
onBack = { navController.popBackStack() }, when (it.header.addIntentRc) {
signIn = { platform -> FilterProvider.REQUEST_NEW_FILTER ->
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to platform) NewFilterDialog.newFilterDialog().show(
when (platform) { supportFragmentManager,
AddAccountDialog.Platform.TASKS_ORG -> SubheaderClickHandler.FRAG_TAG_NEW_FILTER
syncLauncher.launch( )
Intent(this@MainActivity, SignInActivity::class.java)
) REQUEST_NEW_PLACE ->
startActivityForResult(
AddAccountDialog.Platform.GOOGLE_TASKS -> Intent(
syncLauncher.launch( this@MainActivity,
Intent(this@MainActivity, GtasksLoginActivity::class.java) LocationPickerActivity::class.java
) ),
REQUEST_NEW_PLACE
AddAccountDialog.Platform.MICROSOFT -> )
microsoftVM.signIn(this@MainActivity)
REQUEST_NEW_TAGS ->
AddAccountDialog.Platform.CALDAV -> startActivityForResult(
syncLauncher.launch( Intent(
Intent(this@MainActivity, CaldavAccountSettingsActivity::class.java) this@MainActivity,
) TagSettingsActivity::class.java
),
AddAccountDialog.Platform.ETESYNC -> REQUEST_NEW_LIST
syncLauncher.launch( )
Intent(this@MainActivity, EtebaseAccountSettingsActivity::class.java)
) REQUEST_NEW_LIST -> lifecycleScope.launch {
val account =
AddAccountDialog.Platform.LOCAL -> caldavDao.getAccount(it.header.id) ?: return@launch
addAccountViewModel.createLocalAccount() when (it.header.subheaderType) {
NavigationDrawerSubheader.SubheaderType.GOOGLE_TASKS ->
else -> throw IllegalArgumentException() startActivityForResult(
Intent(
this@MainActivity,
GoogleTaskListSettingsActivity::class.java
)
.putExtra(
GoogleTaskListSettingsActivity.EXTRA_ACCOUNT,
account
),
REQUEST_NEW_LIST
)
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS ->
startActivityForResult(
Intent(
this@MainActivity,
account.listSettingsClass()
)
.putExtra(
BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT,
account
),
REQUEST_NEW_LIST
)
else -> {}
}
}
else -> Timber.e("Unhandled request code: $it")
}
} }
}, },
openUrl = { platform -> onErrorClick = {
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to platform.name) context.startActivity(Intent(context, MainPreferences::class.java))
addAccountViewModel.openUrl(this@MainActivity, platform)
}, },
onImportBackup = { searchBar = {
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to "import_backup") MenuSearchBar(
importBackupLauncher.launch( begForMoney = state.begForMoney,
FileHelper.newFilePickerIntent(this@MainActivity, preferences.backupDirectory), onDrawerAction = {
viewModel.closeDrawer()
when (it) {
DrawerAction.PURCHASE ->
if (Tasks.IS_GENERIC)
context.openUri(R.string.url_donate)
else
context.startActivity(
Intent(
context,
PurchaseActivity::class.java
)
)
DrawerAction.SETTINGS ->
settingsRequest.launch(
Intent(
context,
MainPreferences::class.java
)
)
DrawerAction.HELP_AND_FEEDBACK ->
context.startActivity(
Intent(
context,
HelpAndFeedback::class.java
)
)
}
},
query = state.menuQuery,
onQueryChange = { viewModel.queryMenu(it) },
) )
} },
) )
} }
composable<HomeDestination> { }
if (hasAccount != true) { }
return@composable }
}
val scope = rememberCoroutineScope()
val state = viewModel.state.collectAsStateWithLifecycle().value
val drawerState = rememberDrawerState(
initialValue = DrawerValue.Closed,
confirmStateChange = {
viewModel.setDrawerState(it == DrawerValue.Open)
true
}
)
val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
windowAdaptiveInfo = currentWindowAdaptiveInfo(),
).copy(
horizontalPartitionSpacerSize = 0.dp,
verticalPartitionSpacerSize = 0.dp,
)
)
val keyboard = LocalSoftwareKeyboardController.current
LaunchedEffect(state.task) {
val pane = if (state.task == null) {
ThreePaneScaffoldRole.Secondary
} else {
ThreePaneScaffoldRole.Primary
}
Timber.d("Navigating to $pane")
navigator.navigateTo(pane = pane)
}
val isDetailVisible = eventBus
navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded .onEach(this::process)
BackHandler(enabled = state.task == null) { .launchIn(lifecycleScope)
Timber.d("onBackPressed")
if (isDetailVisible && navigator.canNavigateBack()) { lifecycleScope.launch {
scope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
navigator.navigateBack() updateSystemBars(viewModel.state.value.filter)
} }
} else { }
finish()
if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) { viewModel
runBlocking { .state
viewModel.resetFilter() .flowWithLifecycle(lifecycle)
} .map { it.filter to it.task }
.distinctUntilChanged()
.onEach { (newFilter, task) ->
Timber.d("filter: $newFilter task: $task")
val existingTlf =
supportFragmentManager.findFragmentByTag(FRAG_TAG_TASK_LIST) as TaskListFragment?
val existingFilter = existingTlf?.getFilter()
val tlf = if (
existingFilter != null
&& existingFilter.areItemsTheSame(newFilter)
&& existingFilter == newFilter
// && check if manual sort changed
) {
existingTlf
} else {
clearUi()
TaskListFragment.newTaskListFragment(newFilter)
}
val existingTef =
supportFragmentManager.findFragmentByTag(FRAG_TAG_TASK_EDIT) as TaskEditFragment?
val transaction = supportFragmentManager.beginTransaction()
if (task == null) {
if (intent.finishAffinity) {
finishAffinity()
} else if (existingTef != null) {
if (intent.removeTask && intent.broughtToFront) {
moveTaskToBack(true)
}
hideKeyboard()
transaction
.replace(R.id.detail, newEmptyTaskEditFragment())
.runOnCommit {
if (isSinglePaneLayout) {
binding.master.visibility = View.VISIBLE
binding.detail.visibility = View.GONE
} }
} }
} }
LaunchedEffect(state.filter, state.task) { } else if (task != existingTef?.task) {
actionMode?.finish() existingTef?.save(remove = false)
actionMode = null transaction
if (state.task == null) { .replace(R.id.detail, newTaskEditFragment(task), FRAG_TAG_TASK_EDIT)
keyboard?.hide() .runOnCommit {
if (isSinglePaneLayout) {
binding.detail.visibility = View.VISIBLE
binding.master.visibility = View.GONE
} }
drawerState.close()
} }
HomeScreen(
state = state,
drawerState = drawerState,
navigator = navigator,
showNewFilterDialog = {
NewFilterDialog.newFilterDialog().show(
supportFragmentManager,
SubheaderClickHandler.FRAG_TAG_NEW_FILTER
)
},
)
}
} }
defaultFilterProvider.setLastViewedFilter(newFilter)
theme
.withThemeColor(getFilterColor(newFilter))
.applyToContext(this) // must happen before committing fragment
transaction
.replace(R.id.master, tlf, FRAG_TAG_TASK_LIST)
.runOnCommit { updateSystemBars(newFilter) }
.commit()
} }
.launchIn(lifecycleScope)
}
private fun process(event: MainActivityEvent) = when (event) {
is MainActivityEvent.ClearTaskEditFragment ->
viewModel.setTask(null)
}
@Deprecated("Deprecated in Java")
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_NEW_LIST ->
if (resultCode == RESULT_OK && data != null) {
getParcelableExtra(data, OPEN_FILTER, Filter::class.java)?.let {
viewModel.setFilter(it)
}
}
REQUEST_NEW_PLACE ->
if (resultCode == RESULT_OK && data != null) {
getParcelableExtra(data, EXTRA_PLACE, Place::class.java)?.let {
viewModel.setFilter(PlaceFilter(it))
}
}
else ->
super.onActivityResult(requestCode, resultCode, data)
} }
logIntent("onCreate")
handleIntent()
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
@ -311,6 +393,12 @@ class MainActivity : AppCompatActivity() {
handleIntent() handleIntent()
} }
private fun clearUi() {
actionMode?.finish()
actionMode = null
viewModel.closeDrawer()
}
private suspend fun getTaskToLoad(filter: Filter?): Task? = when { private suspend fun getTaskToLoad(filter: Filter?): Task? = when {
intent.isFromHistory -> null intent.isFromHistory -> null
intent.hasExtra(CREATE_TASK) -> { intent.hasExtra(CREATE_TASK) -> {
@ -333,13 +421,16 @@ class MainActivity : AppCompatActivity() {
private fun logIntent(caller: String) { private fun logIntent(caller: String) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.d(""" Timber.d("""
|$caller $caller
|********** **********
|broughtToFront: ${intent.broughtToFront} broughtToFront: ${intent.broughtToFront}
|isFromHistory: ${intent.isFromHistory} isFromHistory: ${intent.isFromHistory}
|flags: ${intent.flagsToString} flags: ${intent.flagsToString}
${intent?.extras?.keySet()?.joinToString("\n") { "|$it: ${intent.extras?.get(it)}" } ?: "|NO EXTRAS"} OPEN_FILTER: ${getParcelableExtra(intent, OPEN_FILTER, Filter::class.java)?.let { "${it.title}: $it" }}
|**********""".trimMargin() LOAD_FILTER: ${intent.getStringExtra(LOAD_FILTER)}
OPEN_TASK: ${getParcelableExtra(intent, OPEN_TASK, Task::class.java)}
CREATE_TASK: ${intent.hasExtra(CREATE_TASK)}
**********""".trimIndent()
) )
} }
} }
@ -354,36 +445,65 @@ class MainActivity : AppCompatActivity() {
} }
} }
private fun updateSystemBars(filter: Filter) {
with (getFilterColor(filter)) {
applyToNavigationBar(this@MainActivity)
applyTaskDescription(this@MainActivity, filter.title ?: getString(R.string.app_name))
}
}
private fun getFilterColor(filter: Filter) =
if (filter.tint != 0)
colorProvider.getThemeColor(filter.tint, true)
else
theme.themeColor
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
Timber.d("onResume")
if (currentNightMode != nightMode || currentPro != inventory.hasPro) { if (currentNightMode != nightMode || currentPro != inventory.hasPro) {
restartActivity() recreate()
return return
} }
if (preferences.getBoolean(R.string.p_just_updated, false)) {
if (preferences.getBoolean(R.string.p_show_whats_new, true)) {
val fragmentManager = supportFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_WHATS_NEW) == null) {
WhatsNewDialog().show(fragmentManager, FRAG_TAG_WHATS_NEW)
}
}
preferences.setBoolean(R.string.p_just_updated, false)
}
} }
override fun onPause() { private suspend fun newTaskEditFragment(task: Task): TaskEditFragment {
super.onPause() AndroidUtilities.assertMainThread()
Timber.d("onPause") clearUi()
return coroutineScope {
withContext(Dispatchers.Default) {
val freshTask = async { if (task.isNew) task else taskDao.fetch(task.id) ?: task }
val list = async { defaultFilterProvider.getList(task) }
val location = async { locationDao.getLocation(task, preferences) }
val tags = async { tagDataDao.getTags(task) }
val alarms = async { alarmDao.getAlarms(task) }
newTaskEditFragment(
freshTask.await(),
list.await(),
location.await(),
tags.await(),
alarms.await(),
)
}
}
} }
private val isSinglePaneLayout: Boolean
get() = !resources.getBoolean(R.bool.two_pane_layout)
override fun onSupportActionModeStarted(mode: ActionMode) { override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode) super.onSupportActionModeStarted(mode)
actionMode = mode actionMode = mode
} }
fun restartActivity() {
finish()
startActivity(
Intent(this, MainActivity::class.java),
ActivityOptions.makeCustomAnimation(
this@MainActivity,
android.R.anim.fade_in, android.R.anim.fade_out
).toBundle()
)
}
companion object { companion object {
/** For indicating the new list screen should be launched at fragment setup time */ /** For indicating the new list screen should be launched at fragment setup time */
const val OPEN_FILTER = "open_filter" // $NON-NLS-1$ const val OPEN_FILTER = "open_filter" // $NON-NLS-1$
@ -393,6 +513,11 @@ class MainActivity : AppCompatActivity() {
const val OPEN_TASK = "open_new_task" // $NON-NLS-1$ const val OPEN_TASK = "open_new_task" // $NON-NLS-1$
const val REMOVE_TASK = "remove_task" const val REMOVE_TASK = "remove_task"
const val FINISH_AFFINITY = "finish_affinity" const val FINISH_AFFINITY = "finish_affinity"
private const val FRAG_TAG_TASK_LIST = "frag_tag_task_list"
private const val FRAG_TAG_WHATS_NEW = "frag_tag_whats_new"
private const val FRAG_TAG_TASK_EDIT = "frag_tag_task_edit"
private const val FLAG_FROM_HISTORY
= Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
val Intent.getFilter: Filter? val Intent.getFilter: Filter?
get() = if (isFromHistory) { get() = if (isFromHistory) {
@ -415,17 +540,35 @@ class MainActivity : AppCompatActivity() {
} }
val Intent.removeTask: Boolean val Intent.removeTask: Boolean
get() = try { get() = if (isFromHistory) {
getBooleanExtra(REMOVE_TASK, false) && !isFromHistory && !broughtToFront false
} finally { } else {
removeExtra(REMOVE_TASK) getBooleanExtra(REMOVE_TASK, false).let {
removeExtra(REMOVE_TASK)
it
}
} }
val Intent.finishAffinity: Boolean val Intent.finishAffinity: Boolean
get() = try { get() = if (isFromHistory) {
getBooleanExtra(FINISH_AFFINITY, false) && !isFromHistory && !broughtToFront false
} finally { } else {
removeExtra(FINISH_AFFINITY) getBooleanExtra(FINISH_AFFINITY, false).let {
removeExtra(FINISH_AFFINITY)
it
}
} }
val Intent.isFromHistory: Boolean
get() = flags and FLAG_FROM_HISTORY == FLAG_FROM_HISTORY
val Intent.broughtToFront: Boolean
get() = flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT > 0
val Intent.flagsToString
get() = Intent::class.java.declaredFields
.filter { it.name.startsWith("FLAG_") }
.filter { flags or it.getInt(null) == flags }
.joinToString(" | ") { it.name }
} }
} }

@ -14,22 +14,15 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.TasksApplication.Companion.IS_GENERIC import org.tasks.Tasks.Companion.IS_GENERIC
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.compose.drawer.DrawerItem import org.tasks.compose.drawer.DrawerItem
import org.tasks.compose.throttleLatest
import org.tasks.data.NO_COUNT import org.tasks.data.NO_COUNT
import org.tasks.data.count import org.tasks.data.count
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
@ -39,18 +32,16 @@ import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider import org.tasks.filters.FilterProvider
import org.tasks.filters.NavigationDrawerSubheader import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.SearchFilter
import org.tasks.filters.getIcon import org.tasks.filters.getIcon
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.TasksPreferences import org.tasks.preferences.TasksPreferences
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class MainActivityViewModel @Inject constructor( class MainActivityViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val defaultFilterProvider: DefaultFilterProvider, private val defaultFilterProvider: DefaultFilterProvider,
private val filterProvider: FilterProvider, private val filterProvider: FilterProvider,
private val taskDao: TaskDao, private val taskDao: TaskDao,
@ -71,7 +62,7 @@ class MainActivityViewModel @Inject constructor(
) )
private val _drawerOpen = MutableStateFlow(false) private val _drawerOpen = MutableStateFlow(false)
private val _updateFilters = MutableStateFlow(0L) val drawerOpen = _drawerOpen.asStateFlow()
private val _state = MutableStateFlow( private val _state = MutableStateFlow(
State( State(
@ -81,22 +72,15 @@ class MainActivityViewModel @Inject constructor(
} }
?: runBlocking { defaultFilterProvider.getStartupFilter() }, ?: runBlocking { defaultFilterProvider.getStartupFilter() },
begForMoney = if (IS_GENERIC) !inventory.hasTasksAccount else !inventory.hasPro, begForMoney = if (IS_GENERIC) !inventory.hasTasksAccount else !inventory.hasPro,
task = savedStateHandle.get<Task>(EXTRA_TASK),
) )
) )
val state = _state.asStateFlow() val state = _state.asStateFlow()
companion object {
private const val EXTRA_TASK = "extra_task"
}
val accountExists: Flow<Boolean>
get() = caldavDao.watchAccountExists()
private val refreshReceiver = object : BroadcastReceiver() { private val refreshReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) { when (intent?.action) {
LocalBroadcastManager.REFRESH -> _updateFilters.update { currentTimeMillis() } LocalBroadcastManager.REFRESH,
LocalBroadcastManager.REFRESH_LIST -> updateFilters()
} }
} }
} }
@ -112,7 +96,6 @@ class MainActivityViewModel @Inject constructor(
if (filter == _state.value.filter && task == null) { if (filter == _state.value.filter && task == null) {
return return
} }
savedStateHandle[EXTRA_TASK] = task
_state.update { _state.update {
it.copy( it.copy(
filter = filter, filter = filter,
@ -120,37 +103,28 @@ class MainActivityViewModel @Inject constructor(
) )
} }
updateFilters() updateFilters()
if (filter !is SearchFilter) { defaultFilterProvider.setLastViewedFilter(filter)
defaultFilterProvider.setLastViewedFilter(filter)
}
} }
fun setDrawerState(opened: Boolean) { fun closeDrawer() {
_drawerOpen.update { opened } _drawerOpen.update { false }
if (!opened) { _state.update { it.copy(menuQuery = "") }
_state.update { it.copy(menuQuery = "") } }
}
fun openDrawer() {
_drawerOpen.update { true }
} }
init { init {
localBroadcastManager.registerRefreshListReceiver(refreshReceiver) localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
updateFilters()
_updateFilters
.onStart { updateFilters() }
.combine(_drawerOpen) { timestamp, drawerOpen ->
if (drawerOpen) timestamp else null
}
.filterNotNull()
.throttleLatest(1000)
.onEach { updateFilters() }
.launchIn(viewModelScope)
} }
override fun onCleared() { override fun onCleared() {
localBroadcastManager.unregisterReceiver(refreshReceiver) localBroadcastManager.unregisterReceiver(refreshReceiver)
} }
private fun updateFilters() = viewModelScope.launch(Dispatchers.IO) { fun updateFilters() = viewModelScope.launch(Dispatchers.Default) {
val selected = state.value.filter val selected = state.value.filter
filterProvider filterProvider
.drawerItems() .drawerItems()
@ -221,18 +195,18 @@ class MainActivityViewModel @Inject constructor(
when (subheader.subheaderType) { when (subheader.subheaderType) {
NavigationDrawerSubheader.SubheaderType.PREFERENCE -> { NavigationDrawerSubheader.SubheaderType.PREFERENCE -> {
tasksPreferences.set(booleanPreferencesKey(subheader.id), collapsed) tasksPreferences.set(booleanPreferencesKey(subheader.id), collapsed)
localBroadcastManager.broadcastRefresh() localBroadcastManager.broadcastRefreshList()
} }
NavigationDrawerSubheader.SubheaderType.GOOGLE_TASKS,
NavigationDrawerSubheader.SubheaderType.CALDAV, NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS -> { NavigationDrawerSubheader.SubheaderType.TASKS -> {
caldavDao.setCollapsed(subheader.id, collapsed) caldavDao.setCollapsed(subheader.id, collapsed)
localBroadcastManager.broadcastRefresh() localBroadcastManager.broadcastRefreshList()
} }
} }
} }
fun setTask(task: Task?) { fun setTask(task: Task?) {
savedStateHandle[EXTRA_TASK] = task
_state.update { it.copy(task = task) } _state.update { it.copy(task = task) }
} }
@ -240,10 +214,4 @@ class MainActivityViewModel @Inject constructor(
_state.update { it.copy(menuQuery = query) } _state.update { it.copy(menuQuery = query) }
updateFilters() updateFilters()
} }
suspend fun getAccount(id: Long) = caldavDao.getAccount(id)
fun openLastViewedFilter() = viewModelScope.launch {
setFilter(defaultFilterProvider.getLastViewedFilter())
}
} }

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

Loading…
Cancel
Save