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

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

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

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

@ -154,8 +154,6 @@ dependencies {
implementation(projects.data)
implementation(projects.kmp)
implementation(projects.icons)
implementation(libs.androidx.navigation)
implementation(libs.androidx.adaptive.navigation.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bitfire.dav4jvm) {
exclude(group = "junit")
@ -178,18 +176,14 @@ dependencies {
implementation(libs.dagger.hilt)
ksp(libs.dagger.hilt.compiler)
ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.navigation)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.core.remoteviews)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.datastore)
implementation(libs.androidx.fragment.compose)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.room)
implementation(libs.androidx.sqlite)
implementation(libs.androidx.appcompat)
implementation(libs.iconics)
implementation(libs.markwon)
@ -199,6 +193,9 @@ dependencies {
implementation(libs.markwon.tables)
implementation(libs.markwon.tasklist)
debugImplementation(libs.facebook.flipper)
debugImplementation(libs.facebook.flipper.network)
debugImplementation(libs.facebook.soloader)
debugImplementation(libs.leakcanary)
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation(libs.kotlin.reflect)
@ -210,7 +207,6 @@ dependencies {
implementation(libs.persistent.cookiejar)
implementation(libs.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.preference)
@ -231,6 +227,8 @@ dependencies {
implementation(libs.colorpicker)
implementation(libs.appauth)
implementation(libs.osmdroid)
implementation(libs.retrofit)
implementation(libs.retrofit.moshi)
implementation(libs.androidx.recyclerview)
implementation(platform(libs.androidx.compose))
@ -241,18 +239,16 @@ dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation("androidx.compose.ui:ui-viewbinding")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation(libs.coil.compose)
implementation(libs.coil.video)
implementation(libs.coil.svg)
implementation(libs.coil.gif)
implementation(libs.ktor)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.content.negotiation)
implementation(libs.ktor.serialization)
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.systemuicontroller)
googleplayImplementation(platform(libs.firebase))
googleplayImplementation(libs.firebase.crashlytics)
@ -269,9 +265,6 @@ dependencies {
googleplayImplementation(libs.horologist.datalayer.grpc)
googleplayImplementation(libs.horologist.datalayer.core)
googleplayImplementation(libs.play.services.wearable)
googleplayImplementation(libs.microsoft.authentication) {
exclude("com.microsoft.device.display", "display-mask")
}
googleplayImplementation(projects.wearDatalayer)
androidTestImplementation(libs.dagger.hilt.testing)

@ -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.** { *; }
# remove logging statements
-assumenosideeffects class timber.log.Timber* {
public static *** v(...);
public static *** d(...);
public static *** i(...);
}
# guava
-dontwarn sun.misc.Unsafe
-dontwarn java.lang.ClassValue
@ -26,8 +33,6 @@
-dontwarn net.fortuna.ical4j.model.**
-dontwarn org.codehaus.groovy.**
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
-dontwarn com.github.erosb.jsonsKema.** # ical4android
-dontwarn org.jparsec.** # ical4android
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
-keep class at.bitfire.** { *; } # all DAVdroid code is required
@ -53,12 +58,4 @@
# material icons
-keep class androidx.compose.material.icons.outlined.** { *; }
# microsoft authentication
-dontwarn com.microsoft.device.display.DisplayMask
-dontwarn com.google.android.libraries.identity.**
-dontwarn edu.umd.cs.findbugs.annotations.**
-dontwarn com.google.crypto.tink.subtle.**
-dontwarn net.jcip.annotations.**
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { <fields>; }

@ -349,7 +349,7 @@ class DateUtilitiesTest {
}
@Test
fun hebrewDateTimeNoYear() = withLocale(Locale.forLanguageTag("he")) {
fun hebrewDateTimeNoYear() = withLocale(Locale.forLanguageTag("iw")) {
freezeAt(DateTime(2018, 12, 12)) {
assertMatches(
"יום ראשון, 14 בינואר( בשעה)? 13:45",
@ -359,7 +359,7 @@ class DateUtilitiesTest {
}
@Test
fun hebrewDateTimeWithYear() = withLocale(Locale.forLanguageTag("he")) {
fun hebrewDateTimeWithYear() = withLocale(Locale.forLanguageTag("iw")) {
freezeAt(DateTime(2017, 12, 12)) {
assertMatches(
"יום ראשון, 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_NEW_TASK
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.todoroo.astrid.activity.MainActivity.Companion.isFromHistory
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.extensions.isFromHistory
@RunWith(AndroidJUnit4::class)
class MainActivityTest {

@ -2,9 +2,12 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import org.tasks.filters.CaldavFilter
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@ -12,16 +15,13 @@ import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task
import org.tasks.filters.CaldavFilter
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.TASK
@ -33,6 +33,7 @@ import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao
@ -44,10 +45,7 @@ class CaldavManualSortTaskAdapterTest : InjectingTestCase() {
private lateinit var adapter: CaldavManualSortTaskAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = CaldavFilter(
calendar = CaldavCalendar(name = "calendar", uuid = "1234"),
account = CaldavAccount(accountType = TYPE_CALDAV)
)
private val filter = CaldavFilter(CaldavCalendar(name = "calendar", uuid = "1234"))
private val dataSource = object : TaskAdapterDataSource {
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 {
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
adapter.moved(from, adjustedTo, indent)
}

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

@ -2,9 +2,11 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import org.tasks.filters.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
@ -15,12 +17,10 @@ import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task
import org.tasks.filters.CaldavFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
@ -29,6 +29,7 @@ import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao
@ -40,10 +41,7 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
private lateinit var adapter: GoogleTaskManualSortAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = CaldavFilter(
calendar = CaldavCalendar(uuid = "1234"),
account = CaldavAccount(accountType = TYPE_GOOGLE_TASKS)
)
private val filter = GtasksFilter(CaldavCalendar(uuid = "1234"))
private val dataSource = object : TaskAdapterDataSource {
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 {
tasks.addAll(taskDao.fetchTasks(getQuery(preferences, filter)))
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) })
val adjustedTo = if (from < to) to + 1 else to
adapter.moved(from, adjustedTo, indent)
}

@ -3,32 +3,53 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.Task
import org.tasks.filters.MyTasksFilter
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.preferences.Preferences
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class OfflineSubtaskTest : InjectingTestCase() {
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskMover: TaskMover
private lateinit var adapter: TaskAdapter
private val tasks = ArrayList<TaskContainer>()
private val filter = runBlocking { MyTasksFilter.create() }
private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position]
override fun getTaskCount() = tasks.size
}
@Before
override fun setUp() {
super.setUp()
preferences.clear()
tasks.clear()
adapter = TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
adapter.setDataSource(dataSource)
}
@Test
@ -36,7 +57,7 @@ class OfflineSubtaskTest : InjectingTestCase() {
val parent = addTask()
val child = addTask(with(PARENT, parent))
val tasks = query()
query()
assertEquals(child, tasks[1].id)
assertEquals(parent, tasks[1].parent)
@ -49,81 +70,20 @@ class OfflineSubtaskTest : InjectingTestCase() {
val parent = addTask(with(PARENT, grandparent))
val child = addTask(with(PARENT, parent))
val tasks = query()
query()
assertEquals(child, tasks[2].id)
assertEquals(parent, tasks[2].parent)
assertEquals(2, tasks[2].indent)
}
@Test
fun parentWithOneChildHasChildrenCountOne() {
val parent = addTask()
addTask(with(PARENT, parent))
val tasks = query()
val parentTask = tasks.find { it.id == parent }!!
assertEquals(1, parentTask.children)
}
@Test
fun parentWithMultipleChildrenHasCorrectCount() {
val parent = addTask()
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
addTask(with(PARENT, parent))
val tasks = query()
val parentTask = tasks.find { it.id == parent }!!
assertEquals(3, parentTask.children)
}
@Test
fun grandparentCountsAllDescendants() {
val grandparent = addTask()
val parent = addTask(with(PARENT, grandparent))
addTask(with(PARENT, parent))
val tasks = query()
val grandparentTask = tasks.find { it.id == grandparent }!!
assertEquals(2, grandparentTask.children)
}
@Test
fun leafTaskHasNoChildren() {
val parent = addTask()
val child = addTask(with(PARENT, parent))
val tasks = query()
val childTask = tasks.find { it.id == child }!!
assertEquals(0, childTask.children)
}
@Test
fun deepHierarchyCountsAllDescendants() {
val root = addTask()
val level1 = addTask(with(PARENT, root))
val level2 = addTask(with(PARENT, level1))
val level3 = addTask(with(PARENT, level2))
addTask(with(PARENT, level3))
val tasks = query()
val rootTask = tasks.find { it.id == root }!!
assertEquals(4, rootTask.children)
}
private fun addTask(vararg properties: PropertyValue<in Task?, *>): Long = runBlocking {
val task = newTask(*properties)
taskDao.createNew(task)
task.id
}
private fun query(): List<TaskContainer> = runBlocking {
taskDao.fetchTasks(getQuery(preferences, filter))
private fun query() = runBlocking {
tasks.addAll(taskDao.fetchTasks { getQuery(preferences, filter) })
}
}
}

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

@ -1,6 +1,7 @@
package com.todoroo.astrid.alarms
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
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.Task
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class AlarmJobServiceTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao

@ -8,13 +8,16 @@ package com.todoroo.astrid.dao
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
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.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@ -11,17 +12,21 @@ import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.RemoteGtaskListMaker
import org.tasks.makers.RemoteGtaskListMaker.newRemoteList
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class GtasksListServiceTest : InjectingTestCase() {
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var caldavDao: CaldavDao
private lateinit var gtasksListService: GtasksListService
@ -29,7 +34,7 @@ class GtasksListServiceTest : InjectingTestCase() {
@Before
override fun setUp() {
super.setUp()
gtasksListService = GtasksListService(caldavDao, taskDeleter, localBroadcastManager)
gtasksListService = GtasksListService(googleTaskListDao, taskDeleter, localBroadcastManager)
}
@Test
@ -39,47 +44,48 @@ class GtasksListServiceTest : InjectingTestCase() {
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "Default")))
assertEquals(
CaldavCalendar(id = 1, account = "account", uuid = "1", name = "Default"),
caldavDao.getCalendarById(1L)
googleTaskListDao.getById(1L)
)
}
@Test
fun testGetListByRemoteId() = runBlocking {
val list = CaldavCalendar(uuid = "1")
caldavDao.insert(list)
assertEquals(list, caldavDao.getCalendarByUuid("1"))
list.id = googleTaskListDao.insertOrReplace(list)
assertEquals(list, googleTaskListDao.getByRemoteId("1"))
}
@Test
fun testGetListReturnsNullWhenNotFound() = runBlocking {
assertNull(caldavDao.getCalendarByUuid("1"))
assertNull(googleTaskListDao.getByRemoteId("1"))
}
@Test
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"))
setLists(taskList)
assertEquals(
listOf(CaldavCalendar(id = 2, account = "account", uuid = "2", name = "Default")),
caldavDao.getCalendarsByAccount("account")
googleTaskListDao.getLists("account")
)
}
@Test
fun testUpdateListName() = runBlocking {
val calendar = CaldavCalendar(uuid = "1", name = "oldName", account = "account")
caldavDao.insert(calendar)
googleTaskListDao.insertOrReplace(
CaldavCalendar(id = 1, uuid = "1", name = "oldName", account = "account")
)
setLists(
newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "newName")))
assertEquals("newName", caldavDao.getCalendarById(calendar.id)!!.name)
assertEquals("newName", googleTaskListDao.getById(1)!!.name)
}
@Test
fun testNewListLastSyncIsZero() = runBlocking {
setLists(TaskList().setId("1"))
assertEquals(0L, caldavDao.getCalendarByUuid("1")!!.lastSync)
assertEquals(0L, googleTaskListDao.getByRemoteId("1")!!.lastSync)
}
private suspend fun setLists(vararg list: TaskList) {

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

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

@ -8,14 +8,17 @@ package com.todoroo.astrid.service
import org.tasks.data.entity.Task
import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.data.dao.TagDataDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import java.util.*
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class QuickAddMarkupTest : InjectingTestCase() {
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.URGENCY_SPECIFIC_DAY
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
@ -15,10 +16,12 @@ import org.tasks.R
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.createDueDate
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TaskCreatorTest : InjectingTestCase() {
@Inject lateinit var preferences: Preferences

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

@ -1,8 +1,11 @@
package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.filters.CaldavFilter
import org.tasks.filters.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
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_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.filters.CaldavFilter
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.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
@ -26,10 +30,13 @@ import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TaskMoverTest : InjectingTestCase() {
@Inject lateinit var taskDaoAsync: TaskDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var workManager: WorkManager
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskMover: TaskMover
@ -56,7 +63,7 @@ class TaskMoverTest : InjectingTestCase() {
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(1, "account1")
val deleted = googleTaskDao.getDeletedByTaskId(1)
assertEquals(1, deleted.size.toLong())
assertEquals(1, deleted[0].task)
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, 2), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(2, "account1")
val deleted = googleTaskDao.getDeletedByTaskId(2)
assertEquals(1, deleted.size.toLong())
assertEquals(2, deleted[0].task)
assertTrue(deleted[0].deleted > 0)
@ -132,9 +139,9 @@ class TaskMoverTest : InjectingTestCase() {
createSubtask(2, 1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToCaldavList("2", 1)
moveToCaldavList("1", 1)
val task = caldavDao.getTask(2)
assertEquals("2", task!!.calendar)
assertEquals("1", task!!.calendar)
assertEquals(1L, taskDao.fetch(2)?.parent)
}
@ -181,7 +188,7 @@ class TaskMoverTest : InjectingTestCase() {
with(TASK, 3L),
with(CALENDAR, "1"),
with(REMOTE_PARENT, "b"))))
moveToGoogleTasks("2", 1)
moveToGoogleTasks("1", 1)
val task = taskDao.fetch(3L)
assertEquals(1L, task?.parent)
}
@ -249,7 +256,7 @@ class TaskMoverTest : InjectingTestCase() {
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
moveToGoogleTasks("1", 1)
assertTrue(googleTaskDao.getDeletedByTaskId(1, "account1").isEmpty())
assertTrue(googleTaskDao.getDeletedByTaskId(1).isEmpty())
assertEquals(1, googleTaskDao.getAllByTaskId(1).size.toLong())
}
@ -299,23 +306,11 @@ class TaskMoverTest : InjectingTestCase() {
}
private suspend fun moveToGoogleTasks(list: String, vararg tasks: Long) {
taskMover.move(
tasks.toList(),
CaldavFilter(
calendar = CaldavCalendar(uuid = list),
account = CaldavAccount(accountType = TYPE_GOOGLE_TASKS)
)
)
taskMover.move(tasks.toList(), GtasksFilter(CaldavCalendar(uuid = list)))
}
private suspend fun moveToCaldavList(calendar: String, vararg tasks: Long) {
taskMover.move(
tasks.toList(),
CaldavFilter(
CaldavCalendar(name = "", uuid = calendar),
account = CaldavAccount(accountType = TYPE_CALDAV)
)
)
taskMover.move(tasks.toList(), CaldavFilter(CaldavCalendar(name = "", uuid = calendar)))
}
private suspend fun setAccountType(account: String, type: Int) {

@ -5,45 +5,37 @@
*/
package com.todoroo.astrid.service
import org.tasks.data.entity.Task
import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import net.fortuna.ical4j.model.Recur.Frequency.DAILY
import net.fortuna.ical4j.model.Recur.Frequency.MONTHLY
import net.fortuna.ical4j.model.Recur.Frequency.WEEKLY
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertTrue
import net.fortuna.ical4j.model.Recur.Frequency.*
import org.junit.Assert.*
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.tasks.R
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Task
import org.tasks.data.newLocalAccount
import org.tasks.date.DateTimeUtils
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences
import org.tasks.repeats.RecurrenceUtils.newRecur
import java.util.Calendar
import java.util.*
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TitleParserTest : InjectingTestCase() {
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var caldavDao: CaldavDao
@Before
override fun setUp() {
runBlocking {
super.setUp()
preferences.setStringFromInteger(R.string.p_default_urgency_key, 0)
caldavDao.newLocalAccount()
}
super.setUp()
preferences.setStringFromInteger(R.string.p_default_urgency_key, 0)
}
/**

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

@ -2,12 +2,15 @@ package com.todoroo.astrid.subtasks
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.tasks.data.entity.TaskListMetadata
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class SubtasksHelperTest : SubtasksTestCase() {
@Before

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

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

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

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

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

@ -2,6 +2,7 @@ package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
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.Companion.ACCESS_READ_WRITE
import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class SharingMailboxDotOrgTest : CaldavTest() {

@ -2,6 +2,7 @@ package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
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_READ_ONLY
import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class SharingOwncloudTest : CaldavTest() {

@ -2,6 +2,7 @@ package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
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.INVITE_ACCEPTED
import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
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.PropertyValue
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
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.entity.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskContainerMaker
import org.tasks.makers.TaskContainerMaker.CREATED
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class CaldavDaoShiftTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao

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

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

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@ -10,11 +11,13 @@ import org.junit.Before
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.dao.GoogleTaskListDao
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.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
@ -23,8 +26,10 @@ import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class GoogleTaskDaoTests : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
@ -96,13 +101,13 @@ class GoogleTaskDaoTests : InjectingTestCase() {
@Test
fun getTaskFromRemoteId() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals(1L, googleTaskDao.getTask("1234", "calendar"))
assertEquals(1L, googleTaskDao.getTask("1234"))
}
@Test
fun getRemoteIdForTask() = runBlocking {
insert(newCaldavTask(with(REMOTE_ID, "1234")))
assertEquals("1234", googleTaskDao.getRemoteId(1L, "calendar"))
assertEquals("1234", googleTaskDao.getRemoteId(1L))
}
@Test
@ -180,21 +185,6 @@ class GoogleTaskDaoTests : InjectingTestCase() {
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
fun updateParents() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))
@ -256,7 +246,7 @@ class GoogleTaskDaoTests : InjectingTestCase() {
}
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) {
@ -278,6 +268,6 @@ class GoogleTaskDaoTests : InjectingTestCase() {
}
private suspend fun getByRemoteId(remoteId: String): CaldavTask {
return googleTaskDao.getByRemoteId(remoteId, "calendar")!!
return googleTaskDao.getByRemoteId(remoteId)!!
}
}

@ -1,16 +1,21 @@
package org.tasks.data
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class GoogleTaskListDaoTest : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var caldavDao: CaldavDao
@Test
@ -21,6 +26,6 @@ class GoogleTaskListDaoTest : InjectingTestCase() {
)
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.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@ -20,6 +21,7 @@ import org.tasks.data.entity.Place
import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DELETION_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 javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class LocationDaoTest : InjectingTestCase() {
@Inject lateinit var locationDao: LocationDao

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

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

@ -9,17 +9,20 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TaskDaoTests : InjectingTestCase() {

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
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.TagData
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class UpgraderDaoTests : InjectingTestCase() {

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

@ -10,6 +10,7 @@ import androidx.test.InstrumentationRegistry
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.After
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.ExportType
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences
import java.io.File
import java.io.IOException
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class BackupServiceTests : InjectingTestCase() {
@Inject lateinit var jsonExporter: TasksJsonExporter

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

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

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

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

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

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

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

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

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

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

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

@ -1,22 +1,25 @@
package org.tasks.ui.editviewmodel
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.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
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.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TitleTests : BaseTaskEditViewModelTest() {
@Test
fun changeTitleCausesChange() {
setup(newTask())
viewModel.setTitle("Test")
viewModel.title = "Test"
assertTrue(viewModel.hasChanges())
}
@ -26,7 +29,7 @@ class TitleTests : BaseTaskEditViewModelTest() {
val task = newTask()
setup(task)
viewModel.setPriority(HIGH)
viewModel.priority.value = HIGH
save()

@ -1,18 +1,19 @@
package org.tasks.billing
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import dagger.hilt.android.testing.UninstallModules
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class InventoryTest : InjectingTestCase() {
@ -23,24 +24,6 @@ class InventoryTest : InjectingTestCase() {
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
fun monthlyIsPro() {
withPurchases(monthly01)
@ -89,17 +72,13 @@ class InventoryTest : InjectingTestCase() {
}
.map(::Purchase)
preferences.setPurchases(asPurchases)
initInventory()
}
private fun initInventory() {
runOnMainSync {
inventory = Inventory(
context,
preferences,
signatureVerifier,
localBroadcastManager,
caldavDao
context,
preferences,
signatureVerifier,
localBroadcastManager,
caldavDao
)
}
}

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

@ -1,11 +1,14 @@
package org.tasks.opentasks
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.R
import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class OpenTasksSubscriptionTest : OpenTasksTest() {
@Test

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

@ -3,24 +3,40 @@ package org.tasks
import android.app.Application
import android.os.StrictMode
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 leakcanary.AppWatcher
import org.tasks.logging.FileLogger
import org.tasks.preferences.Preferences
import timber.log.Timber
import timber.log.Timber.DebugTree
import javax.inject.Inject
class BuildSetup @Inject constructor(
private val context: Application,
private val preferences: Preferences,
private val fileLogger: FileLogger,
) {
private val context: Application,
private val preferences: Preferences) {
fun setup() {
Timber.plant(Timber.DebugTree())
Timber.plant(fileLogger)
Timber.plant(DebugTree())
SoLoader.init(context, false)
if (preferences.getBoolean(R.string.p_leakcanary, false)) {
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)) {
val builder = StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog()
if (preferences.getBoolean(R.string.p_crash_main_queries, false)) {
@ -36,7 +52,9 @@ class BuildSetup @Inject constructor(
.detectLeakedClosableObjects()
.detectFileUriExposure()
.penaltyLog()
.detectContentUriWithoutPermission()
if (atLeastOreo()) {
builder.detectContentUriWithoutPermission()
}
if (atLeastQ()) {
builder
.detectCredentialProtectedWhileLocked()
@ -45,4 +63,4 @@ class BuildSetup @Inject constructor(
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
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import at.bitfire.cert4android.CustomCertManager.Companion.resetCertificates
import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.billing.BillingClient
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.injection.InjectingPreferenceFragment
import org.tasks.preferences.Preferences
@ -28,14 +24,13 @@ class Debug : InjectingPreferenceFragment() {
@Inject lateinit var inventory: Inventory
@Inject lateinit var billingClient: BillingClient
@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 suspend fun setupPreferences(savedInstanceState: Bundle?) {
for (pref in listOf(
R.string.p_leakcanary,
R.string.p_flipper,
R.string.p_strict_mode_vm,
R.string.p_strict_mode_thread,
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_tasker, Inventory.SKU_TASKER)
findPreference(R.string.debug_crash_app).setOnPreferenceClickListener {
throw RuntimeException("Crashed app from debug preferences")
@ -70,25 +66,7 @@ class Debug : InjectingPreferenceFragment() {
preferences.lastSubscribeRequest = 0L
preferences.lastReviewRequest = 0L
preferences.shownBeastModeHint = false
preferences.warnMicrosoft = true
preferences.warnGoogleTasks = true
preferences.warnQuietHoursDisabled = true
preferences.setBoolean(R.string.p_just_updated, 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_vm">Strict mode - VM</string>
<string name="debug_leakcanary">LeakCanary</string>
<string name="debug_flipper">Flipper</string>
<string name="debug_pro">Unlock pro</string>
<string name="debug_purchase">Purchase %s</string>
<string name="debug_consume">Consume %s</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_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_force_restart">Restart app</string>
<string name="debug_clear_hints">Clear hints</string>
<string name="google_oauth_scheme">com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf</string>
<string name="microsoft_oauth_path">/8wnYBRqh5nnQgFzbIXfxXSs41xE=</string>
</resources>

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

@ -1,24 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<manifest>
<application tools:ignore="MissingApplicationIcon">
<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>
<application/>
</manifest>

@ -1,40 +1,20 @@
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 java.util.concurrent.TimeUnit
import javax.inject.Inject
@Suppress("UNUSED_PARAMETER")
class Firebase @Inject constructor(
@param:ApplicationContext val context: Context,
private val preferences: Preferences
) {
class Firebase @Inject constructor() {
fun reportException(t: Throwable) = Timber.e(t)
fun updateRemoteConfig() {}
fun logEvent(event: Int, vararg params: Pair<Int, Any>) {
Timber.d("${context.getString(event)} -> $params")
}
fun logEvent(event: Int, vararg params: Pair<Int, Any>) {}
fun addTask(source: String) =
logEvent(R.string.event_add_task, R.string.param_type to source)
fun addTask(source: String) {}
fun completeTask(source: String) =
logEvent(R.string.event_complete_task, R.string.param_type to source)
fun completeTask(source: String) {}
val subscribeCooldown: Boolean
get() = installCooldown
|| preferences.lastSubscribeRequest + days(28L) > currentTimeMillis()
private val installCooldown: Boolean
get() = preferences.installDate + days(7L) > currentTimeMillis()
private fun days(default: Long): Long =
TimeUnit.DAYS.toMillis(default)
}
val subscribeCooldown = false
val subGroupA = false
}

@ -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 getSkus(skus: List<String>): List<Sku> = emptyList()
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>
</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>
</manifest>

@ -19,42 +19,12 @@ import javax.inject.Singleton
@Singleton
class Firebase @Inject constructor(
@ApplicationContext private val context: Context,
@param:ApplicationContext val context: Context,
private val preferences: Preferences
) {
private val crashlytics by lazy {
if (preferences.isTrackingEnabled) {
FirebaseCrashlytics.getInstance().apply {
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
}
}
private var crashlytics: FirebaseCrashlytics? = null
private var analytics: FirebaseAnalytics? = null
private var remoteConfig: FirebaseRemoteConfig? = null
fun reportException(t: Throwable) {
Timber.e(t)
@ -83,9 +53,7 @@ class Firebase @Inject constructor(
logEvent(R.string.event_complete_task, R.string.param_type to source)
fun logEvent(@StringRes event: Int, vararg p: Pair<Int, Any>) {
val eventName = context.getString(event)
Timber.d("$eventName -> $p")
analytics?.logEvent(eventName, Bundle().apply {
analytics?.logEvent(context.getString(event), Bundle().apply {
p.forEach {
val key = context.getString(it.first)
when (it.second::class) {
@ -107,6 +75,27 @@ class Firebase @Inject constructor(
get() = installCooldown
|| 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 =
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 com.android.billingclient.api.AcknowledgePurchaseParams
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.BillingClientStateListener
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.BillingResult
import com.android.billingclient.api.ConsumeParams
import com.android.billingclient.api.Purchase.PurchaseState
import com.android.billingclient.api.PurchasesResult
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.SkuDetailsParams
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
@ -44,63 +46,11 @@ class BillingClientImpl(
private var connected = false
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 {
executeServiceRequest {
withContext(Dispatchers.IO + NonCancellable) {
val subsParams = QueryPurchasesParams.newBuilder()
.setProductType(ProductType.SUBS)
.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))
}
}
val subs = billingClient.queryPurchasesAsync(SkuType.SUBS)
val iaps = billingClient.queryPurchasesAsync(SkuType.INAPP)
if (subs.success || iaps.success) {
withContext(Dispatchers.Main) {
inventory.clear()
@ -131,7 +81,7 @@ class BillingClientImpl(
purchases?.forEach {
firebase.reportIabResult(
result.responseCodeString,
it.products.joinToString(","),
it.skus.joinToString(","),
it.purchaseState.purchaseStateString
)
}
@ -148,57 +98,31 @@ class BillingClientImpl(
oldPurchase: Purchase?
) {
executeServiceRequest {
val productList = listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId(sku)
.setProductType(skuType)
.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)
}
}
val skuDetailsResult = withContext(Dispatchers.IO) {
billingClient.querySkuDetails(
SkuDetailsParams.newBuilder().setSkusList(listOf(sku)).setType(skuType)
.build()
)
}
productDetailsResult.first.let {
skuDetailsResult.billingResult.let {
if (!it.success) {
throw IllegalStateException(it.responseCodeString)
}
}
val productDetails = productDetailsResult.second?.firstOrNull()
?: throw IllegalStateException("Product $sku not found")
val productDetailsParamsBuilder = ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
// 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))
val skuDetails =
skuDetailsResult
.skuDetailsList
?.firstOrNull()
?: throw IllegalStateException("Sku $sku not found")
val params = BillingFlowParams.newBuilder().setSkuDetails(skuDetails)
oldPurchase?.let {
params.setSubscriptionUpdateParams(
SubscriptionUpdateParams.newBuilder()
.setOldPurchaseToken(it.purchaseToken)
.setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION)
.setOldSkuPurchaseToken(it.purchaseToken)
.setReplaceSkusProrationMode(ProrationMode.IMMEDIATE_WITH_TIME_PRORATION)
.build()
)
}
if (activity is OnPurchasesUpdated) {
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) {
connect()
}
return runnable()
runnable()
}
override suspend fun consume(sku: String) {
@ -266,28 +190,17 @@ class BillingClientImpl(
ConsumeParams.newBuilder().setPurchaseToken(purchase.purchaseToken).build(),
)
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 {
const val TYPE_SUBS = ProductType.SUBS
const val TYPE_SUBS = SkuType.SUBS
const val STATE_PURCHASED = PurchaseState.PURCHASED
private val PurchasesResult.success: Boolean
get() = billingResult.responseCode == BillingResponseCode.OK
private val BillingResult.success: Boolean
get() = responseCode == BillingResponseCode.OK
@ -314,5 +227,11 @@ class BillingClientImpl(
PurchaseState.PENDING -> "PENDING"
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
val sku: String
get() = purchase.products.first()
get() = purchase.skus.first()
val purchaseToken: String
get() = purchase.purchaseToken
@ -55,7 +55,7 @@ class Purchase(private val purchase: Purchase) {
get() {
val matcher = PATTERN.matcher(sku)
if (matcher.matches()) {
val price = matcher.group(2)?.toInt()
val price = matcher.group(2).toInt()
return if (price == 499) 5 else price
}
return null

@ -2,7 +2,6 @@ package org.tasks.location
import android.annotation.SuppressLint
import android.content.Context
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
@ -24,18 +23,14 @@ class GoogleMapFragment @Inject constructor(
private var map: GoogleMap? = 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.dark = dark
val fragmentManager = activity.supportFragmentManager
var mapFragment = fragmentManager.findFragmentByTag(FRAG_TAG_MAP) as SupportMapFragment?
if (mapFragment == null) {
mapFragment = SupportMapFragment()
if (parent == null) {
fragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit()
} else {
fragmentManager.beginTransaction().add(parent, mapFragment, null).commit()
}
fragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit()
}
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 {
phoneDataLayerAppHelper
.connectedAndInstalledNodes
.catch { Timber.e("${it.message}") }
.catch { Timber.e(it) }
.onEach { nodes ->
Timber.d("Connected nodes: ${nodes.joinToString()}")
watchConnected = nodes.isNotEmpty()

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

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="android_wear_capabilities" tools:ignore="UnusedResources">
<resources>
<string-array name="android_wear_capabilities">
<item>data_layer_app_helper_device_phone</item>
<item>horologist_phone</item>
</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"?>
<resources>
<string name="google_oauth_scheme">com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif</string>
<string name="microsoft_oauth_path">/sEe08kX5nGJi4miFX3VkNXICC/Y=</string>
</resources>

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

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

@ -39,14 +39,32 @@ object AndroidUtilities {
return (dp * displayMetrics.density + 0.5f).toInt()
}
fun preOreo(): Boolean {
return !atLeastOreo()
}
fun preS(): Boolean {
return !atLeastS()
}
@JvmStatic
fun preTiramisu(): Boolean {
return !atLeastTiramisu()
}
fun preUpsideDownCake(): Boolean {
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 {
return Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1
}
@ -75,14 +93,6 @@ object AndroidUtilities {
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() {
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.os.Bundle;
import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.activity.EdgeToEdge;
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.RecyclerView;
@ -88,24 +84,12 @@ public class BeastModePreferences extends ThemedInjectingAppCompatActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
BeastModePrefActivityBinding binding = BeastModePrefActivityBinding.inflate(getLayoutInflater());
Toolbar toolbar = binding.toolbar.toolbar;
RecyclerView recyclerView = binding.recyclerView;
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(
getDrawable(R.drawable.ic_outline_arrow_back_24px));
toolbar.setNavigationOnClickListener(v -> finish());

@ -5,88 +5,93 @@
*/
package com.todoroo.astrid.activity
import android.app.ActivityOptions
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import android.view.View
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
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.foundation.layout.Arrangement
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.IntentCompat.getParcelableExtra
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import androidx.lifecycle.repeatOnLifecycle
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.AndroidUtilities.atLeastR
import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment
import com.todoroo.astrid.adapter.SubheaderClickHandler
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity
import com.todoroo.astrid.service.TaskCreator
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.runBlocking
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
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.auth.SignInActivity
import org.tasks.billing.Inventory
import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.compose.AddAccountDestination
import org.tasks.compose.HomeDestination
import org.tasks.compose.accounts.AddAccountScreen
import org.tasks.compose.accounts.AddAccountViewModel
import org.tasks.compose.home.HomeScreen
import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.drawer.DrawerAction
import org.tasks.compose.drawer.DrawerItem
import org.tasks.compose.drawer.MenuSearchBar
import org.tasks.compose.drawer.TaskListDrawer
import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.LocationDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Place
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.etebase.EtebaseAccountSettingsActivity
import org.tasks.dialogs.WhatsNewDialog
import org.tasks.extensions.Context.findActivity
import org.tasks.extensions.Context.nightMode
import org.tasks.extensions.Context.toast
import org.tasks.extensions.broughtToFront
import org.tasks.extensions.flagsToString
import org.tasks.extensions.isFromHistory
import org.tasks.files.FileHelper
import org.tasks.extensions.Context.openUri
import org.tasks.extensions.hideKeyboard
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.HelpAndFeedback
import org.tasks.preferences.MainPreferences
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.TasksTheme
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 javax.inject.Inject
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var preferences: Preferences
@ -99,209 +104,286 @@ class MainActivity : AppCompatActivity() {
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var eventBus: MainActivityEventBus
@Inject lateinit var firebase: Firebase
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var workManager: WorkManager
private val viewModel: MainActivityViewModel by viewModels()
private var currentNightMode = 0
private var currentPro = false
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?) {
super.onCreate(savedInstanceState)
theme.themeBase.set(this)
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !isReady }
theme.applyTheme(this)
currentNightMode = nightMode
currentPro = inventory.hasPro
binding = TaskListActivityBinding.inflate(layoutInflater)
setContentView(binding.root)
logIntent("onCreate")
handleIntent()
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
lightScrim = Color.TRANSPARENT,
darkScrim = Color.TRANSPARENT
),
navigationBarStyle = if (theme.themeBase.isDarkTheme(this)) {
SystemBarStyle.dark(Color.TRANSPARENT)
} else {
SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT)
}
)
setContent {
TasksTheme(
theme = theme.themeBase.index,
primary = theme.themeColor.primaryColor,
) {
val navController = rememberNavController()
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()
}
binding.composeView.setContent {
if (viewModel.drawerOpen.collectAsStateWithLifecycle().value) {
TasksTheme {
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
confirmValueChange = { true },
)
ModalBottomSheet(
sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.surface,
onDismissRequest = { viewModel.closeDrawer() },
) {
val state = viewModel.state.collectAsStateWithLifecycle().value
val context = LocalContext.current
val settingsRequest = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
context.findActivity()?.recreate()
}
val addAccountViewModel: AddAccountViewModel = hiltViewModel()
val microsoftVM: MicrosoftSignInViewModel = hiltViewModel()
val syncLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
syncAdapters.sync(true)
workManager.updateBackgroundSync()
} else {
result.data
?.getStringExtra(GtasksLoginActivity.EXTRA_ERROR)
?.let { toast(it) }
}
}
val importBackupLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
ImportTasksDialog.newImportTasksDialog(uri)
.show(supportFragmentManager, FRAG_TAG_IMPORT_TASKS)
val scope = rememberCoroutineScope()
val bottomSearchBar = atLeastR()
TaskListDrawer(
arrangement = when {
state.menuQuery.isBlank() -> Arrangement.Top
bottomSearchBar -> Arrangement.Bottom
else -> Arrangement.Top
},
bottomSearchBar = bottomSearchBar,
filters = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems,
onClick = {
when (it) {
is DrawerItem.Filter -> {
viewModel.setFilter(it.filter)
scope.launch(Dispatchers.Default) {
sheetState.hide()
viewModel.closeDrawer()
}
}
is DrawerItem.Header -> {
viewModel.toggleCollapsed(it.header)
}
}
}
AddAccountScreen(
gettingStarted = route.showImport,
hasTasksAccount = inventory.hasTasksAccount,
hasPro = inventory.hasPro,
onBack = { navController.popBackStack() },
signIn = { platform ->
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to platform)
when (platform) {
AddAccountDialog.Platform.TASKS_ORG ->
syncLauncher.launch(
Intent(this@MainActivity, SignInActivity::class.java)
)
AddAccountDialog.Platform.GOOGLE_TASKS ->
syncLauncher.launch(
Intent(this@MainActivity, GtasksLoginActivity::class.java)
)
AddAccountDialog.Platform.MICROSOFT ->
microsoftVM.signIn(this@MainActivity)
AddAccountDialog.Platform.CALDAV ->
syncLauncher.launch(
Intent(this@MainActivity, CaldavAccountSettingsActivity::class.java)
)
AddAccountDialog.Platform.ETESYNC ->
syncLauncher.launch(
Intent(this@MainActivity, EtebaseAccountSettingsActivity::class.java)
)
AddAccountDialog.Platform.LOCAL ->
addAccountViewModel.createLocalAccount()
else -> throw IllegalArgumentException()
},
onAddClick = {
scope.launch(Dispatchers.Default) {
sheetState.hide()
viewModel.closeDrawer()
when (it.header.addIntentRc) {
FilterProvider.REQUEST_NEW_FILTER ->
NewFilterDialog.newFilterDialog().show(
supportFragmentManager,
SubheaderClickHandler.FRAG_TAG_NEW_FILTER
)
REQUEST_NEW_PLACE ->
startActivityForResult(
Intent(
this@MainActivity,
LocationPickerActivity::class.java
),
REQUEST_NEW_PLACE
)
REQUEST_NEW_TAGS ->
startActivityForResult(
Intent(
this@MainActivity,
TagSettingsActivity::class.java
),
REQUEST_NEW_LIST
)
REQUEST_NEW_LIST -> lifecycleScope.launch {
val account =
caldavDao.getAccount(it.header.id) ?: return@launch
when (it.header.subheaderType) {
NavigationDrawerSubheader.SubheaderType.GOOGLE_TASKS ->
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 ->
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to platform.name)
addAccountViewModel.openUrl(this@MainActivity, platform)
onErrorClick = {
context.startActivity(Intent(context, MainPreferences::class.java))
},
onImportBackup = {
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to "import_backup")
importBackupLauncher.launch(
FileHelper.newFilePickerIntent(this@MainActivity, preferences.backupDirectory),
searchBar = {
MenuSearchBar(
begForMoney = state.begForMoney,
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 =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
BackHandler(enabled = state.task == null) {
Timber.d("onBackPressed")
if (isDetailVisible && navigator.canNavigateBack()) {
scope.launch {
navigator.navigateBack()
}
} else {
finish()
if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) {
runBlocking {
viewModel.resetFilter()
}
eventBus
.onEach(this::process)
.launchIn(lifecycleScope)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
updateSystemBars(viewModel.state.value.filter)
}
}
viewModel
.state
.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) {
actionMode?.finish()
actionMode = null
if (state.task == null) {
keyboard?.hide()
}
} else if (task != existingTef?.task) {
existingTef?.save(remove = false)
transaction
.replace(R.id.detail, newTaskEditFragment(task), FRAG_TAG_TASK_EDIT)
.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) {
@ -311,6 +393,12 @@ class MainActivity : AppCompatActivity() {
handleIntent()
}
private fun clearUi() {
actionMode?.finish()
actionMode = null
viewModel.closeDrawer()
}
private suspend fun getTaskToLoad(filter: Filter?): Task? = when {
intent.isFromHistory -> null
intent.hasExtra(CREATE_TASK) -> {
@ -333,13 +421,16 @@ class MainActivity : AppCompatActivity() {
private fun logIntent(caller: String) {
if (BuildConfig.DEBUG) {
Timber.d("""
|$caller
|**********
|broughtToFront: ${intent.broughtToFront}
|isFromHistory: ${intent.isFromHistory}
|flags: ${intent.flagsToString}
${intent?.extras?.keySet()?.joinToString("\n") { "|$it: ${intent.extras?.get(it)}" } ?: "|NO EXTRAS"}
|**********""".trimMargin()
$caller
**********
broughtToFront: ${intent.broughtToFront}
isFromHistory: ${intent.isFromHistory}
flags: ${intent.flagsToString}
OPEN_FILTER: ${getParcelableExtra(intent, OPEN_FILTER, Filter::class.java)?.let { "${it.title}: $it" }}
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() {
super.onResume()
Timber.d("onResume")
if (currentNightMode != nightMode || currentPro != inventory.hasPro) {
restartActivity()
recreate()
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() {
super.onPause()
Timber.d("onPause")
private suspend fun newTaskEditFragment(task: Task): TaskEditFragment {
AndroidUtilities.assertMainThread()
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) {
super.onSupportActionModeStarted(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 {
/** For indicating the new list screen should be launched at fragment setup time */
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 REMOVE_TASK = "remove_task"
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?
get() = if (isFromHistory) {
@ -415,17 +540,35 @@ class MainActivity : AppCompatActivity() {
}
val Intent.removeTask: Boolean
get() = try {
getBooleanExtra(REMOVE_TASK, false) && !isFromHistory && !broughtToFront
} finally {
removeExtra(REMOVE_TASK)
get() = if (isFromHistory) {
false
} else {
getBooleanExtra(REMOVE_TASK, false).let {
removeExtra(REMOVE_TASK)
it
}
}
val Intent.finishAffinity: Boolean
get() = try {
getBooleanExtra(FINISH_AFFINITY, false) && !isFromHistory && !broughtToFront
} finally {
removeExtra(FINISH_AFFINITY)
get() = if (isFromHistory) {
false
} else {
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.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
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.launch
import kotlinx.coroutines.runBlocking
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.compose.drawer.DrawerItem
import org.tasks.compose.throttleLatest
import org.tasks.data.NO_COUNT
import org.tasks.data.count
import org.tasks.data.dao.CaldavDao
@ -39,18 +32,16 @@ import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.SearchFilter
import org.tasks.filters.getIcon
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.TasksPreferences
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.themes.ColorProvider
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class MainActivityViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
savedStateHandle: SavedStateHandle,
private val defaultFilterProvider: DefaultFilterProvider,
private val filterProvider: FilterProvider,
private val taskDao: TaskDao,
@ -71,7 +62,7 @@ class MainActivityViewModel @Inject constructor(
)
private val _drawerOpen = MutableStateFlow(false)
private val _updateFilters = MutableStateFlow(0L)
val drawerOpen = _drawerOpen.asStateFlow()
private val _state = MutableStateFlow(
State(
@ -81,22 +72,15 @@ class MainActivityViewModel @Inject constructor(
}
?: runBlocking { defaultFilterProvider.getStartupFilter() },
begForMoney = if (IS_GENERIC) !inventory.hasTasksAccount else !inventory.hasPro,
task = savedStateHandle.get<Task>(EXTRA_TASK),
)
)
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() {
override fun onReceive(context: Context?, intent: Intent?) {
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) {
return
}
savedStateHandle[EXTRA_TASK] = task
_state.update {
it.copy(
filter = filter,
@ -120,37 +103,28 @@ class MainActivityViewModel @Inject constructor(
)
}
updateFilters()
if (filter !is SearchFilter) {
defaultFilterProvider.setLastViewedFilter(filter)
}
defaultFilterProvider.setLastViewedFilter(filter)
}
fun setDrawerState(opened: Boolean) {
_drawerOpen.update { opened }
if (!opened) {
_state.update { it.copy(menuQuery = "") }
}
fun closeDrawer() {
_drawerOpen.update { false }
_state.update { it.copy(menuQuery = "") }
}
fun openDrawer() {
_drawerOpen.update { true }
}
init {
localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
_updateFilters
.onStart { updateFilters() }
.combine(_drawerOpen) { timestamp, drawerOpen ->
if (drawerOpen) timestamp else null
}
.filterNotNull()
.throttleLatest(1000)
.onEach { updateFilters() }
.launchIn(viewModelScope)
updateFilters()
}
override fun onCleared() {
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
private fun updateFilters() = viewModelScope.launch(Dispatchers.IO) {
fun updateFilters() = viewModelScope.launch(Dispatchers.Default) {
val selected = state.value.filter
filterProvider
.drawerItems()
@ -221,18 +195,18 @@ class MainActivityViewModel @Inject constructor(
when (subheader.subheaderType) {
NavigationDrawerSubheader.SubheaderType.PREFERENCE -> {
tasksPreferences.set(booleanPreferencesKey(subheader.id), collapsed)
localBroadcastManager.broadcastRefresh()
localBroadcastManager.broadcastRefreshList()
}
NavigationDrawerSubheader.SubheaderType.GOOGLE_TASKS,
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS -> {
caldavDao.setCollapsed(subheader.id, collapsed)
localBroadcastManager.broadcastRefresh()
localBroadcastManager.broadcastRefreshList()
}
}
}
fun setTask(task: Task?) {
savedStateHandle[EXTRA_TASK] = task
_state.update { it.copy(task = task) }
}
@ -240,10 +214,4 @@ class MainActivityViewModel @Inject constructor(
_state.update { it.copy(menuQuery = query) }
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