Compare commits

...

522 Commits
13.5.1 ... main

Author SHA1 Message Date
Alex Baker 30cb374a21 Revert "chore(deps): update actions/cache action to v4"
This reverts commit f8bb045d76.
4 days ago
Alex Baker b09a8967e4 Merge tag '13.9.9' 4 days ago
Alex Baker 39b56296bd Replace lifecycle-*-ktx deps 4 days ago
Alex Baker 1d8d2efce6 Update version and changelog 1 week ago
Alex Baker c9af39b6ba Fix import backup crashes 1 week ago
renovate[bot] f8bb045d76 chore(deps): update actions/cache action to v4 1 week ago
renovate[bot] 4040a2379b fix(deps): update dependency com.google.android.gms:play-services-oss-licenses to v17.1.0 1 week ago
Alex Baker c4ee7479ca
Cache avd 1 week ago
Alex Baker be861597ef Merge branch '13.9.8' 1 week ago
Alex Baker f27332595d Set root project name for project accessors 1 week ago
Alex Baker ea7f051d85 Revert "Restore @Transaction annotations"
This reverts commit b35090cd43.
1 week ago
Alex Baker 8be7fab033 Update version and changelog 1 week ago
Alex Baker d6e0c0bdcf Fix showing completed tasks in subtask filter 1 week ago
Alex Baker 5ec02011f8 Fix backup import crashes 1 week ago
Alex Baker b35090cd43 Restore @Transaction annotations 1 week ago
renovate[bot] 92f62450ae fix(deps): update accompanist to v0.34.0 1 week ago
renovate[bot] 3ca6912492 fix(deps): update dependency androidx.appcompat:appcompat to v1.7.0 1 week ago
renovate[bot] 080b1428dd fix(deps): update room to v2.7.0-alpha03 1 week ago
Alex Baker f67c3bc56c Enable typesafe project accessors 1 week ago
renovate[bot] 5d0e88a620 fix(deps): update dependency com.google.android.gms:play-services-location to v21.3.0 1 week ago
renovate[bot] c5d5795fe2 fix(deps): update lifecycle to v2.8.1 1 week ago
renovate[bot] 3bbc0e0ab0 fix(deps): update dependency androidx.sqlite:sqlite-bundled to v2.5.0-alpha03 1 week ago
Alex Baker 009a195580 Update plugin definitions 1 week ago
renovate[bot] 772f69d8c0 fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20240521-2.0.0 1 week ago
renovate[bot] 4229bf7067 fix(deps): update dependency com.google.apis:google-api-services-tasks to v1-rev20240526-2.0.0 1 week ago
Alex Baker 212a4b0a3d Delete transaction check
This was using platform sqlite
1 week ago
Alex Baker 4ddfe937b0 Finish converting data module to kmp 2 weeks ago
Alex Baker 19de0e08a5 Migrate to bundled sqlite 2 weeks ago
Alex Baker 60211355e0 Remove Geofence constructors 2 weeks ago
Alex Baker 17d218aa4e Add CommonParcelize 2 weeks ago
Alex Baker 505c8c29d5 Make sure dao methods are suspending 2 weeks ago
Alex Baker 7149308c97 Move Android platform stuff out of data 2 weeks ago
Alex Baker 2c5a497007 Fix backup import crash 2 weeks ago
Alex Baker 09f53fe1e5 Ignore multiplatform agp warnings 2 weeks ago
Alex Baker 5da4183aed Move ksp to gradle catalog 2 weeks ago
Alex Baker d35912e503 Kotlin 2.0 2 weeks ago
renovate[bot] 82fd99f83e fix(deps): update lifecycle to v2.8.0 2 weeks ago
renovate[bot] f944becea1 fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20240509-2.0.0 2 weeks ago
Alex Baker acd713dc5b AGP 8.4.1 2 weeks ago
Alex Baker 1a93c87ad9 Update version and changelog 2 weeks ago
Alex Baker c4e25b8b15 Fix tests 2 weeks ago
Alex Baker e11c0d2528 Add default reminders when adding due/start date 2 weeks ago
Alex Baker 2fc6833854 Don't crash on missing vtodo value 2 weeks ago
Alex Baker 4a2fb13d10 Converting data module to kmp - WIP 3 weeks ago
Alex Baker a2572e2dee Remove CaldavCalendarMaker 3 weeks ago
Alex Baker 64e05c9f8f Convert Tag to data class 3 weeks ago
Alex Baker ad833b5f49 Convert TagData to data class 3 weeks ago
Alex Baker eea944cc7b Update version and changelog 3 weeks ago
Alex Baker c82dfc7d39 Fix test? 3 weeks ago
Alex Baker 8607f9556a Fix test 3 weeks ago
Alex Baker f338e84d46 Fix warnings in Migrations 3 weeks ago
Alex Baker 9ee739627e Remove AlarmEntry 3 weeks ago
Alex Baker a49c233584 Make notification immutable 3 weeks ago
Alex Baker 74fca07c1b Make alarm immutable 3 weeks ago
Alex Baker 5bd0cef42e Remove extra alarm constructor 3 weeks ago
Alex Baker 4c245edbb4 Fix snooze causing duplicate notifications 3 weeks ago
Alex Baker 97a3f074d0 Update alarms after completion transaction 3 weeks ago
Alex Baker 86ecd3cf81 Synchronize alarms before saving 3 weeks ago
Alex Baker 07a2eda5ea Cancel notifications in TaskCompleter 3 weeks ago
renovate[bot] 09ffbdd036 fix(deps): update dependency com.google.firebase:firebase-crashlytics-gradle to v3 3 weeks ago
renovate[bot] 60f22146ca fix(deps): update dependency androidx.fragment:fragment-ktx to v1.7.1 3 weeks ago
renovate[bot] c11225abaf fix(deps): update kotlin 3 weeks ago
dependabot[bot] 133ea493e3 Bump rexml from 3.2.6 to 3.2.8
Bumps [rexml](https://github.com/ruby/rexml) from 3.2.6 to 3.2.8.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.2.6...v3.2.8)

---
updated-dependencies:
- dependency-name: rexml
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
3 weeks ago
Alex Baker 0ba901be69 Remove livedata from data module 4 weeks ago
Alex Baker ebe5e5c009 Replace gson with kotlin serialization 4 weeks ago
Alex Baker d556863fda Use kotlin serialization for backups 4 weeks ago
Alex Baker 55adbc2025 Reorganized data module 4 weeks ago
Alex Baker 06c4255886 Remove androidx.core from data module 4 weeks ago
renovate[bot] 4734a99bae fix(deps): update mockito monorepo to v5.12.0 4 weeks ago
Alex Baker a6a8cac8e4 Update dependencies 4 weeks ago
Alex Baker c3fc9a57cc Replace now with currentTimeMillis 4 weeks ago
Alex Baker 6e14d07d0c Move Room to data module 4 weeks ago
Alex Baker 6118121698 Moving some code out of TimerPlugin 4 weeks ago
Alex Baker 6bf3bd4d08 Update version and changelog 4 weeks ago
Alex Baker 065be79355 Update notification work logic 4 weeks ago
Alex Baker f8f8ba3c51 Don't adjust random reminder time 4 weeks ago
Alex Baker 89465f36b3 Update version and changelog 1 month ago
Alex Baker 1380a34ffa Fix alarm test 1 month ago
Alex Baker 10af5280a3 Fix random reminders 1 month ago
Alex Baker 8c0f7b952d ForegroundInfo for expedited work on Android 11- 1 month ago
Alex Baker 65362b203f Update version and changelog 1 month ago
Alex Baker 3327f97a17 Revert change to not delete evicted notifications 1 month ago
Alex Baker c9fc02a42e Enable room kotlin codegen 1 month ago
renovate[bot] 93670bb9e4 fix(deps): update dependency com.google.firebase:firebase-bom to v33 1 month ago
Alex Baker 1fc6a50d0b Update version and changelog 1 month ago
Alex Baker e1ef924909 Revert "Load initial data in task edit view model"
This reverts commit b2efb42d55.
1 month ago
Alex Baker 686cb5d346 Add empty filter 1 month ago
renovate[bot] ebec25c4cb fix(deps): update dependency com.google.android.material:material to v1.12.0 1 month ago
renovate[bot] c140f7e673
fix(deps): update dependency com.squareup.leakcanary:leakcanary-android to v2.14 1 month ago
renovate[bot] efbcf11a4a fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20240327-2.0.0 1 month ago
renovate[bot] 6adee85a37 fix(deps): update dependency com.google.apis:google-api-services-tasks to v1-rev20240423-2.0.0 1 month ago
Alex Baker 5c8643110b Update back press and intent handling 1 month ago
Alex Baker abd13aeb75 Exclude META-INF/INDEX.LIST 1 month ago
Alex Baker c210fe1893 Fix finishing recurrence 1 month ago
Alex Baker 26aa916c20 Fix widget crash 1 month ago
renovate[bot] 1eff2d1cd5 fix(deps): update dependency androidx.fragment:fragment-ktx to v1.7.0 1 month ago
islam2hamy c90e683ea3
Translated using Weblate (Arabic)
Currently translated at 94.1% (626 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ar/
1 month ago
Alex Baker 3cd0295b71 Refactor notification scheduling
* Remove foreground service
* Use expedited work to trigger notifications
* Remove miscellaneous notification channel
1 month ago
Alex Baker 95c351e9fd Remove midnight refresh worker 1 month ago
renovate[bot] 4ddb7816f1 fix(deps): update dependency androidx.compose:compose-bom to v2024.05.00 1 month ago
renovate[bot] 91c30f7bbf chore(deps): update dependency gradle to v8.7 1 month ago
renovate[bot] 3f4398b6e0 chore(deps): update dependency fastlane to v2.220.0 1 month ago
renovate[bot] c822e989a3 fix(deps): update dependency androidx.compose.compiler:compiler to v1.5.13 1 month ago
renovate[bot] da146723e5 chore(deps): update dependency ruby to v3.3.1 1 month ago
109247019824 931626c84a Translated using Weblate (Bulgarian)
Currently translated at 100.0% (665 of 665 strings)

Co-authored-by: 109247019824 <stoyan@gmx.com>
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
Translation: Tasks.org/Android
1 month ago
Alex Baker c534632c52 Pass uuid to TaskAdapter.onCompletedTask 1 month ago
Alex Baker c1347a7455 Update version and changelog 1 month ago
renovate[bot] 9544909a58 Update dependency androidx.activity:activity-compose to v1.9.0 1 month ago
Yurt Page 5c10dce2b9 fastlane: i18n ru
Signed-off-by: Yurt Page <yurtpage@gmail.com>
1 month ago
Alex Baker 584d4a5cbb Move after update work inside transaction 1 month ago
Alex Baker 7c68a7fa59 AGP 8.4.0 1 month ago
purushottamyadavbattula 215cc838ef Sending local broadcast refresh event for refreshing nav drawer menu to communicate about update events 1 month ago
Alex Baker d60472d1bc Remove RefreshScheduler 1 month ago
Alex Baker f84a37a60a Revert "Replace refresh work with coroutines"
Widgets 😢
1 month ago
Alex Baker 7fb85b6da1 Replace refresh work with coroutines 1 month ago
Alex Baker dc90e583e4 Fix hiding empty items in drawer 1 month ago
Don Zouras 0eac5f61eb Translated using Weblate (Esperanto)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
2 months ago
Milo Ivir c686ce883d Translated using Weblate (Croatian)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hr/
2 months ago
大王叫我来巡山 ab25398cd0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (665 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
2 months ago
renovate[bot] 3b1c133d22 Update kotlin 2 months ago
renovate[bot] 3bfd0ab4f8 Update dependency com.google.firebase:firebase-bom to v32.8.1 2 months ago
Liz de Sartiges ffc0113d7f Initial support for z flip 5 cover screen
see : https://developer.samsung.com/galaxy-z/flex_window.html
2 months ago
Don Zouras 9de9718ad5 Translated using Weblate (Esperanto)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
2 months ago
Oğuz Ersen a7d2c9c406 Translated using Weblate (Turkish)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/tr/
2 months ago
gallegonovato b3006b9ac2 Translated using Weblate (Spanish)
Currently translated at 100.0% (665 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
2 months ago
Don Zouras de3ef1f9c9 Translated using Weblate (Esperanto)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
2 months ago
Alex Baker ce9e722a3f Delete more unused tag picker code 2 months ago
Alex Baker 4b892a0eb1 Rename to TagPickerActivity
Delete some more unused code
2 months ago
Hady e6e275834a
Tag picker compose (#2849)
TagPickerActivity refactoring to Compose

1. state of the SearchBar moved to the viewModel
2. viewModel used as parameter to @Composables instead of number of separate ones
3. Import of TagPickerActivity is replaced by TagPickerActivityCompose through the project
2 months ago
Alex Baker 782f4d6d7c Fix swipe to snooze time 2 months ago
elmuffo a1da71d3e1
Swipe to snooze (#2839) 2 months ago
Alex Baker c793a300cc Add preference summary 2 months ago
Ilya Bizyaev bf84bf9e82 [Feature] Add an option to allow adding tasks without unlock
I often find myself picking up the phone just to write down a task, so
I've added a notification drawer quick setting to speed things up.
However, when I use this button from the lock screen, I have to unlock
my device first, which is annoying. I would like to be able to add (not
view) tasks without the need to unlock my phone.

This PR adds such an optional feature for devices running Android 8.1+.
Note that I am not an Android developer, so the implementation is
probably not perfect. However, from my testing on an emulator, this
code seems to do just what I want.
2 months ago
SC 363b29babb
Translated using Weblate (Portuguese)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt/
2 months ago
min7-i c1ff953f5c Translated using Weblate (German)
Currently translated at 99.3% (651 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
2 months ago
Alex Baker 63482e5db9 AGP 8.3.2 2 months ago
Emin Tufan Çetin 2f7dc0c7f1
Translated using Weblate (Turkish)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/tr/
2 months ago
Lionel HANNEQUIN d672507fae
Translated using Weblate (French)
Currently translated at 99.8% (654 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fr/
2 months ago
Jonatan Nyberg ce2a3c8a3f
Translated using Weblate (Swedish)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sv/
2 months ago
sorifukobexomajepasiricupuva33 9cd114d68b
Translated using Weblate (German)
Currently translated at 99.2% (650 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
2 months ago
Patrick V. Leguizamon 0e663f0e08
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
2 months ago
Mayhm 1d1efd008d
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
2 months ago
Alex Baker 26ab3d5866 Exclude past snooze times from Snooze Filter
This should exclude tasks that were completed before their snooze time
lapsed
2 months ago
Mayhm 9a4fcbbd39
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
2 months ago
Alex Baker 72bfda9224 Fix subtasks row for new tasks 2 months ago
Alex Baker 1067de4183 Emit SectionedDataSource from TaskListViewModel 2 months ago
Alex Baker d686b8c7e0 Add TasksMenu composable 3 months ago
Alex Baker b2efb42d55 Load initial data in task edit view model 3 months ago
Fabio Parri 3448808c94 Translated using Weblate (Portuguese)
Currently translated at 99.3% (651 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt/
3 months ago
Alex Baker 06a9626052 Update version and changelog 3 months ago
Alex Baker e92ab7f7e1 Update to latest ModalBottomSheet 3 months ago
Alex Baker 4ff7b18c0f Fix cloning google tasks 3 months ago
Alex Baker 91887f6b17 Fix backup import dropping some tasks 3 months ago
Alex Baker cf30b56098 Update version and changelog 3 months ago
Alex Baker 9bcadaab5a Fix astrid manual ordering crash in widget 3 months ago
Alex Baker be766074b0 Fix activity crash 3 months ago
Ihor Hordiichuk 64a42a3f61 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/uk/
3 months ago
Mayhm 7b65ba6f06 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.6% (646 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
3 months ago
109247019824 ac2b270e9e Translated using Weblate (Bulgarian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
3 months ago
Alex Baker db2ea0a039 Fix import crash on missing remoteId 3 months ago
renovate[bot] 08b78fe9f4 Update dependency androidx.compose:compose-bom to v2024.03.00 3 months ago
Alex Baker 1a1301ae3e Update version and changelog 3 months ago
Milo Ivir d00061aa7f Translated using Weblate (Croatian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hr/
3 months ago
大王叫我来巡山 45add6ab32 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
3 months ago
Pierfrancesco Passerini af43737c4e Translated using Weblate (Italian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
3 months ago
macpac59 dd40e59b17 Translated using Weblate (German)
Currently translated at 98.9% (648 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
3 months ago
gallegonovato 13f3248a01 Translated using Weblate (Spanish)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
3 months ago
renovate[bot] f6972e3e30 Update dependency com.android.tools.build:gradle to v8.3.1 3 months ago
Alex Baker 83cf48a836 Don't pass filter to remoteviews service
This was working on emulators but crashing in the wild
3 months ago
Alex Baker b7b4747a04 Update translation credits
Was in a rush to get a bug fix out!
3 months ago
Alex Baker 6bec2ceef0 Update version and changelog 3 months ago
Milo Ivir d1e60d6512 Translated using Weblate (Croatian)
Currently translated at 100.0% (654 of 654 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hr/
3 months ago
bittin1ddc447d824349b2 2b85089d3a Translated using Weblate (Swedish)
Currently translated at 100.0% (654 of 654 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sv/
3 months ago
ferranpujolcamins 2a0ef9feb6 Translated using Weblate (Catalan)
Currently translated at 34.7% (227 of 654 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ca/
3 months ago
109247019824 33adbbd884 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (654 of 654 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
3 months ago
Alex Baker c25eb2e0c5 Fix crash on earlier Android versions 3 months ago
Alex Baker 14026356eb Fix widget arrow color 3 months ago
Alex Baker b328651dd4
Run tests on generic flavor (#2808) 3 months ago
Alex Baker a0e9bfabeb Update version and changelog 3 months ago
大王叫我来巡山 a1ad421b33 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
3 months ago
Mayhm 3488a08af1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.4% (648 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
3 months ago
gallegonovato b71d1af516 Translated using Weblate (Spanish)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
3 months ago
Alex Baker 041dce8617 Add dynamic widget theme 3 months ago
Alex Baker 3d92ca78dd Remove unused attrs 3 months ago
Alex Baker a32fce2d8b Remove widget_title_* layouts 3 months ago
Alex Baker 4fb3cda173 Fix loading selected filter on startup 3 months ago
Alex Baker f33cc896dd Refactor widget 3 months ago
Alex Baker 4d1d6a06a8 Fix repeat until crash 3 months ago
Alex Baker 2202516688 Update isOverdue logic 3 months ago
Alex Baker d4a5008ecb Update CI 3 months ago
Alex Baker 08189e10f1 Don't use gradle managed devices in CI 3 months ago
Anonymous d3e4c066d8 Translated using Weblate (Sinhala)
Currently translated at 92.0% (606 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/si/
3 months ago
Anonymous bbc5ae4d6d Translated using Weblate (Tamil)
Currently translated at 68.2% (449 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ta/
3 months ago
Anonymous c6cc00cf07 Translated using Weblate (Thai)
Currently translated at 89.5% (589 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/th/
3 months ago
Anonymous 22e8720021 Translated using Weblate (Hebrew)
Currently translated at 89.5% (589 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/he/
3 months ago
Anonymous a3ce98f0ea Translated using Weblate (Danish)
Currently translated at 95.8% (631 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/da/
3 months ago
macpac59 258f607d52 Translated using Weblate (German)
Currently translated at 99.6% (656 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
3 months ago
ngocanhtve 927acae7e4 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/vi/
3 months ago
Odweta 49ad9bafe3 Translated using Weblate (Czech)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/cs/
3 months ago
Alex Baker 6df616d9ce Use gradle managed devices 3 months ago
Alex Baker 157668e35a Fix tests 3 months ago
Aslam Karachiwala efdf343869 For English (en), replaced "until" with "ends on" in 'repeats_*' keys. 3 months ago
renovate[bot] 5606df17c5 Update flipper to v0.250.0 3 months ago
renovate[bot] fc3b4971f4 Update flipper to v0.249.0 3 months ago
renovate[bot] 6a1699bb33 Update dependency androidx.compose:compose-bom to v2024.02.02 3 months ago
renovate[bot] e49303d5ca Update dependency com.google.firebase:firebase-bom to v32.7.4 3 months ago
renovate[bot] 4b55569b51 Update mockito monorepo to v5.11.0 3 months ago
renovate[bot] 2d7145cde3 Update plugin com.google.devtools.ksp to v1.9.22-1.0.18 3 months ago
renovate[bot] f2ab8bed95 Update dependency com.google.firebase:firebase-bom to v32.7.3 3 months ago
renovate[bot] a5bc4cf536 Update dependency com.android.tools.build:gradle to v8.3.0 3 months ago
renovate[bot] 1b35372b3a Update dependency com.google.apis:google-api-services-tasks to v1-rev20240225-2.0.0 3 months ago
Alex Baker c0fd4bf66a Convert LocalBroadcastManager to Kotlin 3 months ago
renovate[bot] 5d366f0d61 Update dependency io.coil-kt:coil-gif to v2.6.0 3 months ago
renovate[bot] d0635ac6f3 Update hilt to v1.2.0 4 months ago
renovate[bot] 8d4cf4daa5 Update dependency androidx.compose.compiler:compiler to v1.5.10 4 months ago
renovate[bot] d1e439e70e Update dependency androidx.compose:compose-bom to v2024.02.01 4 months ago
renovate[bot] 4d4c3e5193 Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-test to v1.8.0 4 months ago
Alex Baker 20f87061fd Convert WidgetPreferences to Kotlin 4 months ago
renovate[bot] c03e3747c6 Update dependency com.google.gms:google-services to v4.4.1 4 months ago
renovate[bot] 925b1b9124 Update dependency com.google.firebase:firebase-bom to v32.7.2 4 months ago
Alex Baker 43db712f64 Update version and changelog 4 months ago
Alex Baker 9d33a73ee6 Fix drawer highlighting 4 months ago
renovate[bot] 391c600ce2 Update flipper to v0.247.0 4 months ago
renovate[bot] ee4ae94817 Update dependency androidx.compose:compose-bom to v2024.02.00 4 months ago
renovate[bot] 70b4be1447
Update dependency androidx.compose.compiler:compiler to v1.5.9 4 months ago
Don Zouras bc54d92789 Translated using Weblate (Esperanto)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
4 months ago
Сергій 2f34724b95 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/uk/
4 months ago
Alex Baker 940fdc28dd Strip markdown from repeat snackbar 4 months ago
Alex Baker 68542fce38 Fix repeat task toast displaying old due date 4 months ago
renovate[bot] 7ba2977100 Update dependency gradle to v8.6 4 months ago
Don Zouras cb242539f0 Translated using Weblate (Esperanto)
Currently translated at 98.4% (648 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
4 months ago
bittin1ddc447d824349b2 304841f2c3 Translated using Weblate (Swedish)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sv/
4 months ago
Don Zouras 819ea797e6 Translated using Weblate (Esperanto)
Currently translated at 98.4% (648 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
4 months ago
abc0922001 2dbea57262 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hant/
4 months ago
Don Zouras 516a916fd5 Translated using Weblate (Esperanto)
Currently translated at 96.9% (638 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
4 months ago
109247019824 3bd52efc80 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
4 months ago
renovate[bot] 64af955ea7 Update flipper to v0.246.0 4 months ago
Milo Ivir 4cc5ec9639 Translated using Weblate (Croatian)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hr/
4 months ago
大王叫我来巡山 0d9292e53a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
4 months ago
Oğuz Ersen 732ccf1913 Translated using Weblate (Turkish)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/tr/
4 months ago
gallegonovato a2852bdbbf Translated using Weblate (Spanish)
Currently translated at 100.0% (658 of 658 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
4 months ago
Don Zouras 68790ad401 Translated using Weblate (Esperanto)
Currently translated at 91.7% (603 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
4 months ago
Alex Baker e9afacb595 Include hidden subtasks when clearing completed 4 months ago
Alex Baker cf182aceab Display number of tasks to be cleared 4 months ago
Alex Baker db889d233a Remove AfterSaveWork 4 months ago
Alex Baker 457b89c092 Remove cleanup work 4 months ago
Don Zouras ad53af1b6a Translated using Weblate (Esperanto)
Currently translated at 87.0% (572 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
4 months ago
renovate[bot] 2c32b08c97 Update dependency androidx.compose:compose-bom to v2024 4 months ago
renovate[bot] a2fcf57c9e Update mockito monorepo to v5.10.0 4 months ago
renovate[bot] 59a61325f2 Update dependency org.osmdroid:osmdroid-android to v6.1.18 4 months ago
vulewuxe86 38a6064677 Reverted code
Reverted Code involving the action bar search function
4 months ago
renovate[bot] 67daccf3e8 Update lifecycle to v2.7.0 4 months ago
renovate[bot] dfe829d2a1 Update dependency com.google.android.gms:play-services-location to v21.1.0 4 months ago
renovate[bot] 23c64f4d28 Update dependency com.google.apis:google-api-services-drive to v3-rev20240123-2.0.0 4 months ago
renovate[bot] e4b8f694f3 Update dependency com.google.firebase:firebase-bom to v32.7.1 4 months ago
renovate[bot] e667c80731 Update kotlin 4 months ago
renovate[bot] 909b077e25 Update dependency com.android.tools.build:gradle to v8.2.2 4 months ago
Don Zouras e6fab9ad45 Translated using Weblate (Esperanto)
Currently translated at 82.1% (540 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
5 months ago
raulmagdalena 9474f5b7af Translated using Weblate (Catalan)
Currently translated at 34.5% (227 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ca/
5 months ago
Don Zouras 1ee051d768 Translated using Weblate (Esperanto)
Currently translated at 75.1% (494 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
5 months ago
Don Zouras f42edaa158 Translated using Weblate (Esperanto)
Currently translated at 73.8% (485 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
5 months ago
RayBB b97eade59c fix typos 5 months ago
renovate[bot] 41aa1ca65f Update flipper to v0.245.0 5 months ago
renovate[bot] 3e9a13ea14 Update mockito monorepo to v5.9.0 5 months ago
renovate[bot] d966e8a12b Update dependency fastlane to v2.219.0 5 months ago
renovate[bot] 8ba4e64994 Update dependency com.android.tools.build:gradle to v8.2.1 5 months ago
109247019824 ee792f1ceb Translated using Weblate (Bulgarian)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
5 months ago
renovate[bot] caa09163a1 Update dependency ruby to v3.3.0 5 months ago
renovate[bot] d270abf5b3 Update dependency com.facebook.soloader:soloader to v0.11.0 5 months ago
renovate[bot] 1ef530abad Update dependency com.squareup.leakcanary:leakcanary-android to v2.13 5 months ago
renovate[bot] df26a6dbb9 Update flipper to v0.244.0 5 months ago
renovate[bot] 1882c3b7e0 Update dependency androidx.activity:activity-compose to v1.8.2 6 months ago
renovate[bot] cb53a0ca9f Update dependency com.google.android.material:material to v1.11.0 6 months ago
renovate[bot] b2fdef1ae7 Update dagger.hilt to v2.50 6 months ago
renovate[bot] defb16ce95 Update kotlin 6 months ago
renovate[bot] 823f99b28a Update flipper to v0.243.0 6 months ago
renovate[bot] 6df872b1a1 Update actions/upload-artifact action to v4 6 months ago
renovate[bot] 133b960583 Update flipper to v0.242.0 6 months ago
renovate[bot] 2e6753faec Update dependency com.google.apis:google-api-services-drive to v3-rev20231128-2.0.0 6 months ago
renovate[bot] cb07c2c267 Update dependency com.google.firebase:firebase-bom to v32.7.0 6 months ago
renovate[bot] 23757ab320 Update kotlin 6 months ago
Alex Baker 1b6ce0e48e Ignore empty rrule 6 months ago
Kakaeo 5af012068f Translated using Weblate (Persian)
Currently translated at 29.9% (197 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fa/
6 months ago
Alex Baker 6c9ffa57d7 Fix reparenting task to another list 6 months ago
Alex Baker 52c54b1eac Fix excessive querying (again) 6 months ago
Alex Baker c8d81b44b6 Fix excessive querying 6 months ago
renovate[bot] ef27a50e42 Update mockito monorepo to v5.8.0 6 months ago
Alex Baker bde1356e7f Add task to MainActivityViewModel state 6 months ago
Alex Baker 6c031925ba Replace some setter usage with constructors 6 months ago
Alex Baker 8058414137 Use release build for compose metrics 6 months ago
Alex Baker 3e37ea50f0 Update compose-compiler to v1.5.5 6 months ago
renovate[bot] 62f5a9c492 Update actions/setup-java action to v4 6 months ago
renovate[bot] a84fd65722 Update dependency androidx.room:room-ktx to v2.6.1 6 months ago
renovate[bot] 517b2d8f1b Update dependency gradle to v8.5 6 months ago
renovate[bot] 90942bf0be Update dependency com.google.dagger:hilt-android to v2.49 6 months ago
Alex Baker 83c3d1c4ba AGP 8.2.0 6 months ago
Software In Interlingua 6362ece569 Translated using Weblate (Interlingua)
Currently translated at 5.6% (37 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ia/
6 months ago
renovate[bot] 8df85041b8 Update flipper to v0.240.0 6 months ago
ngocanhtve 6d85af4c34 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/vi/
6 months ago
Olli 63f001dd72 Translated using Weblate (Finnish)
Currently translated at 99.0% (651 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fi/
6 months ago
renovate[bot] de49a50944 Update dependency fastlane to v2.217.0 7 months ago
renovate[bot] df20d2f593 Update dependency com.android.billingclient:billing-ktx to v6.1.0 7 months ago
renovate[bot] fd16772236 Update dependency androidx.activity:activity-compose to v1.8.1 7 months ago
renovate[bot] b77caac255 Update dependency com.google.firebase:firebase-bom to v32.6.0 7 months ago
renovate[bot] ad058ed09b Update kotlin 7 months ago
Alex Baker 8312113d7b Merge branch '13.6.3' 7 months ago
Alex Baker ee21cc660e Update version and changelog 7 months ago
Alex Baker 5edc481ffe Fix etag check for DecSync 7 months ago
Alex Baker d0360a4862 Revert "Update timestamp on edits"
This reverts commit b477623524.
7 months ago
Alex Baker ac35002408 Revert "Update modification timestamp logic (#2585)"
This reverts commit 775289b058.
7 months ago
Subham Jena 582ebad0f0 Translated using Weblate (Odia)
Currently translated at 20.7% (136 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/or/
7 months ago
Shaban Mamedov 684c47184a Added translation using Weblate (Azerbaijani) 7 months ago
ngocanhtve ac7a519e4e Translated using Weblate (Vietnamese)
Currently translated at 99.6% (655 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/vi/
7 months ago
renovate[bot] 5c2b41af9d Update flipper to v0.238.0 7 months ago
renovate[bot] 13986cf380 Update flipper to v0.237.0 7 months ago
CennoxX c4f0b404e9 Translated using Weblate (German)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
7 months ago
Alex Baker 145b5afbc6 AGP 8.2.0-rc03 7 months ago
elig0n 0b87a206fe Translated using Weblate (Hebrew)
Currently translated at 90.2% (593 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/he/
7 months ago
mm4c d0e70ceea8 Translated using Weblate (Dutch)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/nl/
7 months ago
J. Lavoie bf3546a878 Translated using Weblate (French)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fr/
7 months ago
Alex Baker 8895acbf6b Prevent flashing empty inbox when switching lists 7 months ago
Alex Baker a52b1200f5 Fix menu expansion 7 months ago
renovate[bot] 23964e807a
Update flipper to v0.236.0 (#2617)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
7 months ago
Alex Baker 287b106dd4 Highlight selected list in drawer 7 months ago
renovate[bot] 33bab626e0
Update mockito monorepo to v5.7.0 (#2613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
7 months ago
renovate[bot] a980cd75cc Update hilt to v1.1.0 7 months ago
renovate[bot] 7eac4ac223 Update dependency androidx.fragment:fragment-ktx to v1.6.2 7 months ago
renovate[bot] 82cb2f7d3f
Update dependency com.android.tools:desugar_jdk_libs to v2.0.4 7 months ago
renovate[bot] da2646597c
Update flipper to v0.235.0 (#2608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
7 months ago
renovate[bot] 495855133c Update dependency com.google.firebase:firebase-bom to v32.5.0 7 months ago
renovate[bot] 242cb61662 Update coil to v2.5.0 7 months ago
renovate[bot] ab8886f3dc Update flipper to v0.234.0 7 months ago
Alex Baker e48e92d2e6 Fix dates 🤦 7 months ago
Alex Baker 5f22f5cd38 AGP 8.2.0-rc02 7 months ago
Alex Baker 8a47cc2934 Don't set local only notifications on Android 14+ 7 months ago
Alex Baker 0d94729d37 Merge branch '13.6.2' 7 months ago
Alex Baker 14599eb3c0 Update version and changelog 7 months ago
Alex Baker b477623524 Update timestamp on edits
Fix bugs introduced by 775289b05
7 months ago
Alex Baker c8bfb67b50 Allow multi-select for gallery picker 7 months ago
Alex Baker 0a36e58525 Allow multi-select in storage picker 7 months ago
Alex Baker 94a719cb66 Improve menu dismissal
Copy M3 ModalBottomSheet to add 'skipPartiallyCollapsed' support 😕
7 months ago
Alex Baker b5748aa8e6 New drawer 7 months ago
Alex Baker 7fd5647cb8 Exclude hidden and completed from snoozed filter 7 months ago
Alex Baker 2545832d67 Update version and changelog 7 months ago
Alex Baker 738bf435db Fix some back handlers 7 months ago
renovate[bot] ab02323f29
Update flipper to v0.233.0 (#2599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 months ago
Alex Baker d73a9d2795 Update version and changelog 8 months ago
Alex Baker ebe67354b6 Add tests for recurrence without intervals 8 months ago
Alex Baker 58edc6b4d8 Fix basic hourly and weekly recurrence 8 months ago
Weblate (bot) 78b2cdac06
Translations update from Hosted Weblate (#2596)
* Translated using Weblate (French)

Currently translated at 99.5% (654 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fr/

* Translated using Weblate (French)

Currently translated at 99.5% (654 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fr/

---------

Co-authored-by: Bruno Duyé <brunetton@gmail.com>
Co-authored-by: Lionel HANNEQUIN <Lionel-HANNEQUIN@users.noreply.hosted.weblate.org>
8 months ago
renovate[bot] c3d7db0087
Update flipper to v0.232.0 (#2597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 months ago
renovate[bot] d7b1770b85
Update flipper to v0.231.0 (#2591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 months ago
Weblate (bot) bebb3165a5
Translations update from Hosted Weblate (#2589)
Translated using Weblate (French)

Currently translated at 99.5% (654 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fr/

Co-authored-by: Lionel HANNEQUIN <Lionel-Fr-56@users.noreply.hosted.weblate.org>
8 months ago
Alex Baker ad1198aace Don't require network for OpenTasks sync 8 months ago
Alex Baker 7ae77a81e1 Don't need background sync for OpenTasks
Background sync handled by sync apps
8 months ago
Weblate (bot) 3e79dd5190
Translations update from Hosted Weblate (#2587)
Translated using Weblate (French)

Currently translated at 99.6% (655 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fr/

Co-authored-by: Lionel HANNEQUIN <Lionel-Fr-56@users.noreply.hosted.weblate.org>
8 months ago
renovate[bot] 9d57a849bf
Update flipper to v0.230.0 (#2586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 months ago
Alex Baker 82103eb477 Make chips skippable 8 months ago
renovate[bot] 11fa9a2bbd Update dependency com.google.firebase:firebase-bom to v32.4.0 8 months ago
renovate[bot] b65831120f Update room to v2.6.0 8 months ago
renovate[bot] f26a90a4f9 Update hilt to v1.1.0-rc01 8 months ago
renovate[bot] dd3aa20485 Update dependency androidx.recyclerview:recyclerview to v1.3.2 8 months ago
renovate[bot] 8c84e1af50 Update dependency com.google.android.gms:play-services-maps to v18.2.0 8 months ago
renovate[bot] dc1eac23b9 Update dependency androidx.compose:compose-bom to v2023.10.01 8 months ago
renovate[bot] 5883952883
Update flipper to v0.229.0 (#2579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 months ago
Alex Baker 775289b058
Update modification timestamp logic (#2585) 8 months ago
Alex Baker ee500c24b1
Convert filters to data classes (#2569) 8 months ago
Igor Sorocean 68fd36b14d Translated using Weblate (Romanian)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ro/
8 months ago
renovate[bot] b8f265fa36 Update okhttp to v4.12.0 8 months ago
Alex Baker cf4e6c1273 Update AGP to 8.2.0-rc01 8 months ago
renovate[bot] 0dcc577497 Update flipper to v0.227.0 8 months ago
kmj-99 b525e8cab3 Refactor: Change deprecated code in Fragment onAttach 8 months ago
renovate[bot] db0ad280eb Update dependency com.google.auth:google-auth-library-oauth2-http to v1.20.0 8 months ago
Alex Baker 5092f80dcc Update billing to v6.0.1 8 months ago
Kazushi Hayama 6bc42363dd Translated using Weblate (Japanese)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ja/
8 months ago
Michal Šmahel 115461c7b0 Translated using Weblate (Czech)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/cs/
8 months ago
renovate[bot] 369c508890 Update flipper to v0.226.0 8 months ago
bittin1ddc447d824349b2 a432cc33cc Translated using Weblate (Swedish)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sv/
8 months ago
Alex Baker e5b51150cb Start sync if enqueued when app is backgrounded 8 months ago
renovate[bot] d43639556e Update dependency com.google.android.material:material to v1.10.0 8 months ago
Alex Baker ef2dd8f202 Merge branch '13.6' 8 months ago
Alex Baker abc099c309 Update version and changelog 8 months ago
Alex Baker 348367e084 Replace INSTALL_SHORTCUT with requestPinShortcut 8 months ago
renovate[bot] 6a73f6745c Update flipper to v0.225.0 8 months ago
renovate[bot] 5185c14e44 Update mockito monorepo to v5.6.0 8 months ago
Olli aa7ff0fa16 Translated using Weblate (Finnish)
Currently translated at 99.0% (651 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fi/
8 months ago
renovate[bot] 12b979d363 Update flipper to v0.224.0 8 months ago
Alex Baker 082f741983 Convert Filter to data class 8 months ago
Alex Baker 0bdd83988f Fix lint errors 8 months ago
Alex Baker 60784c10b5 Update filter fields 8 months ago
Alex Baker da8467ac56 Remove constructor 8 months ago
renovate[bot] 434d067822 Update dependency gradle to v8.4 8 months ago
renovate[bot] 04af310285 Update hilt to v1.1.0-beta01 8 months ago
renovate[bot] 5555771f45 Update dependency com.google.dagger:hilt-android-testing to v2.48.1 8 months ago
renovate[bot] 35b60df0ff Update dependency androidx.activity:activity-compose to v1.8.0 8 months ago
renovate[bot] fef19b4995 Update dependency androidx.compose:compose-bom to v2023.10.00 8 months ago
Alex Baker 4c25b81a4d Move Parcelable 8 months ago
Alex Baker 0f37f4859e Update compose reports 8 months ago
Alex Baker ee3d3fa4f5 Convert FilterListItem to interface 8 months ago
Alex Baker a32d35720a Refresh after changing sort mode 8 months ago
Alex Baker bf6fe02fe3 Convert FilterListItems to Kotlin 8 months ago
Alex Baker 6664defc16 Minor refactoring 8 months ago
renovate[bot] b318b930a5 Update flipper to v0.223.0 8 months ago
Loucura 91d18fd675 Translated using Weblate (Portuguese)
Currently translated at 99.8% (656 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt/
8 months ago
Alex Baker 94b6d7569b Move search to viewmodel 8 months ago
Alex Baker e70f5f3b24 Move query constants 8 months ago
renovate[bot] 68c21c4b1f Update dependency androidx.compose:compose-bom to v2023.09.02 8 months ago
renovate[bot] cbcc7f9bee Update dependency com.android.tools.build:gradle to v8.2.0-beta06 8 months ago
Milo Ivir ba394b9db4
Translated using Weblate (Croatian)
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hr/
9 months ago
Eric 13298aa3be
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
9 months ago
Alex Baker 993c41b197 Remove RecurringIntervalIntentService 9 months ago
Alex Baker 2bfc46f32b Remove CalendarReminderActivity 9 months ago
Alex Baker 4c61353411 Remove TimerControlSetCallback 9 months ago
Alex Baker f8d3985e97 Add hideKeyboard extension methods 9 months ago
Alex Baker c2a9d21f01 Make review request from edit fragment 9 months ago
Alex Baker 20c81417a0 Handle sort result in task list fragment 9 months ago
Alex Baker 77c86bbfb4 Move sync state to viewmodel 9 months ago
renovate[bot] 7e9ec26f53 Update flipper to v0.222.0 9 months ago
Alex Baker 928ec9f647 Add night mode context extensions 9 months ago
Alex Baker 84ab8d0517 Remove MainActivity.onBackPressed 9 months ago
Alex Baker db66a66578 Move subscription begging to viewmodel 9 months ago
Alex Baker ea8a4b5e2d Remove InjectingAppCompatActivity 9 months ago
Alex Baker 5a4485818f Update dependency androidx.compose:compose-bom to v2023.09.01 9 months ago
renovate[bot] 1d348fcac9 Update room to v2.6.0-rc01 9 months ago
renovate[bot] 79250cb8ff Update dependency com.android.tools.build:gradle to v8.2.0-beta05 9 months ago
renovate[bot] 5d550df62e Update flipper to v0.221.0 9 months ago
Pierfrancesco Passerini 1267fbeb0d Translated using Weblate (Italian)
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
9 months ago
renovate[bot] 2b0e285b42 Update dependency fastlane to v2.216.0 9 months ago
Alex Baker 374f10c731 Add subtasks in order for new tasks 9 months ago
abc0922001 a38fdc065e Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hant/
9 months ago
Ihor Hordiichuk 6d4159eaac Translated using Weblate (Ukrainian)
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/uk/
9 months ago
Oğuz Ersen 20fe494cd9 Translated using Weblate (Turkish)
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/tr/
9 months ago
Frits van Bommel e7686bd9eb Translated using Weblate (Dutch)
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/nl/
9 months ago
Kaci d4d721f060 Translated using Weblate (Hungarian)
Currently translated at 99.7% (666 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hu/
9 months ago
Florian Trayon 1e9b39afd5 Translated using Weblate (French)
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fr/
9 months ago
Florian Trayon 8878df27c4 Translated using Weblate (Spanish)
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
9 months ago
C. Rüdinger 07eb9db157 Translated using Weblate (German)
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
9 months ago
109247019824 b4ad27152f Translated using Weblate (Bulgarian)
Currently translated at 100.0% (668 of 668 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
9 months ago
kmj-99 417a1cca46 Change deprecated code from RecyclerView.ViewHolder 9 months ago
renovate[bot] 3a6086adbd
Update flipper to v0.220.0 (#2522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
9 months ago
Alex Baker 9f3f0a9698 Show unstarted tasks in widget by default 9 months ago
Alex Baker 8a085861de Update version and changelog 9 months ago
Alex Baker 7048f6a965 Fix crash in debug logging 9 months ago
Alex Baker a9cb7b0e89 Fix default reminders for remote icalendar tasks 9 months ago
Alex Baker 4c7e2caa73 Update OK/Cancel string
Was causing too much confusion for translators
9 months ago
Alex Baker 3a5e45283a Update change priority menu option 9 months ago
renovate[bot] a1885574da
Update dependency fastlane to v2.215.1 (#2520)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
9 months ago
vulewuxe86 113cf6f1b8
#2257 Change priority for selected items (#2452) 9 months ago
renovate[bot] 138cc21796 Update dependency com.google.firebase:firebase-bom to v32.3.1 9 months ago
Alex Baker dea3484a2f Actually update fastlane 9 months ago
Alex Baker 5948e4a958 Set googleplay as default flavor 9 months ago
renovate[bot] 10d2e8feda Update dependency com.google.gms:google-services to v4.4.0 9 months ago
renovate[bot] cef7998a52 Update dependency fastlane to v2.215.0 9 months ago
renovate[bot] 834bef7933 Update flipper to v0.219.0 9 months ago
renovate[bot] 8ed6afff2b Update flipper to v0.218.0 9 months ago
renovate[bot] 7d13e4f0ba Update dependency com.android.tools.build:gradle to v8.2.0-beta04 9 months ago
Alex Baker 5cb8419206 Fix network call on main thread 9 months ago
Alex Baker 7283491872 Update Hilt to 1.1.0-alpha01 9 months ago
Alex Baker 2de5b3c275 Use share text when subject is missing 9 months ago
Alex Baker 864550d027 Enable ksp for dagger 9 months ago
renovate[bot] b48348f30e Update dependency org.osmdroid:osmdroid-android to v6.1.17 9 months ago
renovate[bot] 81fdddc631 Update flipper to v0.216.0 9 months ago
Luna Jernberg 21cb25d902 Translated using Weblate (Swedish)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sv/
9 months ago
Anaemix b73ba43735 Translated using Weblate (Swedish)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sv/
9 months ago
renovate[bot] a3ba87e4e6 Update kotlin 9 months ago
renovate[bot] 26321633e2 Update dependency androidx.compose:compose-bom to v2023.09.00 9 months ago
renovate[bot] 1c3656a69c Update dependency com.google.firebase:firebase-bom to v32.2.3 9 months ago
renovate[bot] 88bb66a7b3 Update dependency com.google.firebase:firebase-crashlytics-gradle to v2.9.9 9 months ago
renovate[bot] 0f7c200851 Update dependency com.google.apis:google-api-services-drive to v3-rev20230822-2.0.0 9 months ago
renovate[bot] f6d5732c07 Update lifecycle to v2.6.2 9 months ago
renovate[bot] b278a04fce Update room to v2.6.0-beta01 9 months ago
renovate[bot] 71c2e2b0f6 Update dagger.hilt to v2.48 9 months ago
renovate[bot] 87639da922 Update dependency com.android.tools.build:gradle to v8.2.0-beta03 9 months ago
Anaemix d7e366712c Translated using Weblate (Swedish)
Currently translated at 98.5% (657 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sv/
9 months ago
renovate[bot] 74ecb4a8bf Update actions/checkout action to v4 9 months ago
Emin Tufan Çetin 305bd24883 Translated using Weblate (Turkish)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/tr/
9 months ago
renovate[bot] 68b7bef1ca Update flipper to v0.215.1 9 months ago
renovate[bot] f6a6b0716f Update flipper to v0.215.0 9 months ago
renovate[bot] dced669176 Update flipper to v0.214.0 9 months ago
Emin Tufan Çetin 1b431f7a61 Translated using Weblate (Turkish)
Currently translated at 98.9% (660 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/tr/
9 months ago
Joan Montané 2b3b7184e0 Translated using Weblate (Catalan)
Currently translated at 32.6% (218 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ca/
9 months ago
renovate[bot] a7062fe937 Update flipper to v0.213.0 10 months ago
Alex Baker 573f6b897e Set audio attributes on completion sound 10 months ago
renovate[bot] e1845d71bc Update accompanist to v0.32.0 10 months ago
renovate[bot] 1027d57860 Update dependency com.google.apis:google-api-services-drive to v3-rev20230815-2.0.0 10 months ago
renovate[bot] a2c92b8fd9 Update mockito monorepo to v5.5.0 10 months ago
renovate[bot] 77643d7355 Update flipper to v0.212.0 10 months ago
Patrick V. Leguizamon 54911021a5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.6% (651 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
10 months ago
renovate[bot] 908ac19754 Update dependency gradle to v8.3 10 months ago
Alex Baker 4ebd53fdf7 Enable dagger.ignoreProvisionKeyWildcards 10 months ago
Alex Baker a0fbeba938 'More settings' opens directly to channel settings 10 months ago
Alex Baker bd6000fcd6 Add subtasks to top 10 months ago
Alex Baker aa861cb5e5 Convert room to ksp 10 months ago
Alex Baker 17818c6e29 Remove checkstyle 10 months ago
Poesty Li ca6521db23
Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
10 months ago
Alex Baker dedf306106 Update kotlinx-coroutines-test to 1.7.3 10 months ago
Alex Baker 40f9b83dba Remove bundles from version catalog 10 months ago
Alex Baker c8b057867f Gradle Plugin 8.2.0-alpha16 10 months ago
Alex Baker d1ebd45492 Update to Kotlin 1.9, Compose compiler 1.5.1 10 months ago
Alex Baker ac28e26333 Update Room to 2.6.0-alpha03 10 months ago
renovate[bot] fdf9fbce08 Update flipper to v0.211.1 10 months ago
renovate[bot] 6d712642b3 Update dependency com.google.apis:google-api-services-drive to v3-rev20230714-2.0.0 10 months ago
renovate[bot] 329939e2b0 Update dependency androidx.compose:compose-bom to v2023.08.00 10 months ago
Alex Baker f62de8b7f3 Microsoft recurrence sync
Limit recurrence options for Microsoft tasks
10 months ago
deep map a2ef184c7d Translated using Weblate (German)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
10 months ago
Salif Mehmed 6ac2c88782 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
10 months ago
Naga ada31293ea Translated using Weblate (Japanese)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ja/
10 months ago
Pierfrancesco Passerini 805d914ff4 Translated using Weblate (Italian)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
10 months ago
109247019824 aa1b4ef71a Translated using Weblate (Bulgarian)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
10 months ago
vulewuxe86 3d0cf46f8d
Select the new items after duplication (#2446)
#1201 Select the new items after duplication
10 months ago
abc0922001 f7e2c7824a Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hant/
10 months ago
Stefan 2e2bdbe07a Reduced minimum widget height 10 months ago
Alex Baker 427ee369b4 Fix miscellaneous inspections 10 months ago
Alex Baker 804c0f974a Force a due date for recurring tasks 10 months ago
109247019824 691dc635a9 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
10 months ago
renovate[bot] 9f2364867b Update dependency com.google.firebase:firebase-bom to v32.2.2 10 months ago
renovate[bot] cd638bba71
Update dependency com.google.firebase:firebase-crashlytics-gradle to v2.9.8 10 months ago
Naga 156669cb86 Translated using Weblate (Japanese)
Currently translated at 98.6% (658 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ja/
10 months ago
Kazushi Hayama 6c1daf5a3c Translated using Weblate (Japanese)
Currently translated at 98.6% (658 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ja/
10 months ago
Pierfrancesco Passerini 0a297c595f Translated using Weblate (Italian)
Currently translated at 99.2% (662 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
10 months ago
qwerty287 b698fc04db Translated using Weblate (German)
Currently translated at 98.6% (658 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
10 months ago
109247019824 ab6f3463d0 Translated using Weblate (Bulgarian)
Currently translated at 98.8% (659 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
10 months ago

@ -19,12 +19,12 @@ jobs:
- name: Decode Keystore - name: Decode Keystore
run: | run: |
echo ${{ secrets.KEY_STORE }} | base64 -di > "${RUNNER_TEMP}"/keystore.jks echo ${{ secrets.KEY_STORE }} | base64 -di > "${RUNNER_TEMP}"/keystore.jks
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
bundler-cache: true bundler-cache: true
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
@ -41,7 +41,7 @@ jobs:
GOOGLE_KEY: ${{ secrets.GOOGLE_KEY }} GOOGLE_KEY: ${{ secrets.GOOGLE_KEY }}
run: bundle exec fastlane bundle run: bundle exec fastlane bundle
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: release name: release
path: app/build/outputs/** path: app/build/outputs/**

@ -11,12 +11,12 @@ jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
bundler-cache: true bundler-cache: true
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
@ -26,31 +26,66 @@ jobs:
- name: Lint checks - name: Lint checks
run: bundle exec fastlane lint run: bundle exec fastlane lint
- name: Archive lint reports - name: Archive lint reports
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: ${{ always() }} if: ${{ always() }}
with: with:
name: lint-reports name: lint-reports
path: app/build/reports/*.html path: app/build/reports/*.html
test: test:
runs-on: macos-latest runs-on: ubuntu-latest
strategy:
matrix:
flavor: [Googleplay, Generic]
api-level: [29]
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: '17' java-version: '17'
cache: 'gradle' cache: 'gradle'
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: AVD cache
uses: actions/cache@v3
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}
- name: create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: run tests - name: run tests
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
with: with:
api-level: 29 api-level: ${{ matrix.api-level }}
script: ./gradlew -Pcoverage app:testGoogleplayDebugUnitTest app:connectedGoogleplayDebugAndroidTest force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew -Pcoverage app:test${{ matrix.flavor }}DebugUnitTest app:connected${{ matrix.flavor }}DebugAndroidTest
- name: Upload test reports - name: Upload test reports
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
if: ${{ always() }} if: ${{ always() }}
with: with:
name: test-reports name: test-reports-${{ matrix.flavor }}
path: app/build/reports/** path: app/build/reports/**

1
.gitignore vendored

@ -1,3 +1,4 @@
.kotlin
.idea .idea
*.iml *.iml
.gradle .gradle

@ -1 +1 @@
3.2.2 3.3.1

@ -1,3 +1,171 @@
### 13.9.9 (2024-05-30)
* Fix import backup crashes
* Fix showing completed subtasks in edit screen
### 13.9.7 (2024-05-23)
* Add default reminders when adding start/due dates to existing tasks [#1846](https://github.com/tasks/tasks/issues/1846)
* Fix import backup crash
### 13.9.6 (2024-05-18)
* Fix widget crash [#2873](https://github.com/tasks/tasks/issues/2873)
* Fix recurrence unable to finish [#2874](https://github.com/tasks/tasks/issues/2874)
* Fix edit screen being cleared when reopening app [#2857](https://github.com/tasks/tasks/issues/2857)
* Fix performance regressions
* Simplified internal alarm scheduling logic
* Update translations
* Arabic - @islam2hamy
* Bulgarian - @StoyanDimitrov
### 13.9 (2024-05-01)
* @elmuffo: Add swipe-to-snooze [#2839](https://github.com/tasks/tasks/pull/2839)
* @IlyaBizyaev: Add option to use quick tile without unlocking device [#2847](https://github.com/tasks/tasks/pull/2847)
* @liz-desartiges: Add support for Z Flip 5 cover screen [#2843](https://github.com/tasks/tasks/pull/2843)
* @purushyb: Fix drawer not updating after editing items [#2855](https://github.com/tasks/tasks/pull/2855)
* @hady-exc: Migrate tag picker screen to Compose [#2849](https://github.com/tasks/tasks/pull/2849)
* @yurtpage: Add Russian app store description [#2848](https://github.com/tasks/tasks/pull/2848)
* Fix duplicate notifications [#2835](https://github.com/tasks/tasks/issues/2835)
* Fix adding '(Completed)' to calendar entries [#2832](https://github.com/tasks/tasks/issues/2832)
* Fix hiding empty items from drawer [#2831](https://github.com/tasks/tasks/issues/2831)
* Exclude old snoozed tasks from snoozed task filter
* Update translations
* Brazilian Portuguese - @mayhmemo, @gorgonun
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Esperanto - Don Zouras
* French - Lionel HANNEQUIN
* German - sorifukobexomajepasiricupuva33, min7-i
* Portuguese - @fparri, @laralem
* Spanish - gallegonovato
* Swedish - @JonatanWick
* Turkish - @emintufan, @oersen
### 13.8.1 (2024-03-24)
* Fix copy causing duplicate Google Tasks
* Fix navigation drawer crash
* Fix backup import dropping tasks
### 13.8 (2024-03-22)
* Dynamic widget theme (name-your-price subscription required)
* Replace 'until' with 'ends on' for repeating tasks [#2797](https://github.com/tasks/tasks/pull/2797) - @akwala
* Fix loading selected list on startup [#2777](https://github.com/tasks/tasks/issues/2777)
* Fix repeating tasks ending one day early
* Fix repeating task crash
* Fix backup import crash
* Fix Astrid manual ordering crash in widget
* Update translations
* Brazilian Portuguese - @mayhmemo
* Bulgarian - @StoyanDimitrov
* Catalan - @ferranpujolcamins
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* Czech - Odweta
* German - @macpac59
* Italian - @ppasserini
* Spanish - gallegonovato
* Swedish - @bittin
* Ukrainian - @IhorHordiichuk
* Vietnamese - @ngocanhtve
### 13.7 (2024-02-07)
* Fix returning to previous filter after search [#2700](https://github.com/tasks/tasks/pull/2700)
* Fix wearable notifications on Android 14+
* Fix issue causing repeating tasks to not repeat
* Fix dragging a task into a subtask in another list
* Rewrote navigation drawer in Jetpack Compose
* Internal changes to navigation
* Enable multi-select when adding attachments
* Show count of tasks to be deleted when clearing completed
* Include hidden subtasks when clearing completed [#2724](https://github.com/tasks/tasks/issues/2724)
* Don't show hidden or completed tasks in snoozed filter
* Remove markdown from repeating task snackbar
* Update translations
* Azerbaijani - Shaban Mamedov
* Bulgarian - @StoyanDimitrov
* Catalan - raulmagdalena
* Chinese (Simplified) - 大王叫我来巡山
* Chinese (Traditional) - @abc0922001
* Croatian - @milotype
* Dutch - @mm4c
* Esperanto - Don Zouras
* Finnish - @millerii
* French - J. Lavoie
* German - @CennoxX
* Hebrew - @elig0n
* Interlingua - @softinterlingua
* Odia - @SubhamJena
* Persian - @Monirzadeh
* Spanish - gallegonovato
* Swedish - @bittin
* Turkish - @oersen
* Ukrainian - Сергій
* Vietnamese - @ngocanhtve
### 13.6.3 (2023-11-25)
* Revert "Preserve modification times on initial sync" [#2460](https://github.com/tasks/tasks/issues/2640)
* Fix unnecessary DecSync work
### 13.6.2 (2023-10-30)
* Fix updating modification timestamp on edits
### 13.6.1 (2023-10-27)
* Push pending changes when app is backgrounded
* Don't require internet connection for DAVx5/EteSync/DecSync sync
* Don't perform background sync for DAVx5/EteSync/DecSync
* Background sync is performed by the sync app
* Preserve modification times on initial sync [#2496](https://github.com/tasks/tasks/issues/2496)
* Replace deprecated method call [#2547](https://github.com/tasks/tasks/pull/2547) - @kmj-99
* Improve task list scrolling performance
* Fix hourly recurrence bug
* Update translations
* Chinese (Simplified) - Eric
* Croatian - @milotype
* Czech - @ceskyDJ
* Finnish - @millerii
* French - Lionel HANNEQUIN, Bruno Duyé
* Japanese - Kazushi Hayama
* Portuguese - @loucurapt
* Romanian - @ygorigor
* Swedish - @bittin
### 13.6 (2023-10-07)
* Change priority with multi-select [#2257](https://github.com/tasks/tasks/pull/2452) - @vulewuxe86
* Automatically select newly copied tasks [#2246](https://github.com/tasks/tasks/pull/2446) - @vulewuxe86
* Reduce minimum size for widgets [#2436](https://github.com/tasks/tasks/pull/2436) - @histefanhere
* Replace deprecated method call [#2526](https://github.com/tasks/tasks/pull/2526) - @kmj-99
* Improve handling text shared to Tasks [#2485](https://github.com/tasks/tasks/issues/2485)
* Use notification audio stream for completion sound
* Notification preference 'More settings' opens channel settings directly
* Respect 'New tasks on top' preference when creating subtasks
* Automatically add due dates for recurring tasks
* Fix crash on startup
* Update translations
* Brazilian Portuguese - @gorgonun
* Bulgarian - @StoyanDimitrov, @salif
* Catalan - Joan Montané
* Chinese (Simplified) - Poesty Li
* Chinese (Traditional) - @abc0922001
* Dutch - @fvbommel
* French - @FlorianLeChat
* German - @qwerty287, deep map, @franconian
* Hungarian - Kaci
* Italian - @ppasserini
* Japanese - Kazushi Hayama, Naga
* Spanish - @FlorianLeChat
* Swedish - @Anaemix, @bittin
* Turkish - @emintufan, @oersen
* Ukrainian - @IhorHordiichuk
### 13.5.1 (2023-08-02) ### 13.5.1 (2023-08-02)
* Fix crash when importing Google Tasks from a backup file * Fix crash when importing Google Tasks from a backup file

@ -1,29 +1,32 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
CFPropertyList (3.0.6) CFPropertyList (3.0.7)
base64
nkf
rexml rexml
addressable (2.8.4) addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15) artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.2.0) aws-eventstream (1.3.0)
aws-partitions (1.786.0) aws-partitions (1.923.0)
aws-sdk-core (3.178.0) aws-sdk-core (3.194.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.71.0) aws-sdk-kms (1.80.0)
aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-core (~> 3, >= 3.193.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.130.0) aws-sdk-s3 (1.149.0)
aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-core (~> 3, >= 3.194.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6) aws-sigv4 (~> 1.8)
aws-sigv4 (1.6.0) aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4) babosa (1.0.4)
base64 (0.2.0)
claide (1.1.0) claide (1.1.0)
colored (1.2) colored (1.2)
colored2 (3.1.2) colored2 (3.1.2)
@ -32,11 +35,10 @@ GEM
declarative (0.0.20) declarative (0.0.20)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701) domain_name (0.6.20240107)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.8.1) dotenv (2.8.1)
emoji_regex (3.2.3) emoji_regex (3.2.3)
excon (0.100.0) excon (0.110.0)
faraday (1.10.3) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
@ -65,15 +67,15 @@ GEM
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.0) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
fastimage (2.2.7) fastimage (2.3.1)
fastlane (2.214.0) fastlane (2.220.0)
CFPropertyList (>= 2.3, < 4.0.0) CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0) addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0) artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0) aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0) babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0) bundler (>= 1.12.0, < 3.0.0)
colored colored (~> 1.2)
commander (~> 4.6) commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0) dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0) emoji_regex (>= 0.1, < 4.0)
@ -85,30 +87,32 @@ GEM
gh_inspector (>= 1.1.2, < 2.0.0) gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3) google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1) google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-env (>= 1.6.0, < 2.0.0)
google-cloud-storage (~> 1.31) google-cloud-storage (~> 1.31)
highline (~> 2.0) highline (~> 2.0)
http-cookie (~> 1.0.5)
json (< 3.0.0) json (< 3.0.0)
jwt (>= 2.1.0, < 3) jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0) mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0) multipart-post (>= 2.0.0, < 3.0.0)
naturally (~> 2.2) naturally (~> 2.2)
optparse (~> 0.1.1) optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0) plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0) rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3) security (= 0.1.5)
simctl (~> 1.6.3) simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0) terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0) terminal-table (~> 3)
tty-screen (>= 0.6.3, < 1.0.0) tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0) word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0) xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0) xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
gh_inspector (1.1.3) gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.45.0) google-apis-androidpublisher_v3 (0.54.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-core (0.11.0) google-apis-core (0.11.3)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.1, < 3.a)
@ -116,31 +120,29 @@ GEM
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml rexml
webrick
google-apis-iamcredentials_v1 (0.17.0) google-apis-iamcredentials_v1 (0.17.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-playcustomapp_v1 (0.13.0) google-apis-playcustomapp_v1 (0.13.0)
google-apis-core (>= 0.11.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-apis-storage_v1 (0.19.0) google-apis-storage_v1 (0.31.0)
google-apis-core (>= 0.9.0, < 2.a) google-apis-core (>= 0.11.0, < 2.a)
google-cloud-core (1.6.0) google-cloud-core (1.7.0)
google-cloud-env (~> 1.0) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0) google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0) faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.3.1) google-cloud-errors (1.4.0)
google-cloud-storage (1.44.0) google-cloud-storage (1.47.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1) google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.19.0) google-apis-storage_v1 (~> 0.31.0)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a) googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.6.0) googleauth (1.8.1)
faraday (>= 0.17.3, < 3.a) faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
@ -149,31 +151,33 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
httpclient (2.8.3) httpclient (2.8.3)
jmespath (1.6.2) jmespath (1.6.2)
json (2.6.3) json (2.7.2)
jwt (2.7.1) jwt (2.8.1)
memoist (0.16.2) base64
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.5)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.4.0)
nanaimo (0.3.0) nanaimo (0.3.0)
naturally (2.2.1) naturally (2.2.1)
optparse (0.1.1) nkf (0.2.0)
optparse (0.5.0)
os (1.1.4) os (1.1.4)
plist (3.7.0) plist (3.7.1)
public_suffix (5.0.3) public_suffix (5.0.5)
rake (13.0.6) rake (13.2.1)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.5) rexml (3.2.8)
strscan (>= 3.0.9)
rouge (2.0.7) rouge (2.0.7)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
security (0.1.3) security (0.1.5)
signet (0.17.0) signet (0.19.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0) jwt (>= 1.5, < 3.0)
@ -181,22 +185,19 @@ GEM
simctl (1.6.10) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
strscan (3.1.0)
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (1.8.0) terminal-table (3.0.2)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (>= 1.1.1, < 3)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-screen (0.8.1) tty-screen (0.8.2)
tty-spinner (0.9.3) tty-spinner (0.9.3)
tty-cursor (~> 0.7) tty-cursor (~> 0.7)
uber (0.1.0) uber (0.1.0)
unf (0.1.4) unicode-display_width (2.5.0)
unf_ext
unf_ext (0.0.8.2)
unicode-display_width (1.8.0)
webrick (1.8.1)
word_wrap (1.0.0) word_wrap (1.0.0)
xcodeproj (1.22.0) xcodeproj (1.24.0)
CFPropertyList (>= 2.3.3, < 4.0) CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3) atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0) claide (>= 1.0.2, < 2.0)

@ -1,33 +1,31 @@
@file:Suppress("UnstableApiUsage") @file:Suppress("UnstableApiUsage")
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
id("com.android.application") alias(libs.plugins.android.application)
id("checkstyle")
id("com.google.gms.google-services") id("com.google.gms.google-services")
id("com.google.firebase.crashlytics") id("com.google.firebase.crashlytics")
kotlin("android") kotlin("android")
kotlin("kapt")
id("dagger.hilt.android.plugin") id("dagger.hilt.android.plugin")
id("com.google.android.gms.oss-licenses-plugin") id("com.google.android.gms.oss-licenses-plugin")
id("kotlin-parcelize") alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.ksp)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose.compiler)
} }
repositories { kotlin {
mavenCentral() compilerOptions {
google() jvmTarget.set(JvmTarget.JVM_17)
maven { val composeReports = project.properties["composeMetrics"] ?: project.buildDir.absolutePath
url = uri("https://jitpack.io") freeCompilerArgs = listOf(
content { "-P",
includeGroup("com.github.tasks") "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${composeReports}/compose-metrics",
includeModule("com.github.bitfireAT", "cert4android") "-P",
includeModule("com.github.bitfireAT", "dav4jvm") "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${composeReports}/compose-metrics",
includeModule("com.github.tasks.opentasks", "opentasks-provider") )
includeModule("com.github.QuadFlask", "colorpicker")
includeModule("com.github.twofortyfouram", "android-plugin-api-for-locale")
includeModule("com.github.franmontiel", "PersistentCookieJar")
}
} }
} }
@ -51,23 +49,16 @@ android {
textReport = true textReport = true
} }
compileSdk = 33 compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig { defaultConfig {
testApplicationId = "org.tasks.test" testApplicationId = "org.tasks.test"
applicationId = "org.tasks" applicationId = "org.tasks"
versionCode = 130501 versionCode = 130909
versionName = "13.5.1" versionName = "13.9.9"
targetSdk = 33 targetSdk = libs.versions.android.targetSdk.get().toInt()
minSdk = 24 minSdk = libs.versions.android.minSdk.get().toInt()
testInstrumentationRunner = "org.tasks.TestRunner" testInstrumentationRunner = "org.tasks.TestRunner"
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
}
}
} }
signingConfigs { signingConfigs {
@ -90,20 +81,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
}
kotlinOptions {
jvmTarget = "17"
val composeReports = project.properties["composeMetrics"] ?: project.buildDir.absolutePath
freeCompilerArgs = listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${composeReports}/compose-metrics",
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${composeReports}/compose-metrics",
)
}
flavorDimensions += listOf("store") flavorDimensions += listOf("store")
@Suppress("LocalVariableName") @Suppress("LocalVariableName")
@ -138,21 +115,29 @@ android {
dimension = "store" dimension = "store"
} }
create("googleplay") { create("googleplay") {
isDefault = true
dimension = "store" dimension = "store"
} }
} }
packagingOptions { packaging {
resources { resources {
excludes += setOf("META-INF/*.kotlin_module") excludes += setOf("META-INF/*.kotlin_module", "META-INF/INDEX.LIST")
} }
} }
namespace = "org.tasks" testOptions {
} managedDevices {
localDevices {
create("pixel2api30") {
device = "Pixel 2"
apiLevel = 30
systemImageSource = "aosp-atd"
}
}
}
}
configure<CheckstyleExtension> { namespace = "org.tasks"
configFile = project.file("google_checks.xml")
toolVersion = "8.16"
} }
configurations.all { configurations.all {
@ -169,6 +154,7 @@ val genericImplementation by configurations
val googleplayImplementation by configurations val googleplayImplementation by configurations
dependencies { dependencies {
implementation(projects.data)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bitfire.dav4jvm) { implementation(libs.bitfire.dav4jvm) {
exclude(group = "junit") exclude(group = "junit")
@ -189,29 +175,37 @@ dependencies {
implementation(libs.dmfs.jems) implementation(libs.dmfs.jems)
implementation(libs.dagger.hilt) implementation(libs.dagger.hilt)
kapt(libs.dagger.hilt.compiler) ksp(libs.dagger.hilt.compiler)
kapt(libs.androidx.hilt.compiler) ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.work) implementation(libs.androidx.hilt.work)
implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.runtime) implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.room) implementation(libs.androidx.room)
kapt(libs.androidx.room.compiler) implementation(libs.androidx.sqlite)
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.bundles.markwon) implementation(libs.markwon)
implementation(libs.markwon.editor)
implementation(libs.markwon.linkify)
implementation(libs.markwon.strikethrough)
implementation(libs.markwon.tables)
implementation(libs.markwon.tasklist)
debugImplementation(libs.bundles.flipper) debugImplementation(libs.facebook.flipper)
debugImplementation(libs.facebook.flipper.network)
debugImplementation(libs.facebook.soloader)
debugImplementation(libs.leakcanary) debugImplementation(libs.leakcanary)
debugImplementation("androidx.compose.ui:ui-tooling") debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation(libs.kotlin.reflect) debugImplementation(libs.kotlin.reflect)
implementation(libs.kotlin.jdk8) implementation(libs.kotlin.jdk8)
implementation(libs.kotlin.immutable)
implementation(libs.kotlinx.serialization)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.persistent.cookiejar) implementation(libs.persistent.cookiejar)
implementation(libs.gson)
implementation(libs.material) implementation(libs.material)
implementation(libs.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.swiperefreshlayout) implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.preference) implementation(libs.androidx.preference)
@ -232,7 +226,8 @@ dependencies {
implementation(libs.colorpicker) implementation(libs.colorpicker)
implementation(libs.appauth) implementation(libs.appauth)
implementation(libs.osmdroid) implementation(libs.osmdroid)
implementation(libs.bundles.retrofit) implementation(libs.retrofit)
implementation(libs.retrofit.moshi)
implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerview)
implementation(platform(libs.androidx.compose)) implementation(platform(libs.androidx.compose))
@ -246,7 +241,10 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation("androidx.compose.ui:ui-viewbinding") implementation("androidx.compose.ui:ui-viewbinding")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation(libs.bundles.coil) implementation(libs.coil.compose)
implementation(libs.coil.video)
implementation(libs.coil.svg)
implementation(libs.coil.gif)
implementation(libs.accompanist.flowlayout) implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.permissions) implementation(libs.accompanist.permissions)
@ -266,8 +264,8 @@ dependencies {
googleplayImplementation(libs.play.services.oss.licenses) googleplayImplementation(libs.play.services.oss.licenses)
androidTestImplementation(libs.dagger.hilt.testing) androidTestImplementation(libs.dagger.hilt.testing)
kaptAndroidTest(libs.dagger.hilt.compiler) kspAndroidTest(libs.dagger.hilt.compiler)
kaptAndroidTest(libs.androidx.hilt.compiler) kspAndroidTest(libs.androidx.hilt.compiler)
androidTestImplementation(libs.mockito.android) androidTestImplementation(libs.mockito.android)
androidTestImplementation(libs.make.it.easy) androidTestImplementation(libs.make.it.easy)
androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.runner)

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

@ -4,7 +4,7 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
@ -15,9 +15,9 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.data.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase

@ -12,6 +12,9 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.* import org.tasks.data.*
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskContainerMaker.PARENT import org.tasks.makers.TaskContainerMaker.PARENT

@ -4,7 +4,6 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
@ -14,14 +13,14 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavCalendarMaker
import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.CaldavTaskMaker.newCaldavTask
@ -42,7 +41,7 @@ class GoogleTaskManualSortAdapterTest : InjectingTestCase() {
private lateinit var adapter: GoogleTaskManualSortAdapter private lateinit var adapter: GoogleTaskManualSortAdapter
private val tasks = ArrayList<TaskContainer>() private val tasks = ArrayList<TaskContainer>()
private val filter = GtasksFilter(newCaldavCalendar(with(CaldavCalendarMaker.UUID, "1234"))) private val filter = GtasksFilter(CaldavCalendar(uuid = "1234"))
private val dataSource = object : TaskAdapterDataSource { private val dataSource = object : TaskAdapterDataSource {
override fun getItem(position: Int) = tasks[position] override fun getItem(position: Int) = tasks[position]

@ -6,7 +6,7 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.core.BuiltInFilterExposer import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
@ -15,8 +15,8 @@ import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListQuery.getQuery import org.tasks.data.TaskListQuery.getQuery
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase

@ -4,7 +4,7 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.core.BuiltInFilterExposer import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

@ -1,106 +1,255 @@
package com.todoroo.astrid.alarms package com.todoroo.astrid.alarms
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.tasks.data.Alarm import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME import org.tasks.data.createDueDate
import org.tasks.data.Alarm.Companion.TYPE_RANDOM import org.tasks.data.dao.TaskDao
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.entity.Alarm
import org.tasks.data.Alarm.Companion.whenDue import org.tasks.data.entity.Notification
import org.tasks.data.Alarm.Companion.whenOverdue import org.tasks.data.entity.Task
import org.tasks.data.AlarmDao
import org.tasks.data.TaskDao
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.jobs.AlarmEntry
import org.tasks.jobs.NotificationQueue
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_DATE
import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.REMINDER_LAST
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class AlarmJobServiceTest : InjectingTestCase() { class AlarmJobServiceTest : InjectingTestCase() {
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var jobs: NotificationQueue
@Inject lateinit var alarmService: AlarmService @Inject lateinit var alarmService: AlarmService
@Test @Test
fun scheduleAlarm() = runBlocking { fun testNoAlarms() = runBlocking {
val task = taskDao.createNew(newTask()) testResults(emptyList(), 0)
val alarm = insertAlarm(Alarm(task, DateTime(2017, 9, 24, 19, 57).millis, TYPE_DATE_TIME))
verify(AlarmEntry(alarm, task, DateTime(2017, 9, 24, 19, 57).millis, TYPE_DATE_TIME))
} }
@Test @Test
fun ignoreStaleAlarm() = runBlocking { fun futureAlarmWithNoPastAlarm() = runBlocking {
val alarmTime = DateTime(2017, 9, 24, 19, 57) freezeAt(DateTime(2024, 5, 17, 23, 20)) {
val task = taskDao.createNew(newTask(with(REMINDER_LAST, alarmTime.endOfMinute()))) taskDao.insert(
alarmDao.insert(Alarm(task, alarmTime.millis, TYPE_DATE_TIME)) Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 18).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
verify() testResults(emptyList(), DateTime(2024, 5, 18, 18, 0).millis)
}
} }
@Test @Test
fun dontScheduleReminderForCompletedTask() = runBlocking { fun pastAlarmWithNoFutureAlarm() = runBlocking {
val task = taskDao.insert( freezeAt(DateTime(2024, 5, 17, 23, 20)) {
newTask( taskDao.insert(
with(DUE_DATE, newDateTime()), Task(
with(COMPLETION_TIME, newDateTime()) dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
0
)
}
}
@Test
fun pastRecurringAlarmWithFutureRecurrence() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(
Alarm(
type = Alarm.TYPE_REL_END,
repeat = 1,
interval = TimeUnit.HOURS.toMillis(6)
)
)
) )
)
alarmDao.insert(whenDue(task))
verify() testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
DateTime(2024, 5, 18, 0, 0).millis
)
}
} }
@Test @Test
fun dontScheduleReminderForDeletedTask() = runBlocking { fun pastAlarmsRemoveSnoozed() = runBlocking {
val task = taskDao.insert( freezeAt(DateTime(2024, 5, 17, 23, 20)) {
newTask( taskDao.insert(
with(DUE_DATE, newDateTime()), Task(
with(DELETION_TIME, newDateTime()) dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(
Alarm(type = Alarm.TYPE_REL_END),
Alarm(time = DateTimeUtils2.currentTimeMillis(), type = Alarm.TYPE_SNOOZE)
)
) )
)
alarmDao.insert(whenDue(task))
verify() testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
0
)
assertEquals(
listOf(Alarm(id = 1, task = 1, time = 0, type = Alarm.TYPE_REL_END)),
alarmService.getAlarms(1)
)
}
} }
@Test @Test
fun snoozeOverridesAll() = runBlocking { fun futureSnoozeOverrideOverdue() = runBlocking {
val now = newDateTime() freezeAt(DateTime(2024, 5, 17, 23, 20)) {
val task = taskDao.insert(newTask(with(DUE_TIME, now))) taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
)
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(
Alarm(type = Alarm.TYPE_REL_END),
Alarm(
time = DateTimeUtils2.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5),
type = Alarm.TYPE_SNOOZE
)
)
)
alarmDao.insert(whenDue(task)) testResults(
alarmDao.insert(whenOverdue(task)) emptyList(),
alarmDao.insert(Alarm(task, DateUtilities.ONE_HOUR, TYPE_RANDOM)) DateTimeUtils2.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)
val alarm = alarmDao.insert(Alarm(task, now.plusMonths(12).millis, TYPE_SNOOZE)) )
}
}
verify(AlarmEntry(alarm, task, now.plusMonths(12).millis, TYPE_SNOOZE)) @Test
fun ignoreStaleAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
reminderLast = DateTime(2024, 5, 17, 18, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
} }
private suspend fun insertAlarm(alarm: Alarm): Long { @Test
alarm.id = alarmDao.insert(alarm) fun dontScheduleForCompletedTask() = runBlocking {
return alarm.id freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
completionDate = DateTime(2024, 5, 17, 14, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
} }
private suspend fun verify(vararg alarms: AlarmEntry) { @Test
alarmService.scheduleAllAlarms() fun dontScheduleForDeletedTask() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
deletionDate = DateTime(2024, 5, 17, 14, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
}
assertEquals(alarms.toList(), jobs.getJobs()) private suspend fun testResults(notifications: List<Notification>, nextAlarm: Long) {
val actualNextAlarm = alarmService.triggerAlarms {
assertEquals(notifications, it)
it.forEach { taskDao.setLastNotified(it.taskId, DateTimeUtils2.currentTimeMillis()) }
}
assertEquals(nextAlarm, actualNextAlarm)
} }
} }

@ -5,8 +5,7 @@
*/ */
package com.todoroo.astrid.dao package com.todoroo.astrid.dao
import com.todoroo.andlib.utility.DateUtilities import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
@ -15,6 +14,7 @@ import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -75,23 +75,23 @@ class TaskDaoTests : InjectingTestCase() {
// create hidden task // create hidden task
task = Task() task = Task()
task.title = "hidden" task.title = "hidden"
task.hideUntil = DateUtilities.now() + 10000 task.hideUntil = currentTimeMillis() + 10000
taskDao.createNew(task) taskDao.createNew(task)
// create task with deadlines // create task with deadlines
task = Task() task = Task()
task.title = "deadlineInFuture" task.title = "deadlineInFuture"
task.dueDate = DateUtilities.now() + 10000 task.dueDate = currentTimeMillis() + 10000
taskDao.createNew(task) taskDao.createNew(task)
task = Task() task = Task()
task.title = "deadlineInPast" task.title = "deadlineInPast"
task.dueDate = DateUtilities.now() - 10000 task.dueDate = currentTimeMillis() - 10000
taskDao.createNew(task) taskDao.createNew(task)
// create completed task // create completed task
task = Task() task = Task()
task.title = "completed" task.title = "completed"
task.completionDate = DateUtilities.now() - 10000 task.completionDate = currentTimeMillis() - 10000
taskDao.createNew(task) taskDao.createNew(task)
// check is active // check is active

@ -11,15 +11,12 @@ import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.CaldavAccount import org.tasks.data.dao.CaldavDao
import org.tasks.data.CaldavDao import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.GoogleTaskListDao import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavCalendarMaker.ID
import org.tasks.makers.CaldavCalendarMaker.NAME
import org.tasks.makers.CaldavCalendarMaker.UUID
import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar
import org.tasks.makers.RemoteGtaskListMaker import org.tasks.makers.RemoteGtaskListMaker
import org.tasks.makers.RemoteGtaskListMaker.newRemoteList import org.tasks.makers.RemoteGtaskListMaker.newRemoteList
import javax.inject.Inject import javax.inject.Inject
@ -46,13 +43,14 @@ class GtasksListServiceTest : InjectingTestCase() {
newRemoteList( newRemoteList(
with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "Default"))) with(RemoteGtaskListMaker.REMOTE_ID, "1"), with(RemoteGtaskListMaker.NAME, "Default")))
assertEquals( assertEquals(
newCaldavCalendar(with(ID, 1L), with(UUID, "1"), with(NAME, "Default")), CaldavCalendar(id = 1, account = "account", uuid = "1", name = "Default"),
googleTaskListDao.getById(1L)) googleTaskListDao.getById(1L)
)
} }
@Test @Test
fun testGetListByRemoteId() = runBlocking { fun testGetListByRemoteId() = runBlocking {
val list = newCaldavCalendar(with(UUID, "1")) val list = CaldavCalendar(uuid = "1")
list.id = googleTaskListDao.insertOrReplace(list) list.id = googleTaskListDao.insertOrReplace(list)
assertEquals(list, googleTaskListDao.getByRemoteId("1")) assertEquals(list, googleTaskListDao.getByRemoteId("1"))
} }
@ -64,18 +62,19 @@ class GtasksListServiceTest : InjectingTestCase() {
@Test @Test
fun testDeleteMissingList() = runBlocking { fun testDeleteMissingList() = runBlocking {
googleTaskListDao.insertOrReplace(newCaldavCalendar(with(ID, 1L), with(UUID, "1"))) googleTaskListDao.insertOrReplace(CaldavCalendar(id = 1, account = "account", uuid = "1"))
val taskList = newRemoteList(with(RemoteGtaskListMaker.REMOTE_ID, "2")) val taskList = newRemoteList(with(RemoteGtaskListMaker.REMOTE_ID, "2"))
setLists(taskList) setLists(taskList)
assertEquals( assertEquals(
listOf(newCaldavCalendar(with(ID, 2L), with(UUID, "2"), with(NAME, "Default"))), listOf(CaldavCalendar(id = 2, account = "account", uuid = "2", name = "Default")),
googleTaskListDao.getLists("account")) googleTaskListDao.getLists("account")
)
} }
@Test @Test
fun testUpdateListName() = runBlocking { fun testUpdateListName() = runBlocking {
googleTaskListDao.insertOrReplace( googleTaskListDao.insertOrReplace(
newCaldavCalendar(with(ID, 1L), with(UUID, "1"), with(NAME, "oldName")) CaldavCalendar(id = 1, uuid = "1", name = "oldName", account = "account")
) )
setLists( setLists(
newRemoteList( newRemoteList(
@ -90,10 +89,10 @@ class GtasksListServiceTest : InjectingTestCase() {
} }
private suspend fun setLists(vararg list: TaskList) { private suspend fun setLists(vararg list: TaskList) {
val account = CaldavAccount().apply { val account = CaldavAccount(
username = "account" username = "account",
uuid = "account" uuid = "account",
} )
caldavDao.insert(account) caldavDao.insert(account)
gtasksListService.updateLists(account, listOf(*list)) gtasksListService.updateLists(account, listOf(*list))
} }

@ -1,7 +1,7 @@
package com.todoroo.astrid.model package com.todoroo.astrid.model
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -10,7 +10,7 @@ import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeClock import org.tasks.SuspendFreeze.Companion.freezeClock
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -23,7 +23,7 @@ class TaskTest : InjectingTestCase() {
freezeClock { freezeClock {
val task = Task() val task = Task()
taskDao.createNew(task) taskDao.createNew(task)
assertEquals(DateTimeUtils.currentTimeMillis(), task.creationDate) assertEquals(currentTimeMillis(), task.creationDate)
} }
} }

@ -1,50 +1,68 @@
package com.todoroo.astrid.repeats package com.todoroo.astrid.repeats
import com.natpryce.makeiteasy.MakeItEasy.with import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskCompleter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.TaskDao import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.COMPLETION_TIME import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.RECUR
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class RepeatWithSubtasksTests : InjectingTestCase() { class RepeatWithSubtasksTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var repeat: RepeatTaskHelper @Inject lateinit var taskCompleter: TaskCompleter
@Test @Test
fun uncompleteGrandchildren() = runBlocking { fun uncompleteGrandchildren() = runBlocking {
val grandparent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY"))) val grandparent = taskDao.createNew(
val parent = taskDao.createNew(newTask(with(PARENT, grandparent))) Task(
val child = taskDao.createNew(newTask( recurrence = "RRULE:FREQ=DAILY"
with(PARENT, parent), )
with(COMPLETION_TIME, DateTime()) )
)) val parent = taskDao.createNew(
Task(
parent = grandparent
)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
repeat.handleRepeat(taskDao.fetch(grandparent)!!) assertTrue(taskDao.fetch(child)!!.isCompleted)
taskCompleter.setComplete(grandparent)
assertFalse(taskDao.fetch(child)!!.isCompleted) assertFalse(taskDao.fetch(child)!!.isCompleted)
} }
@Test @Test
fun uncompleteGoogleTaskChildren() = runBlocking { fun uncompleteGoogleTaskChildren() = runBlocking {
val parent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY"))) val parent = taskDao.createNew(
val child = taskDao.createNew(newTask( Task(
with(PARENT, parent), recurrence = "RRULE:FREQ=DAILY"
with(COMPLETION_TIME, DateTime()) )
)) )
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
assertTrue(taskDao.fetch(child)!!.isCompleted)
repeat.handleRepeat(taskDao.fetch(parent)!!) taskCompleter.setComplete(parent)
assertFalse(taskDao.fetch(child)!!.isCompleted) assertFalse(taskDao.fetch(child)!!.isCompleted)
} }

@ -5,14 +5,14 @@
*/ */
package com.todoroo.astrid.service package com.todoroo.astrid.service
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.utility.TitleParser import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.tasks.data.TagDataDao import org.tasks.data.dao.TagDataDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import java.util.* import java.util.*

@ -1,10 +1,12 @@
package com.todoroo.astrid.service package com.todoroo.astrid.service
import com.todoroo.astrid.api.PermaSql.* import com.todoroo.astrid.api.PermaSql.VALUE_EOD
import com.todoroo.astrid.data.Task import com.todoroo.astrid.api.PermaSql.VALUE_EOD_NEXT_WEEK
import com.todoroo.astrid.data.Task.Companion.DUE_DATE import com.todoroo.astrid.api.PermaSql.VALUE_EOD_TOMORROW
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY 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.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -12,6 +14,7 @@ import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.tasks.R import org.tasks.R
import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.createDueDate
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
@ -35,7 +38,7 @@ class TaskCreatorTest : InjectingTestCase() {
assertEquals(DateTime(2021, 2, 4).millis, task.hideUntil) assertEquals(DateTime(2021, 2, 4).millis, task.hideUntil)
assertEquals( assertEquals(
Task.createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis), createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis),
task.dueDate task.dueDate
) )
} }
@ -63,7 +66,7 @@ class TaskCreatorTest : InjectingTestCase() {
assertEquals(DateTime(2021, 2, 4).millis, task.hideUntil) assertEquals(DateTime(2021, 2, 4).millis, task.hideUntil)
assertEquals( assertEquals(
Task.createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 4).millis), createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 4).millis),
task.dueDate task.dueDate
) )
} }
@ -93,7 +96,7 @@ class TaskCreatorTest : InjectingTestCase() {
} }
assertEquals( assertEquals(
Task.createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis), createDueDate(URGENCY_SPECIFIC_DAY, DateTime(2021, 2, 5).millis),
task.dueDate task.dueDate
) )
} }

@ -1,22 +1,15 @@
package com.todoroo.astrid.service package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with import org.tasks.data.entity.Task
import com.todoroo.astrid.core.BuiltInFilterExposer.Companion.getMyTasksFilter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.GoogleTaskDao import org.tasks.data.dao.TaskDao
import org.tasks.data.TaskDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.RECUR
import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -24,75 +17,26 @@ import javax.inject.Inject
class TaskDeleterTest : InjectingTestCase() { class TaskDeleterTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter @Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Test @Test
fun clearCompletedTask() = runBlocking { fun markTaskAsDeleted() = runBlocking {
val task = taskDao.createNew(newTask(with(COMPLETION_TIME, DateTime()))) val task = Task()
taskDao.createNew(task)
clearCompleted() taskDeleter.markDeleted(task)
assertTrue(taskDao.fetch(task)!!.isDeleted) assertTrue(taskDao.fetch(task.id)!!.isDeleted)
} }
@Test @Test
fun dontDeleteTaskWithRecurringParent() = runBlocking { fun dontDeleteReadOnlyTasks() = runBlocking {
val parent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1"))) val task = Task(
val child = taskDao.createNew(newTask( readOnly = true
with(PARENT, parent), )
with(COMPLETION_TIME, DateTime()) taskDao.createNew(task)
))
clearCompleted() taskDeleter.markDeleted(task)
assertFalse(taskDao.fetch(child)!!.isDeleted) assertFalse(taskDao.fetch(task.id)!!.isDeleted)
} }
}
@Test
fun dontDeleteTaskWithRecurringGrandparent() = runBlocking {
val grandparent = taskDao.createNew(newTask(with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1")))
val parent = taskDao.createNew(newTask(with(PARENT, grandparent)))
val child = taskDao.createNew(newTask(
with(PARENT, parent),
with(COMPLETION_TIME, DateTime())
))
clearCompleted()
assertFalse(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearGrandchildWithNoRecurringAncestors() = runBlocking {
val grandparent = taskDao.createNew(newTask())
val parent = taskDao.createNew(newTask(with(PARENT, grandparent)))
val child = taskDao.createNew(newTask(
with(PARENT, parent),
with(COMPLETION_TIME, DateTime())
))
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearGrandchildWithCompletedRecurringAncestor() = runBlocking {
val grandparent = taskDao.createNew(newTask(
with(RECUR, "RRULE:FREQ=DAILY;INTERVAL=1"),
with(COMPLETION_TIME, DateTime())
))
val parent = taskDao.createNew(newTask(with(PARENT, grandparent)))
val child = taskDao.createNew(newTask(
with(PARENT, parent),
with(COMPLETION_TIME, DateTime())
))
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
private suspend fun clearCompleted() =
taskDeleter.clearCompleted(getMyTasksFilter(context.resources))
}

@ -11,20 +11,15 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV import org.tasks.data.dao.CaldavDao
import org.tasks.data.CaldavAccount.Companion.TYPE_GOOGLE_TASKS import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.CaldavCalendar import org.tasks.data.entity.CaldavAccount
import org.tasks.data.CaldavDao import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.GoogleTaskDao import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.makers.CaldavAccountMaker
import org.tasks.makers.CaldavAccountMaker.ACCOUNT_TYPE
import org.tasks.makers.CaldavAccountMaker.newCaldavAccount
import org.tasks.makers.CaldavCalendarMaker
import org.tasks.makers.CaldavCalendarMaker.ACCOUNT
import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
@ -48,8 +43,8 @@ class TaskMoverTest : InjectingTestCase() {
@Before @Before
fun setup() { fun setup() {
runBlocking { runBlocking {
caldavDao.insert(newCaldavCalendar(with(CaldavCalendarMaker.UUID, "1"), with(ACCOUNT, "account1"))) caldavDao.insert(CaldavCalendar(uuid = "1", account = "account1"))
caldavDao.insert(newCaldavCalendar(with(CaldavCalendarMaker.UUID, "2"), with(ACCOUNT, "account2"))) caldavDao.insert(CaldavCalendar(uuid = "2", account = "account2"))
} }
} }
@ -311,7 +306,7 @@ class TaskMoverTest : InjectingTestCase() {
} }
private suspend fun moveToGoogleTasks(list: String, vararg tasks: Long) { private suspend fun moveToGoogleTasks(list: String, vararg tasks: Long) {
taskMover.move(tasks.toList(), GtasksFilter(newCaldavCalendar(with(CaldavCalendarMaker.UUID, list)))) taskMover.move(tasks.toList(), GtasksFilter(CaldavCalendar(uuid = list)))
} }
private suspend fun moveToCaldavList(calendar: String, vararg tasks: Long) { private suspend fun moveToCaldavList(calendar: String, vararg tasks: Long) {
@ -319,6 +314,11 @@ class TaskMoverTest : InjectingTestCase() {
} }
private suspend fun setAccountType(account: String, type: Int) { private suspend fun setAccountType(account: String, type: Int) {
caldavDao.insert(newCaldavAccount(with(CaldavAccountMaker.UUID, account), with(ACCOUNT_TYPE, type))) caldavDao.insert(
CaldavAccount(
uuid = account,
accountType = type,
)
)
} }
} }

@ -5,7 +5,7 @@
*/ */
package com.todoroo.astrid.service package com.todoroo.astrid.service
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.utility.TitleParser import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
@ -16,7 +16,7 @@ import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.tasks.R import org.tasks.R
import org.tasks.data.TagDataDao import org.tasks.data.dao.TagDataDao
import org.tasks.date.DateTimeUtils import org.tasks.date.DateTimeUtils
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule

@ -3,7 +3,6 @@
package com.todoroo.astrid.service package com.todoroo.astrid.service
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -12,12 +11,12 @@ import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.TestUtilities.assertEquals import org.tasks.TestUtilities.assertEquals
import org.tasks.caldav.VtodoCache import org.tasks.caldav.VtodoCache
import org.tasks.data.CaldavCalendar import org.tasks.data.dao.CaldavDao
import org.tasks.data.CaldavDao import org.tasks.data.dao.TaskDao
import org.tasks.data.TaskDao import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.CaldavTaskMaker.TASK
@ -44,7 +43,7 @@ class Upgrade_11_3_Test : InjectingTestCase() {
@Before @Before
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
calendar = newCaldavCalendar() calendar = CaldavCalendar()
runBlocking { runBlocking {
caldavDao.insert(calendar) caldavDao.insert(calendar)
} }

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

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

@ -1,20 +1,20 @@
package com.todoroo.astrid.subtasks package com.todoroo.astrid.subtasks
import androidx.test.InstrumentationRegistry import androidx.test.InstrumentationRegistry
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.AstridOrderingFilter
import com.todoroo.astrid.core.BuiltInFilterExposer import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.tasks.data.TaskListMetadataDao import org.tasks.data.dao.TaskListMetadataDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
abstract class SubtasksTestCase : InjectingTestCase() { abstract class SubtasksTestCase : InjectingTestCase() {
lateinit var updater: SubtasksFilterUpdater lateinit var updater: SubtasksFilterUpdater
lateinit var filter: Filter lateinit var filter: AstridOrderingFilter
@Inject lateinit var taskListMetadataDao: TaskListMetadataDao @Inject lateinit var taskListMetadataDao: TaskListMetadataDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences

@ -1,9 +1,9 @@
package com.todoroo.astrid.sync package com.todoroo.astrid.sync
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.dao.TagDataDao
import org.tasks.data.TagData import org.tasks.data.entity.TagData
import org.tasks.data.TagDataDao import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import javax.inject.Inject import javax.inject.Inject
@ -12,17 +12,17 @@ open class NewSyncTestCase : InjectingTestCase() {
@Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var tagDataDao: TagDataDao
suspend fun createTask(): Task { suspend fun createTask(): Task {
val task = Task() val task = Task(
task.title = SYNC_TASK_TITLE title = SYNC_TASK_TITLE,
task.priority = SYNC_TASK_IMPORTANCE priority = SYNC_TASK_IMPORTANCE,
)
taskDao.createNew(task) taskDao.createNew(task)
return task return task
} }
suspend fun createTagData(): TagData { suspend fun createTagData(): TagData {
val tag = TagData() val tag = TagData(name = "new tag")
tag.name = "new tag" tagDataDao.insert(tag)
tagDataDao.createNew(tag)
return tag return tag
} }

@ -1,6 +1,6 @@
package com.todoroo.astrid.sync package com.todoroo.astrid.sync
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

@ -1,15 +1,15 @@
package org.tasks.caldav package org.tasks.caldav
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.helper.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.data.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.ETAG import org.tasks.makers.CaldavTaskMaker.ETAG
@ -25,12 +25,13 @@ class CaldavSynchronizerTest : CaldavTest() {
@Before @Before
override fun setUp() = runBlocking { override fun setUp() = runBlocking {
super.setUp() super.setUp()
account = CaldavAccount().apply { account = CaldavAccount(
uuid = UUIDHelper.newUUID() uuid = UUIDHelper.newUUID(),
username = "username" username = "username",
password = encryption.encrypt("password") password = encryption.encrypt("password"),
url = server.url("/remote.php/dav/calendars/user1/").toString() url = server.url("/remote.php/dav/calendars/user1/").toString(),
id = caldavDao.insert(this) ).let {
it.copy(id = caldavDao.insert(it))
} }
} }
@ -45,11 +46,13 @@ class CaldavSynchronizerTest : CaldavTest() {
@Test @Test
fun dontFetchCalendarIfCtagMatches() = runBlocking { fun dontFetchCalendarIfCtagMatches() = runBlocking {
caldavDao.insert(CaldavCalendar( caldavDao.insert(
account = this@CaldavSynchronizerTest.account.uuid, CaldavCalendar(
ctag = "http://sabre.io/ns/sync/1", account = this@CaldavSynchronizerTest.account.uuid,
url = "${this@CaldavSynchronizerTest.account.url}test-shared/", ctag = "http://sabre.io/ns/sync/1",
)) url = "${this@CaldavSynchronizerTest.account.url}test-shared/",
)
)
enqueue(OC_SHARE_PROPFIND) enqueue(OC_SHARE_PROPFIND)
sync() sync()

@ -9,8 +9,8 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.rules.Timeout import org.junit.rules.Timeout
import org.tasks.R import org.tasks.R
import org.tasks.data.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption import org.tasks.security.KeyStoreEncryption

@ -1,19 +1,19 @@
package org.tasks.caldav package org.tasks.caldav
import com.todoroo.astrid.helper.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.tasks.data.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.SERVER_OPEN_XCHANGE import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV import org.tasks.data.entity.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS import org.tasks.data.entity.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.CaldavAccount.Companion.TYPE_CALDAV import org.tasks.data.entity.CaldavAccount.Companion.TYPE_CALDAV
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -79,13 +79,14 @@ class ServerDetectionTest : CaldavTest() {
vararg headers: Pair<String, String>, vararg headers: Pair<String, String>,
accountType: Int = TYPE_CALDAV accountType: Int = TYPE_CALDAV
) { ) {
account = CaldavAccount().apply { account = CaldavAccount(
uuid = UUIDHelper.newUUID() uuid = UUIDHelper.newUUID(),
username = "username" username = "username",
password = encryption.encrypt("password") password = encryption.encrypt("password"),
url = server.url("/remote.php/dav/calendars/user1/").toString() url = server.url("/remote.php/dav/calendars/user1/").toString(),
id = caldavDao.insert(this) accountType = accountType,
this.accountType = accountType ).let {
it.copy(id = caldavDao.insert(it))
} }
this.headers.putAll(headers) this.headers.putAll(headers)
enqueue(NO_CALENDARS) enqueue(NO_CALENDARS)

@ -1,16 +1,16 @@
package org.tasks.caldav package org.tasks.caldav
import com.todoroo.astrid.helper.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Test import org.junit.Test
import org.tasks.data.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.PrincipalDao import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@ -22,12 +22,13 @@ class SharingMailboxDotOrgTest : CaldavTest() {
@Test @Test
fun ownerAccess() = runBlocking { fun ownerAccess() = runBlocking {
account = CaldavAccount().apply { account = CaldavAccount(
uuid = UUIDHelper.newUUID() uuid = UUIDHelper.newUUID(),
username = "3" username = "3",
password = encryption.encrypt("password") password = encryption.encrypt("password"),
url = server.url("/caldav/").toString() url = server.url("/caldav/").toString(),
id = caldavDao.insert(this) ).let {
it.copy(id = caldavDao.insert(it))
} }
val calendar = CaldavCalendar( val calendar = CaldavCalendar(
account = this@SharingMailboxDotOrgTest.account.uuid, account = this@SharingMailboxDotOrgTest.account.uuid,
@ -45,12 +46,13 @@ class SharingMailboxDotOrgTest : CaldavTest() {
@Test @Test
fun principalForSharee() = runBlocking { fun principalForSharee() = runBlocking {
account = CaldavAccount().apply { account = CaldavAccount(
uuid = UUIDHelper.newUUID() uuid = UUIDHelper.newUUID(),
username = "3" username = "3",
password = encryption.encrypt("password") password = encryption.encrypt("password"),
url = server.url("/caldav/").toString() url = server.url("/caldav/").toString(),
id = caldavDao.insert(this) ).let {
it.copy(id = caldavDao.insert(it))
} }
val calendar = CaldavCalendar( val calendar = CaldavCalendar(
account = this@SharingMailboxDotOrgTest.account.uuid, account = this@SharingMailboxDotOrgTest.account.uuid,

@ -1,16 +1,16 @@
package org.tasks.caldav package org.tasks.caldav
import com.todoroo.astrid.helper.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test import org.junit.Test
import org.tasks.data.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.PrincipalDao import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@ -21,12 +21,13 @@ class SharingOwncloudTest : CaldavTest() {
@Inject lateinit var principalDao: PrincipalDao @Inject lateinit var principalDao: PrincipalDao
private suspend fun setupAccount(user: String) { private suspend fun setupAccount(user: String) {
account = CaldavAccount().apply { account = CaldavAccount(
uuid = UUIDHelper.newUUID() uuid = UUIDHelper.newUUID(),
username = user username = user,
password = encryption.encrypt("password") password = encryption.encrypt("password"),
url = server.url("/remote.php/dav/calendars/$user/").toString() url = server.url("/remote.php/dav/calendars/$user/").toString(),
id = caldavDao.insert(this) ).let {
it.copy(id = caldavDao.insert(it))
} }
} }

@ -1,18 +1,18 @@
package org.tasks.caldav package org.tasks.caldav
import com.todoroo.astrid.helper.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.CaldavCalendar.Companion.INVITE_ACCEPTED import org.tasks.data.entity.CaldavCalendar.Companion.INVITE_ACCEPTED
import org.tasks.data.PrincipalDao import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@ -23,12 +23,13 @@ class SharingSabredavTest : CaldavTest() {
@Inject lateinit var principalDao: PrincipalDao @Inject lateinit var principalDao: PrincipalDao
private suspend fun setupAccount(user: String) { private suspend fun setupAccount(user: String) {
account = CaldavAccount().apply { account = CaldavAccount(
uuid = UUIDHelper.newUUID() uuid = UUIDHelper.newUUID(),
username = user username = user,
password = encryption.encrypt("password") password = encryption.encrypt("password"),
url = server.url("/calendars/$user/").toString() url = server.url("/calendars/$user/").toString(),
id = caldavDao.insert(this) ).let {
it.copy(id = caldavDao.insert(it))
} }
} }

@ -2,7 +2,6 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.andlib.utility.DateUtilities.now
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -10,11 +9,15 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Test import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.SuspendFreeze.Companion.freezeAt
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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskContainerMaker import org.tasks.makers.TaskContainerMaker
import org.tasks.makers.TaskContainerMaker.CREATED import org.tasks.makers.TaskContainerMaker.CREATED
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -101,7 +104,9 @@ class CaldavDaoShiftTests : InjectingTestCase() {
fun ignoreMovedTasksWhenShiftingDown() = runBlocking { fun ignoreMovedTasksWhenShiftingDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17) val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created)) addTask(with(CREATED, created))
caldavDao.update(caldavDao.getTask(tasks[0].id).apply { this?.deleted = now() }!!) caldavDao.update(caldavDao.getTask(tasks[0].id).apply { this?.deleted =
currentTimeMillis()
}!!)
caldavDao.shiftDown("calendar", 0, created.toAppleEpoch()) caldavDao.shiftDown("calendar", 0, created.toAppleEpoch())
@ -112,7 +117,7 @@ class CaldavDaoShiftTests : InjectingTestCase() {
fun ignoreDeletedTasksWhenShiftingDown() = runBlocking { fun ignoreDeletedTasksWhenShiftingDown() = runBlocking {
val created = DateTime(2020, 5, 17, 9, 53, 17) val created = DateTime(2020, 5, 17, 9, 53, 17)
addTask(with(CREATED, created)) addTask(with(CREATED, created))
taskDao.update(taskDao.fetch(tasks[0].id).apply { this?.deletionDate = now() }!!) taskDao.update(taskDao.fetch(tasks[0].id).apply { this?.deletionDate = currentTimeMillis() }!!)
caldavDao.shiftDown("calendar", 0, created.toAppleEpoch()) caldavDao.shiftDown("calendar", 0, created.toAppleEpoch())

@ -2,12 +2,18 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TagDao
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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.CREATION_TIME import org.tasks.makers.TaskMaker.CREATION_TIME
@ -85,8 +91,7 @@ class CaldavDaoTests : InjectingTestCase() {
@Test @Test
fun noResultsForEmptyAccounts() = runBlocking { fun noResultsForEmptyAccounts() = runBlocking {
val caldavAccount = CaldavAccount() val caldavAccount = CaldavAccount(uuid = UUIDHelper.newUUID())
caldavAccount.uuid = UUIDHelper.newUUID()
caldavDao.insert(caldavAccount) caldavDao.insert(caldavAccount)
assertTrue(caldavDao.getCaldavFilters(caldavAccount.uuid!!).isEmpty()) assertTrue(caldavDao.getCaldavFilters(caldavAccount.uuid!!).isEmpty())
} }

@ -2,13 +2,18 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.CaldavDao.Companion.LOCAL 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.CaldavCalendar
import org.tasks.data.entity.CaldavTask
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
@ -16,7 +21,7 @@ import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -43,7 +48,7 @@ class DeletionDaoTests : InjectingTestCase() {
deletionDao.markDeleted(listOf(task.id)) deletionDao.markDeleted(listOf(task.id))
task = taskDao.fetch(task.id)!! task = taskDao.fetch(task.id)!!
assertTrue(task.modificationDate > task.creationDate) assertTrue(task.modificationDate > task.creationDate)
assertTrue(task.modificationDate < DateTimeUtils.currentTimeMillis()) assertTrue(task.modificationDate < currentTimeMillis())
} }
@Test @Test
@ -53,7 +58,7 @@ class DeletionDaoTests : InjectingTestCase() {
deletionDao.markDeleted(listOf(task.id)) deletionDao.markDeleted(listOf(task.id))
task = taskDao.fetch(task.id)!! task = taskDao.fetch(task.id)!!
assertTrue(task.deletionDate > task.creationDate) assertTrue(task.deletionDate > task.creationDate)
assertTrue(task.deletionDate < DateTimeUtils.currentTimeMillis()) assertTrue(task.deletionDate < currentTimeMillis())
} }
@Test @Test

@ -9,12 +9,15 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.data.CaldavAccount.Companion.TYPE_GOOGLE_TASKS 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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavAccountMaker.ACCOUNT_TYPE
import org.tasks.makers.CaldavAccountMaker.newCaldavAccount
import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
@ -35,8 +38,8 @@ class GoogleTaskDaoTests : InjectingTestCase() {
override fun setUp() { override fun setUp() {
super.setUp() super.setUp()
runBlocking { runBlocking {
caldavDao.insert(newCaldavAccount(with(ACCOUNT_TYPE, TYPE_GOOGLE_TASKS))) caldavDao.insert(CaldavAccount(uuid = "account", accountType = TYPE_GOOGLE_TASKS))
caldavDao.insert(newCaldavCalendar()) caldavDao.insert(CaldavCalendar(account = "account", uuid = "calendar"))
} }
} }

@ -3,8 +3,11 @@ package org.tasks.data
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.assertTrue
import org.junit.Test 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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@ -17,10 +20,10 @@ class GoogleTaskListDaoTest : InjectingTestCase() {
@Test @Test
fun noResultsForEmptyAccount() = runBlocking { fun noResultsForEmptyAccount() = runBlocking {
val account = CaldavAccount().apply { val account = CaldavAccount(
uuid = "user@gmail.com" uuid = "user@gmail.com",
username = "user@gmail.com" username = "user@gmail.com",
} )
caldavDao.insert(account) caldavDao.insert(account)
assertTrue(googleTaskListDao.getGoogleTaskFilters(account.username!!).isEmpty()) assertTrue(googleTaskListDao.getGoogleTaskFilters(account.username!!).isEmpty())

@ -1,17 +1,24 @@
package org.tasks.data package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.caldav.GeoUtils.toLikeString import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.LocationDao
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.Geofence
import org.tasks.data.entity.Place
import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
@ -21,6 +28,7 @@ import org.tasks.makers.TaskMaker.DUE_TIME
import org.tasks.makers.TaskMaker.HIDE_TYPE import org.tasks.makers.TaskMaker.HIDE_TYPE
import org.tasks.makers.TaskMaker.ID import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -113,61 +121,89 @@ class LocationDaoTest : InjectingTestCase() {
@Test @Test
fun ignoreArrivalForSnoozedTask() = runBlocking { fun ignoreArrivalForSnoozedTask() = runBlocking {
freezeAt(now()).thawAfter { freezeAt(currentTimeMillis()).thawAfter {
val place = Place() val place = Place()
locationDao.insert(place) locationDao.insert(place)
val task = taskDao.createNew(newTask()) val task = taskDao.createNew(newTask())
alarmDao.insert(Alarm(task, newDateTime().plusMinutes(15).millis, TYPE_SNOOZE)) alarmDao.insert(
Alarm(
task = task,
time = newDateTime().plusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
locationDao.insert(Geofence(task = task, place = place.uid, isArrival = true)) locationDao.insert(Geofence(task = task, place = place.uid, isArrival = true))
assertTrue(locationDao.getArrivalGeofences(place.uid!!, now()).isEmpty()) assertTrue(locationDao.getArrivalGeofences(place.uid!!, currentTimeMillis()).isEmpty())
} }
} }
@Test @Test
fun ignoreDepartureForSnoozedTask() = runBlocking { fun ignoreDepartureForSnoozedTask() = runBlocking {
freezeAt(now()).thawAfter { freezeAt(currentTimeMillis()).thawAfter {
val place = Place() val place = Place()
locationDao.insert(place) locationDao.insert(place)
val task = taskDao.createNew(newTask()) val task = taskDao.createNew(newTask())
alarmDao.insert(Alarm(task, newDateTime().plusMinutes(15).millis, TYPE_SNOOZE)) alarmDao.insert(
Alarm(
task = task,
time = newDateTime().plusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
locationDao.insert(Geofence(task = task, place = place.uid, isDeparture = true)) locationDao.insert(Geofence(task = task, place = place.uid, isDeparture = true))
assertTrue(locationDao.getDepartureGeofences(place.uid!!, now()).isEmpty()) assertTrue(locationDao.getDepartureGeofences(place.uid!!, currentTimeMillis()).isEmpty())
} }
} }
@Test @Test
fun getArrivalWithElapsedSnooze() = runBlocking { fun getArrivalWithElapsedSnooze() = runBlocking {
freezeAt(now()).thawAfter { freezeAt(currentTimeMillis()).thawAfter {
val place = Place() val place = Place()
locationDao.insert(place) locationDao.insert(place)
val task = taskDao.createNew(newTask()) val task = taskDao.createNew(newTask())
alarmDao.insert(Alarm(task, newDateTime().minusMinutes(15).millis, TYPE_SNOOZE)) alarmDao.insert(
Alarm(
task = task,
time = newDateTime().minusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
val geofence = Geofence(task = task, place = place.uid, isArrival = true) val geofence = Geofence(task = task, place = place.uid, isArrival = true)
.let { it.copy(id = locationDao.insert(it)) } .let { it.copy(id = locationDao.insert(it)) }
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!, now())) assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!,
currentTimeMillis()
))
} }
} }
@Test @Test
fun getDepartureWithElapsedSnooze() = runBlocking { fun getDepartureWithElapsedSnooze() = runBlocking {
freezeAt(now()).thawAfter { freezeAt(currentTimeMillis()).thawAfter {
val place = Place() val place = Place()
locationDao.insert(place) locationDao.insert(place)
val task = taskDao.createNew(newTask()) val task = taskDao.createNew(newTask())
alarmDao.insert(Alarm(task, newDateTime().minusMinutes(15).millis, TYPE_SNOOZE)) alarmDao.insert(
Alarm(
task = task,
time = newDateTime().minusMinutes(15).millis,
type = TYPE_SNOOZE
)
)
val geofence = Geofence(task = task, place = place.uid, isDeparture = true) val geofence = Geofence(task = task, place = place.uid, isDeparture = true)
.let { it.copy(id = locationDao.insert(it)) } .let { it.copy(id = locationDao.insert(it)) }
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!, now())) assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!,
currentTimeMillis()
))
} }
} }
@Test @Test
fun ignoreArrivalForHiddenTask() = runBlocking { fun ignoreArrivalForHiddenTask() = runBlocking {
freezeAt(now()).thawAfter { freezeAt(currentTimeMillis()).thawAfter {
val place = Place() val place = Place()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask( taskDao.createNew(newTask(
@ -176,13 +212,13 @@ class LocationDaoTest : InjectingTestCase() {
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME))) with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true)) locationDao.insert(Geofence(task = 1, place = place.uid, isArrival = true))
assertTrue(locationDao.getArrivalGeofences(place.uid!!, now()).isEmpty()) assertTrue(locationDao.getArrivalGeofences(place.uid!!, currentTimeMillis()).isEmpty())
} }
} }
@Test @Test
fun ignoreDepartureForHiddenTask() = runBlocking { fun ignoreDepartureForHiddenTask() = runBlocking {
freezeAt(now()).thawAfter { freezeAt(currentTimeMillis()).thawAfter {
val place = Place() val place = Place()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask( taskDao.createNew(newTask(
@ -191,13 +227,13 @@ class LocationDaoTest : InjectingTestCase() {
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME))) with(HIDE_TYPE, Task.HIDE_UNTIL_DUE_TIME)))
locationDao.insert(Geofence(task = 1, place = place.uid, isDeparture = true)) locationDao.insert(Geofence(task = 1, place = place.uid, isDeparture = true))
assertTrue(locationDao.getDepartureGeofences(place.uid!!, now()).isEmpty()) assertTrue(locationDao.getDepartureGeofences(place.uid!!, currentTimeMillis()).isEmpty())
} }
} }
@Test @Test
fun getArrivalWithElapsedHideUntil() = runBlocking { fun getArrivalWithElapsedHideUntil() = runBlocking {
freezeAt(now()).thawAfter { freezeAt(currentTimeMillis()).thawAfter {
val place = Place() val place = Place()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask( taskDao.createNew(newTask(
@ -209,13 +245,15 @@ class LocationDaoTest : InjectingTestCase() {
it.copy(id = locationDao.insert(it)) it.copy(id = locationDao.insert(it))
} }
assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!, now())) assertEquals(listOf(geofence), locationDao.getArrivalGeofences(place.uid!!,
currentTimeMillis()
))
} }
} }
@Test @Test
fun getDepartureWithElapsedHideUntil() = runBlocking { fun getDepartureWithElapsedHideUntil() = runBlocking {
freezeAt(now()).thawAfter { freezeAt(currentTimeMillis()).thawAfter {
val place = Place() val place = Place()
locationDao.insert(place) locationDao.insert(place)
taskDao.createNew(newTask( taskDao.createNew(newTask(
@ -225,7 +263,9 @@ class LocationDaoTest : InjectingTestCase() {
val geofence = Geofence(task = 1, place = place.uid, isDeparture = true) val geofence = Geofence(task = 1, place = place.uid, isDeparture = true)
.let { it.copy(id = locationDao.insert(it)) } .let { it.copy(id = locationDao.insert(it)) }
assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!, now())) assertEquals(listOf(geofence), locationDao.getDepartureGeofences(place.uid!!,
currentTimeMillis()
))
} }
} }
} }

@ -3,7 +3,6 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -11,11 +10,12 @@ import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.tasks.R import org.tasks.R
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavAccountMaker.newCaldavAccount
import org.tasks.makers.CaldavCalendarMaker.UUID
import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.TASK import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.CaldavTaskMaker.newCaldavTask
@ -40,9 +40,9 @@ class ManualGoogleTaskQueryTest : InjectingTestCase() {
super.setUp() super.setUp()
preferences.clear() preferences.clear()
preferences.setBoolean(R.string.p_manual_sort, true) preferences.setBoolean(R.string.p_manual_sort, true)
val calendar = newCaldavCalendar(with(UUID, "1234")) val calendar = CaldavCalendar(uuid = "1234")
runBlocking { runBlocking {
caldavDao.insert(newCaldavAccount()) caldavDao.insert(CaldavAccount())
caldavDao.insert(calendar) caldavDao.insert(calendar)
} }
filter = GtasksFilter(calendar) filter = GtasksFilter(calendar)

@ -8,14 +8,12 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.TagDao
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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TagDataMaker.NAME
import org.tasks.makers.TagDataMaker.newTagData
import org.tasks.makers.TagMaker.TAGDATA
import org.tasks.makers.TagMaker.TAGUID
import org.tasks.makers.TagMaker.TASK
import org.tasks.makers.TagMaker.newTag
import org.tasks.makers.TaskMaker.ID import org.tasks.makers.TaskMaker.ID
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject import javax.inject.Inject
@ -29,13 +27,13 @@ class TagDataDaoTest : InjectingTestCase() {
@Test @Test
fun tagDataOrderedByNameIgnoresNullNames() = runBlocking { fun tagDataOrderedByNameIgnoresNullNames() = runBlocking {
tagDataDao.createNew(newTagData(with(NAME, null as String?))) tagDataDao.insert(TagData(name = null))
assertTrue(tagDataDao.tagDataOrderedByName().isEmpty()) assertTrue(tagDataDao.tagDataOrderedByName().isEmpty())
} }
@Test @Test
fun tagDataOrderedByNameIgnoresEmptyNames() = runBlocking { fun tagDataOrderedByNameIgnoresEmptyNames() = runBlocking {
tagDataDao.createNew(newTagData(with(NAME, ""))) tagDataDao.insert(TagData(name = ""))
assertTrue(tagDataDao.tagDataOrderedByName().isEmpty()) assertTrue(tagDataDao.tagDataOrderedByName().isEmpty())
} }
@ -46,20 +44,19 @@ class TagDataDaoTest : InjectingTestCase() {
@Test @Test
fun getTagWithCaseFixesCase() = runBlocking { fun getTagWithCaseFixesCase() = runBlocking {
tagDataDao.createNew(newTagData(with(NAME, "Derp"))) tagDataDao.insert(TagData(name = "Derp"))
assertEquals("Derp", tagDataDao.getTagWithCase("derp")) assertEquals("Derp", tagDataDao.getTagWithCase("derp"))
} }
@Test @Test
fun getTagsByName() = runBlocking { fun getTagsByName() = runBlocking {
val tagData = newTagData(with(NAME, "Derp")) val tagData = TagData(name = "Derp").let { it.copy(id = tagDataDao.insert(it)) }
tagDataDao.createNew(tagData)
assertEquals(listOf(tagData), tagDataDao.getTags(listOf("Derp"))) assertEquals(listOf(tagData), tagDataDao.getTags(listOf("Derp")))
} }
@Test @Test
fun getTagsByNameCaseSensitive() = runBlocking { fun getTagsByNameCaseSensitive() = runBlocking {
tagDataDao.createNew(newTagData(with(NAME, "Derp"))) tagDataDao.insert(TagData(name = "Derp"))
assertTrue(tagDataDao.getTags(listOf("derp")).isEmpty()) assertTrue(tagDataDao.getTags(listOf("derp")).isEmpty())
} }
@ -69,20 +66,18 @@ class TagDataDaoTest : InjectingTestCase() {
val taskTwo = newTask() val taskTwo = newTask()
taskDao.createNew(taskOne) taskDao.createNew(taskOne)
taskDao.createNew(taskTwo) taskDao.createNew(taskTwo)
val tagOne = newTagData(with(NAME, "one")) val tagOne = TagData(name = "one").let { it.copy(id = tagDataDao.insert(it)) }
val tagTwo = newTagData(with(NAME, "two")) val tagTwo = TagData(name = "two").let { it.copy(id = tagDataDao.insert(it)) }
tagDataDao.createNew(tagOne) tagDao.insert(Tag(task = taskOne.id, taskUid = taskOne.uuid, tagUid = tagOne.remoteId))
tagDataDao.createNew(tagTwo) tagDao.insert(Tag(task = taskTwo.id, taskUid = taskTwo.uuid, tagUid = tagTwo.remoteId))
tagDao.insert(newTag(with(TAGDATA, tagOne), with(TASK, taskOne)))
tagDao.insert(newTag(with(TAGDATA, tagTwo), with(TASK, taskTwo)))
assertEquals(listOf(tagOne), tagDataDao.getTagDataForTask(taskOne.id)) assertEquals(listOf(tagOne), tagDataDao.getTagDataForTask(taskOne.id))
} }
@Test @Test
fun getEmptyTagSelections() = runBlocking { fun getEmptyTagSelections() = runBlocking {
val selections = tagDataDao.getTagSelections(listOf(1L)) val selections = tagDataDao.getTagSelections(listOf(1L))
assertTrue(selections.first!!.isEmpty()) assertTrue(selections.first.isEmpty())
assertTrue(selections.second!!.isEmpty()) assertTrue(selections.second.isEmpty())
} }
@Test @Test
@ -97,7 +92,7 @@ class TagDataDaoTest : InjectingTestCase() {
fun getEmptyPartialSelections() = runBlocking { fun getEmptyPartialSelections() = runBlocking {
newTag(1, "tag1") newTag(1, "tag1")
newTag(2, "tag1") newTag(2, "tag1")
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).first!!.isEmpty()) assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).first.isEmpty())
} }
@Test @Test
@ -111,15 +106,15 @@ class TagDataDaoTest : InjectingTestCase() {
fun getEmptyCommonSelections() = runBlocking { fun getEmptyCommonSelections() = runBlocking {
newTag(1, "tag1") newTag(1, "tag1")
newTag(2, "tag2") newTag(2, "tag2")
assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).second!!.isEmpty()) assertTrue(tagDataDao.getTagSelections(listOf(1L, 2L)).second.isEmpty())
} }
@Test @Test
fun getSelectionsWithNoTags() = runBlocking { fun getSelectionsWithNoTags() = runBlocking {
newTag(1) newTag(1)
val selections = tagDataDao.getTagSelections(listOf(1L)) val selections = tagDataDao.getTagSelections(listOf(1L))
assertTrue(selections.first!!.isEmpty()) assertTrue(selections.first.isEmpty())
assertTrue(selections.second!!.isEmpty()) assertTrue(selections.second.isEmpty())
} }
@Test @Test
@ -128,14 +123,14 @@ class TagDataDaoTest : InjectingTestCase() {
newTag(2) newTag(2)
val selections = tagDataDao.getTagSelections(listOf(1L, 2L)) val selections = tagDataDao.getTagSelections(listOf(1L, 2L))
assertEquals(setOf("tag1"), selections.first) assertEquals(setOf("tag1"), selections.first)
assertTrue(selections.second!!.isEmpty()) assertTrue(selections.second.isEmpty())
} }
private suspend fun newTag(taskId: Long, vararg tags: String) { private suspend fun newTag(taskId: Long, vararg tags: String) {
val task = newTask(with(ID, taskId)) val task = newTask(with(ID, taskId))
taskDao.createNew(task) taskDao.createNew(task)
for (tag in tags) { for (tag in tags) {
tagDao.insert(newTag(with(TASK, task), with(TAGUID, tag))) tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = tag))
} }
} }
} }

@ -6,8 +6,7 @@
package org.tasks.data package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.andlib.utility.DateUtilities import org.tasks.data.entity.Task
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
@ -15,10 +14,12 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.PARENT import org.tasks.makers.TaskMaker.PARENT
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -44,23 +45,23 @@ class TaskDaoTests : InjectingTestCase() {
// create hidden task // create hidden task
task = Task() task = Task()
task.title = "hidden" task.title = "hidden"
task.hideUntil = DateUtilities.now() + 10000 task.hideUntil = currentTimeMillis() + 10000
taskDao.createNew(task) taskDao.createNew(task)
// create task with deadlines // create task with deadlines
task = Task() task = Task()
task.title = "deadlineInFuture" task.title = "deadlineInFuture"
task.dueDate = DateUtilities.now() + 10000 task.dueDate = currentTimeMillis() + 10000
taskDao.createNew(task) taskDao.createNew(task)
task = Task() task = Task()
task.title = "deadlineInPast" task.title = "deadlineInPast"
task.dueDate = DateUtilities.now() - 10000 task.dueDate = currentTimeMillis() - 10000
taskDao.createNew(task) taskDao.createNew(task)
// create completed task // create completed task
task = Task() task = Task()
task.title = "completed" task.title = "completed"
task.completionDate = DateUtilities.now() - 10000 task.completionDate = currentTimeMillis() - 10000
taskDao.createNew(task) taskDao.createNew(task)
// check is active // check is active

@ -8,10 +8,15 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TagDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.dao.UpgraderDao
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.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TagDataMaker
import org.tasks.makers.TagMaker
import org.tasks.makers.TaskMaker import org.tasks.makers.TaskMaker
import javax.inject.Inject import javax.inject.Inject
@ -29,12 +34,12 @@ class UpgraderDaoTests : InjectingTestCase() {
fun getCaldavTasksWithTags() = runBlocking { fun getCaldavTasksWithTags() = runBlocking {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.ID, 1L)) val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.ID, 1L))
taskDao.createNew(task) taskDao.createNew(task)
val one = TagDataMaker.newTagData() val one = TagData()
val two = TagDataMaker.newTagData() val two = TagData()
tagDataDao.createNew(one) tagDataDao.insert(one)
tagDataDao.createNew(two) tagDataDao.insert(two)
tagDao.insert(TagMaker.newTag(MakeItEasy.with(TagMaker.TASK, task), MakeItEasy.with(TagMaker.TAGDATA, one))) tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = one.remoteId))
tagDao.insert(TagMaker.newTag(MakeItEasy.with(TagMaker.TASK, task), MakeItEasy.with(TagMaker.TAGDATA, two))) tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = two.remoteId))
caldavDao.insert(CaldavTask(task = task.id, calendar = "calendar")) caldavDao.insert(CaldavTask(task = task.id, calendar = "calendar"))
assertEquals(listOf(task.id), upgraderDao.tasksWithTags()) assertEquals(listOf(task.id), upgraderDao.tasksWithTags())
} }
@ -43,9 +48,9 @@ class UpgraderDaoTests : InjectingTestCase() {
fun ignoreNonCaldavTaskWithTags() = runBlocking { fun ignoreNonCaldavTaskWithTags() = runBlocking {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.ID, 1L)) val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.ID, 1L))
taskDao.createNew(task) taskDao.createNew(task)
val tag = TagDataMaker.newTagData() val tag = TagData()
tagDataDao.createNew(tag) tagDataDao.insert(tag)
tagDao.insert(TagMaker.newTag(MakeItEasy.with(TagMaker.TASK, task), MakeItEasy.with(TagMaker.TAGDATA, tag))) tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = tag.remoteId))
assertTrue(upgraderDao.tasksWithTags().isEmpty()) assertTrue(upgraderDao.tasksWithTags().isEmpty())
} }
@ -53,7 +58,7 @@ class UpgraderDaoTests : InjectingTestCase() {
fun ignoreCaldavTaskWithoutTags() = runBlocking { fun ignoreCaldavTaskWithoutTags() = runBlocking {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.ID, 1L)) val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.ID, 1L))
taskDao.createNew(task) taskDao.createNew(task)
tagDataDao.createNew(TagDataMaker.newTagData()) tagDataDao.insert(TagData())
caldavDao.insert(CaldavTask(task = task.id, calendar = "calendar")) caldavDao.insert(CaldavTask(task = task.id, calendar = "calendar"))
assertTrue(upgraderDao.tasksWithTags().isEmpty()) assertTrue(upgraderDao.tasksWithTags().isEmpty())
} }

@ -2,7 +2,7 @@ package org.tasks.gtasks
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Test import org.junit.Test

@ -2,7 +2,7 @@ package org.tasks.injection
import android.content.Context import android.content.Context
import androidx.room.Room import androidx.room.Room
import com.todoroo.astrid.dao.Database import org.tasks.data.db.Database
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn

@ -8,7 +8,7 @@ package org.tasks.jobs
import android.net.Uri import android.net.Uri
import androidx.test.InstrumentationRegistry import androidx.test.InstrumentationRegistry
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

@ -1,7 +1,7 @@
package org.tasks.opentasks package org.tasks.opentasks
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

@ -1,11 +1,13 @@
package org.tasks.opentasks package org.tasks.opentasks
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.TestUtilities.withTZ import org.tasks.TestUtilities.withTZ
@ -13,27 +15,25 @@ import org.tasks.caldav.iCalendar.Companion.collapsed
import org.tasks.caldav.iCalendar.Companion.order import org.tasks.caldav.iCalendar.Companion.order
import org.tasks.caldav.iCalendar.Companion.parent import org.tasks.caldav.iCalendar.Companion.parent
import org.tasks.caldav.iCalendar.Companion.snooze import org.tasks.caldav.iCalendar.Companion.snooze
import org.tasks.data.Alarm import org.tasks.data.dao.AlarmDao
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.dao.TagDao
import org.tasks.data.AlarmDao import org.tasks.data.dao.TagDataDao
import org.tasks.data.TagDao import org.tasks.data.entity.Alarm
import org.tasks.data.TagDataDao 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.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker import org.tasks.makers.CaldavTaskMaker
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.newCaldavTask import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TagDataMaker.NAME
import org.tasks.makers.TagDataMaker.newTagData
import org.tasks.makers.TagMaker.TAGDATA
import org.tasks.makers.TagMaker.TASK
import org.tasks.makers.TagMaker.newTag
import org.tasks.makers.TaskMaker import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.COLLAPSED import org.tasks.makers.TaskMaker.COLLAPSED
import org.tasks.makers.TaskMaker.ORDER import org.tasks.makers.TaskMaker.ORDER
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.util.* import java.util.TimeZone
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -90,8 +90,7 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
@Test @Test
fun matchExistingTag() = runBlocking { fun matchExistingTag() = runBlocking {
val (_, list) = withVtodo(ONE_TAG) val (_, list) = withVtodo(ONE_TAG)
val tag = newTagData(with(NAME, "Tag1")) val tag = TagData(name = "Tag1").let { it.copy(id = tagDataDao.insert(it)) }
tagDataDao.createNew(tag)
synchronizer.sync() synchronizer.sync()
@ -213,7 +212,14 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
?.let { taskDao.fetch(it.task) } ?.let { taskDao.fetch(it.task) }
assertEquals( assertEquals(
listOf(Alarm(task!!.id, 1612972355000, TYPE_SNOOZE).apply { id = 1 }), listOf(
Alarm(
id = 1,
task = task!!.id,
time = 1612972355000,
type = TYPE_SNOOZE
)
),
alarmDao.getAlarms(task.id) alarmDao.getAlarms(task.id)
) )
} }
@ -222,7 +228,13 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
fun pushSnoozeTime() = withTZ(CHICAGO) { fun pushSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = openTaskDao.insertList() val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask()) val taskId = taskDao.createNew(newTask())
alarmDao.insert(Alarm(taskId, DateTime(2021, 2, 4, 13, 30).millis, TYPE_SNOOZE)) alarmDao.insert(
Alarm(
task = taskId,
time = DateTime(2021, 2, 4, 13, 30).millis,
type = TYPE_SNOOZE
)
)
caldavDao.insert(newCaldavTask( caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid), with(CALENDAR, list.uuid),
@ -241,7 +253,13 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
fun dontPushLapsedSnoozeTime() = withTZ(CHICAGO) { fun dontPushLapsedSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = openTaskDao.insertList() val (listId, list) = openTaskDao.insertList()
val taskId = taskDao.createNew(newTask()) val taskId = taskDao.createNew(newTask())
alarmDao.insert(Alarm(taskId, DateTime(2021, 2, 4, 13, 30).millis, TYPE_SNOOZE)) alarmDao.insert(
Alarm(
task = taskId,
time = DateTime(2021, 2, 4, 13, 30).millis,
type = TYPE_SNOOZE
)
)
caldavDao.insert(newCaldavTask( caldavDao.insert(newCaldavTask(
with(CALENDAR, list.uuid), with(CALENDAR, list.uuid),
@ -257,17 +275,18 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
} }
@Test @Test
fun removeSnoozeTime() = runBlocking { fun removeSnoozeTime() = withTZ(CHICAGO) {
val (listId, list) = withVtodo(SNOOZED) val (listId, list) = withVtodo(SNOOZED)
synchronizer.sync() synchronizer.sync()
val task = caldavDao.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5") val task = caldavDao.getTaskByRemoteId(list.uuid!!, "4CBBC669-70E3-474D-A0A3-0FC42A14A5A5")
?: throw IllegalStateException("Missing task") ?: throw IllegalStateException("Missing task")
val snooze = alarmDao.getSnoozed(listOf(task.task)) assertEquals(
assertEquals(1, snooze.size) listOf(Alarm(1, task.id, DateTime(2021, 2, 10, 9, 52, 35).millis, TYPE_SNOOZE)),
alarmDao.delete(snooze.first()) alarmDao.getAlarms(1)
assertTrue(alarmDao.getSnoozed(listOf(task.task)).isEmpty()) )
alarmDao.deleteSnoozed(listOf(1))
taskDao.touch(task.task) taskDao.touch(task.task)
synchronizer.sync() synchronizer.sync()
@ -281,9 +300,9 @@ class OpenTasksPropertiesTests : OpenTasksTest() {
} }
private suspend fun insertTag(task: Task, name: String) = private suspend fun insertTag(task: Task, name: String) =
newTagData(with(NAME, name)) TagData(name = name)
.apply { tagDataDao.createNew(this) } .apply { tagDataDao.insert(this) }
.let { tagDao.insert(newTag(with(TASK, task), with(TAGDATA, it))) } .let { tagDao.insert(Tag(task = task.id, taskUid = task.uuid, tagUid = it.remoteId)) }
companion object { companion object {
private val CHICAGO = TimeZone.getTimeZone("America/Chicago") private val CHICAGO = TimeZone.getTimeZone("America/Chicago")

@ -4,11 +4,13 @@ import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
@ -38,10 +40,12 @@ class OpenTasksSynchronizerTest : OpenTasksTest() {
@Test @Test
fun deleteRemovedAccounts() = runBlocking { fun deleteRemovedAccounts() = runBlocking {
caldavDao.insert(CaldavAccount().apply { caldavDao.insert(
uuid = "bitfire.at.davdroid:test_account" CaldavAccount(
accountType = TYPE_OPENTASKS uuid = "bitfire.at.davdroid:test_account",
}) accountType = TYPE_OPENTASKS,
)
)
synchronizer.sync() synchronizer.sync()

@ -3,8 +3,8 @@ package org.tasks.opentasks
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import org.junit.Before import org.junit.Before
import org.tasks.R import org.tasks.R
import org.tasks.data.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject

@ -4,13 +4,13 @@ import android.content.ContentProviderResult
import android.content.Context import android.content.Context
import at.bitfire.ical4android.BatchOperation import at.bitfire.ical4android.BatchOperation
import at.bitfire.ical4android.Task import at.bitfire.ical4android.Task
import com.todoroo.astrid.helper.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.dmfs.tasks.contract.TaskContract import org.dmfs.tasks.contract.TaskContract
import org.dmfs.tasks.contract.TaskContract.TaskListColumns.ACCESS_LEVEL_OWNER import org.dmfs.tasks.contract.TaskContract.TaskListColumns.ACCESS_LEVEL_OWNER
import org.tasks.caldav.iCalendar import org.tasks.caldav.iCalendar
import org.tasks.data.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.MyAndroidTask import org.tasks.data.MyAndroidTask
import org.tasks.data.OpenTaskDao import org.tasks.data.OpenTaskDao
import javax.inject.Inject import javax.inject.Inject
@ -20,11 +20,11 @@ class TestOpenTaskDao @Inject constructor(
private val caldavDao: CaldavDao private val caldavDao: CaldavDao
) : OpenTaskDao(context, caldavDao) { ) : OpenTaskDao(context, caldavDao) {
suspend fun insertList( suspend fun insertList(
name: String = DEFAULT_LIST, name: String = DEFAULT_LIST,
type: String = DEFAULT_TYPE, type: String = DEFAULT_TYPE,
account: String = DEFAULT_ACCOUNT, account: String = DEFAULT_ACCOUNT,
url: String = UUIDHelper.newUUID(), url: String = UUIDHelper.newUUID(),
accessLevel: Int = ACCESS_LEVEL_OWNER, accessLevel: Int = ACCESS_LEVEL_OWNER,
): Pair<Long, CaldavCalendar> { ): Pair<Long, CaldavCalendar> {
val uri = taskLists.buildUpon() val uri = taskLists.buildUpon()
.appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true") .appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true")

@ -3,9 +3,9 @@ package org.tasks.preferences
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.todoroo.astrid.data.Task.Companion.NOTIFY_AFTER_DEADLINE import org.tasks.data.entity.Task.Companion.NOTIFY_AFTER_DEADLINE
import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_DEADLINE import org.tasks.data.entity.Task.Companion.NOTIFY_AT_DEADLINE
import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_START import org.tasks.data.entity.Task.Companion.NOTIFY_AT_START
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test

@ -11,7 +11,8 @@ import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.time.DateTime import org.tasks.time.DateTime
import java.text.ParseException import java.text.ParseException
import java.util.* import java.util.Locale
import java.util.TimeZone
import javax.inject.Inject import javax.inject.Inject
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@ -84,7 +85,7 @@ class RepeatRuleToStringTest : InjectingTestCase() {
Freeze.freezeAt(DateTime(2021, 1, 4)) { Freeze.freezeAt(DateTime(2021, 1, 4)) {
withTZ(BERLIN) { withTZ(BERLIN) {
assertEquals( assertEquals(
"Repeats daily until February 23", "Repeats daily, ends on February 23",
toString("RRULE:FREQ=DAILY;UNTIL=20210223;INTERVAL=1") toString("RRULE:FREQ=DAILY;UNTIL=20210223;INTERVAL=1")
) )
} }
@ -96,7 +97,7 @@ class RepeatRuleToStringTest : InjectingTestCase() {
Freeze.freezeAt(DateTime(2021, 1, 4)) { Freeze.freezeAt(DateTime(2021, 1, 4)) {
withTZ(LONDON) { withTZ(LONDON) {
assertEquals( assertEquals(
"Repeats daily until February 23", "Repeats daily, ends on February 23",
toString("RRULE:FREQ=DAILY;UNTIL=20210223;INTERVAL=1") toString("RRULE:FREQ=DAILY;UNTIL=20210223;INTERVAL=1")
) )
} }
@ -108,7 +109,7 @@ class RepeatRuleToStringTest : InjectingTestCase() {
Freeze.freezeAt(DateTime(2021, 1, 4)) { Freeze.freezeAt(DateTime(2021, 1, 4)) {
withTZ(NEW_YORK) { withTZ(NEW_YORK) {
assertEquals( assertEquals(
"Repeats daily until February 23", "Repeats daily, ends on February 23",
toString("RRULE:FREQ=DAILY;UNTIL=20210223;INTERVAL=1") toString("RRULE:FREQ=DAILY;UNTIL=20210223;INTERVAL=1")
) )
} }

@ -3,9 +3,9 @@ package org.tasks.ui.editviewmodel
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import com.todoroo.astrid.activity.TaskEditFragment import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.Database import org.tasks.data.db.Database
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.gcal.GCalHelper import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
@ -15,10 +15,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tasks.calendars.CalendarEventProvider import org.tasks.calendars.CalendarEventProvider
import org.tasks.data.AlarmDao import org.tasks.data.dao.AlarmDao
import org.tasks.data.LocationDao import org.tasks.data.dao.LocationDao
import org.tasks.data.TagDataDao import org.tasks.data.dao.TagDataDao
import org.tasks.data.UserActivityDao import org.tasks.data.dao.UserActivityDao
import org.tasks.data.getLocation
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.location.GeofenceApi import org.tasks.location.GeofenceApi
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
@ -64,20 +65,20 @@ open class BaseTaskEditViewModelTest : InjectingTestCase() {
calendarEventProvider, calendarEventProvider,
gCalHelper, gCalHelper,
taskMover, taskMover,
db.locationDao, db.locationDao(),
geofenceApi, geofenceApi,
db.tagDao, db.tagDao(),
db.tagDataDao, db.tagDataDao(),
preferences, preferences,
db.googleTaskDao, db.googleTaskDao(),
db.caldavDao, db.caldavDao(),
taskCompleter, taskCompleter,
alarmService, alarmService,
MutableSharedFlow(), MutableSharedFlow(),
MutableSharedFlow(), MutableSharedFlow(),
userActivityDao = userActivityDao, userActivityDao = userActivityDao,
taskAttachmentDao = db.taskAttachmentDao, taskAttachmentDao = db.taskAttachmentDao(),
alarmDao = db.alarmDao, alarmDao = db.alarmDao(),
) )
} }

@ -1,7 +1,7 @@
package org.tasks.ui.editviewmodel package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy import com.natpryce.makeiteasy.MakeItEasy
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import org.junit.Assert import org.junit.Assert

@ -1,80 +1,79 @@
package org.tasks.ui.editviewmodel package org.tasks.ui.editviewmodel
import com.todoroo.astrid.data.Task import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.service.TaskCreator.Companion.setDefaultReminders
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.Alarm import org.tasks.R
import org.tasks.data.Alarm.Companion.whenOverdue import org.tasks.data.createDueDate
import org.tasks.data.entity.Alarm
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.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.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils.currentTimeMillis import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
@UninstallModules(ProductionModule::class) @UninstallModules(ProductionModule::class)
@HiltAndroidTest @HiltAndroidTest
class ReminderTests : BaseTaskEditViewModelTest() { class ReminderTests : BaseTaskEditViewModelTest() {
@Test @Test
fun whenStartReminder() = runBlocking { fun whenStartReminder() = runBlocking {
val task = newTask() preferences.setStringSet(
task.defaultReminders(Task.NOTIFY_AT_START) R.string.p_default_reminders_key,
setup(task) hashSetOf(Task.NOTIFY_AT_START.toString())
viewModel.setStartDate(
Task.createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME,
currentTimeMillis()
)
) )
val task = newTask(with(START_DATE, DateTime()))
task.setDefaultReminders(preferences)
save() setup(task)
assertEquals( assertEquals(
listOf(Alarm(1, 0, Alarm.TYPE_REL_START).apply { id = 1 }), listOf(Alarm(type = Alarm.TYPE_REL_START)),
alarmDao.getAlarms(task.id) viewModel.selectedAlarms.value
) )
} }
@Test @Test
fun whenDueReminder() = runBlocking { fun whenDueReminder() = runBlocking {
val task = newTask() preferences.setStringSet(
task.defaultReminders(Task.NOTIFY_AT_DEADLINE) R.string.p_default_reminders_key,
setup(task) hashSetOf(Task.NOTIFY_AT_DEADLINE.toString())
viewModel.setDueDate(
Task.createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME,
currentTimeMillis()
)
) )
val task = newTask(with(DUE_TIME, DateTime()))
task.setDefaultReminders(preferences)
save() setup(task)
assertEquals( assertEquals(
listOf(Alarm(1, 0, Alarm.TYPE_REL_END).apply { id = 1 }), listOf(Alarm(type = Alarm.TYPE_REL_END)),
alarmDao.getAlarms(task.id) viewModel.selectedAlarms.value
) )
} }
@Test @Test
fun whenOverDueReminder() = runBlocking { fun whenOverDueReminder() = runBlocking {
val task = newTask() preferences.setStringSet(
task.defaultReminders(Task.NOTIFY_AFTER_DEADLINE) R.string.p_default_reminders_key,
setup(task) hashSetOf(Task.NOTIFY_AFTER_DEADLINE.toString())
viewModel.setDueDate(
Task.createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME,
currentTimeMillis()
)
) )
val task = newTask(with(DUE_TIME, DateTime()))
task.setDefaultReminders(preferences)
save() setup(task)
assertEquals( assertEquals(
listOf(whenOverdue(1).apply { id = 1 }), listOf(whenOverdue(0)),
alarmDao.getAlarms(task.id) viewModel.selectedAlarms.value
) )
} }
@ -129,4 +128,67 @@ class ReminderTests : BaseTaskEditViewModelTest() {
assertFalse(taskDao.fetch(task.id)!!.isNotifyModeFive) assertFalse(taskDao.fetch(task.id)!!.isNotifyModeFive)
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeNonstop) assertTrue(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
} }
}
@Test
fun noDefaultRemindersWithNoDates() = runBlocking {
val task = newTask()
task.setDefaultReminders(preferences)
setup(task)
save()
assertTrue(alarmDao.getAlarms(task.id).isEmpty())
}
@Test
fun addDefaultRemindersWhenAddingDueDate() = runBlocking {
preferences.setStringSet(
R.string.p_default_reminders_key,
hashSetOf(
Task.NOTIFY_AT_DEADLINE.toString(),
Task.NOTIFY_AFTER_DEADLINE.toString(),
)
)
val task = newTask()
setup(task)
viewModel.setDueDate(
createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME,
currentTimeMillis()
)
)
save()
assertEquals(
listOf(whenDue(1).copy(id = 1), whenOverdue(1).copy(id = 2)),
alarmDao.getAlarms(task.id)
)
}
@Test
fun addDefaultRemindersWhenAddingStartDate() = runBlocking {
preferences.setStringSet(
R.string.p_default_reminders_key,
hashSetOf(Task.NOTIFY_AT_START.toString())
)
val task = newTask()
setup(task)
viewModel.setStartDate(
createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME,
currentTimeMillis()
)
)
save()
assertEquals(
listOf(whenStarted(1).copy(id = 1)),
alarmDao.getAlarms(task.id)
)
}
}

@ -1,6 +1,6 @@
package org.tasks.ui.editviewmodel package org.tasks.ui.editviewmodel
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

@ -0,0 +1,161 @@
package org.tasks.ui.editviewmodel
import com.todoroo.astrid.core.BuiltInFilterExposer
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.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.data.dao.DeletionDao
import org.tasks.data.dao.TaskDao
import org.tasks.injection.InjectingTestCase
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
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var deletionDao: DeletionDao
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var inventory: Inventory
@Inject lateinit var firebase: Firebase
@Before
override fun setUp() {
super.setUp()
viewModel = TaskListViewModel(
context = context,
preferences = preferences,
taskDao = taskDao,
deletionDao = deletionDao,
taskDeleter = taskDeleter,
localBroadcastManager = localBroadcastManager,
inventory = inventory,
firebase = firebase,
)
viewModel.setFilter(BuiltInFilterExposer.getMyTasksFilter(context.resources))
}
@Test
fun clearCompletedTask() = runBlocking {
val task = taskDao.createNew(
Task(completionDate = currentTimeMillis())
)
clearCompleted()
assertTrue(taskDao.fetch(task)!!.isDeleted)
}
@Test
fun dontDeleteTaskWithRecurringParent() = runBlocking {
val parent = taskDao.createNew(
Task(
recurrence = "RRULE:FREQ=DAILY;INTERVAL=1"
)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertFalse(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun dontDeleteTaskWithRecurringGrandparent() = runBlocking {
val grandparent = taskDao.createNew(
Task(recurrence = "RRULE:FREQ=DAILY;INTERVAL=1")
)
val parent = taskDao.createNew(
Task(parent = grandparent)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertFalse(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearGrandchildWithNoRecurringAncestors() = runBlocking {
val grandparent = taskDao.createNew(Task())
val parent = taskDao.createNew(
Task(parent = grandparent)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearGrandchildWithCompletedRecurringAncestor() = runBlocking {
val grandparent = taskDao.createNew(
Task(
recurrence = "RRULE:FREQ=DAILY;INTERVAL=1",
completionDate = currentTimeMillis(),
)
)
val parent = taskDao.createNew(
Task(parent = grandparent)
)
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
@Test
fun clearHiddenSubtask() = runBlocking {
preferences.showCompleted = false
val parent = taskDao.createNew(Task())
val child = taskDao.createNew(
Task(
parent = parent,
completionDate = currentTimeMillis(),
)
)
clearCompleted()
assertTrue(taskDao.fetch(child)!!.isDeleted)
}
private suspend fun clearCompleted() = viewModel.markDeleted(viewModel.getTasksToClear())
}

@ -1,7 +1,7 @@
package org.tasks.ui.editviewmodel package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task.Priority.Companion.HIGH import org.tasks.data.entity.Task.Priority.Companion.HIGH
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking

@ -7,7 +7,7 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.injection.InjectingTestCase import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences

@ -1,7 +1,7 @@
package org.tasks.caldav package org.tasks.caldav
import androidx.test.annotation.UiThreadTest import androidx.test.annotation.UiThreadTest
import com.todoroo.astrid.helper.UUIDHelper import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -9,7 +9,7 @@ import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.tasks.R import org.tasks.R
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.data.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import javax.inject.Inject import javax.inject.Inject
@ -25,10 +25,8 @@ class CaldavSubscriptionTest : CaldavTest() {
inventory.clear() inventory.clear()
inventory.add(emptyList()) inventory.add(emptyList())
account = CaldavAccount().apply { account = CaldavAccount(uuid = UUIDHelper.newUUID())
uuid = UUIDHelper.newUUID() .let { it.copy(id = caldavDao.insert(it)) }
id = caldavDao.insert(this)
}
synchronizer.sync(account) synchronizer.sync(account)

@ -5,8 +5,8 @@ import com.facebook.flipper.plugins.network.NetworkReporter
import com.facebook.flipper.plugins.network.NetworkReporter.ResponseInfo import com.facebook.flipper.plugins.network.NetworkReporter.ResponseInfo
import com.google.api.client.http.* import com.google.api.client.http.*
import com.google.api.client.json.GenericJson import com.google.api.client.json.GenericJson
import com.todoroo.andlib.utility.DateUtilities import org.tasks.data.UUIDHelper
import com.todoroo.astrid.helper.UUIDHelper import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
@ -18,12 +18,12 @@ internal class FlipperHttpInterceptor<T>(private val plugin: NetworkFlipperPlugi
private set private set
override fun intercept(request: HttpRequest) { override fun intercept(request: HttpRequest) {
plugin.reportRequest(toRequestInfo(request, DateUtilities.now())) plugin.reportRequest(toRequestInfo(request, currentTimeMillis()))
} }
@Throws(IOException::class) @Throws(IOException::class)
override fun interceptResponse(response: HttpResponse) { override fun interceptResponse(response: HttpResponse) {
plugin.reportResponse(toResponseInfo(response, DateUtilities.now())) plugin.reportResponse(toResponseInfo(response, currentTimeMillis()))
} }
@Throws(IOException::class) @Throws(IOException::class)

@ -5,7 +5,6 @@ import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import at.bitfire.cert4android.CustomCertManager.Companion.resetCertificates import at.bitfire.cert4android.CustomCertManager.Companion.resetCertificates
import com.todoroo.andlib.utility.DateUtilities.now
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
@ -14,6 +13,7 @@ import org.tasks.billing.Inventory
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.toast
import org.tasks.injection.InjectingPreferenceFragment import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.min import kotlin.math.min
@ -62,7 +62,7 @@ class Debug : InjectingPreferenceFragment() {
findPreference(R.string.debug_clear_hints).setOnPreferenceClickListener { findPreference(R.string.debug_clear_hints).setOnPreferenceClickListener {
preferences.installDate = preferences.installDate =
min(preferences.installDate, now() - TimeUnit.DAYS.toMillis(14)) min(preferences.installDate, currentTimeMillis() - TimeUnit.DAYS.toMillis(14))
preferences.lastSubscribeRequest = 0L preferences.lastSubscribeRequest = 0L
preferences.lastReviewRequest = 0L preferences.lastReviewRequest = 0L
preferences.shownBeastModeHint = false preferences.shownBeastModeHint = false

@ -7,11 +7,11 @@ import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.todoroo.andlib.utility.DateUtilities.now
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R import org.tasks.R
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -64,14 +64,14 @@ class Firebase @Inject constructor(
} }
private val installCooldown: Boolean private val installCooldown: Boolean
get() = preferences.installDate + days("install_cooldown", 14L) > now() get() = preferences.installDate + days("install_cooldown", 14L) > currentTimeMillis()
val reviewCooldown: Boolean val reviewCooldown: Boolean
get() = installCooldown || preferences.lastReviewRequest + days("review_cooldown", 30L) > now() get() = installCooldown || preferences.lastReviewRequest + days("review_cooldown", 30L) > currentTimeMillis()
val subscribeCooldown: Boolean val subscribeCooldown: Boolean
get() = installCooldown get() = installCooldown
|| preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > now() || preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > currentTimeMillis()
val moreOptionsBadge: Boolean val moreOptionsBadge: Boolean
get() = remoteConfig?.getBoolean("more_options_badge") ?: false get() = remoteConfig?.getBoolean("more_options_badge") ?: false

@ -2,12 +2,23 @@ package org.tasks.billing
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import com.android.billingclient.api.* import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient.* import com.android.billingclient.api.BillingClient.BillingResponseCode
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.ProrationMode import com.android.billingclient.api.BillingFlowParams.ProrationMode
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ConsumeParams
import com.android.billingclient.api.Purchase.PurchaseState import com.android.billingclient.api.Purchase.PurchaseState
import com.android.billingclient.api.Purchase.PurchasesResult import com.android.billingclient.api.PurchasesResult
import com.android.billingclient.api.PurchasesUpdatedListener
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 dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
@ -38,8 +49,8 @@ class BillingClientImpl(
override suspend fun queryPurchases(throwError: Boolean) = try { override suspend fun queryPurchases(throwError: Boolean) = try {
executeServiceRequest { executeServiceRequest {
withContext(Dispatchers.IO + NonCancellable) { withContext(Dispatchers.IO + NonCancellable) {
val subs = billingClient.queryPurchases(SkuType.SUBS) val subs = billingClient.queryPurchasesAsync(SkuType.SUBS)
val iaps = billingClient.queryPurchases(SkuType.INAPP) val iaps = billingClient.queryPurchasesAsync(SkuType.INAPP)
if (subs.success || iaps.success) { if (subs.success || iaps.success) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
inventory.clear() inventory.clear()
@ -125,7 +136,7 @@ class BillingClientImpl(
.setPurchaseToken(purchase.purchaseToken) .setPurchaseToken(purchase.purchaseToken)
.build() .build()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
suspendCoroutine<BillingResult> { cont -> suspendCoroutine { cont ->
billingClient.acknowledgePurchase(params) { billingClient.acknowledgePurchase(params) {
Timber.d("acknowledge: ${it.responseCodeString} $purchase") Timber.d("acknowledge: ${it.responseCodeString} $purchase")
cont.resume(it) cont.resume(it)
@ -188,7 +199,7 @@ class BillingClientImpl(
const val STATE_PURCHASED = PurchaseState.PURCHASED const val STATE_PURCHASED = PurchaseState.PURCHASED
private val PurchasesResult.success: Boolean private val PurchasesResult.success: Boolean
get() = responseCode == BillingResponseCode.OK get() = billingResult.responseCode == BillingResponseCode.OK
private val BillingResult.success: Boolean private val BillingResult.success: Boolean
get() = responseCode == BillingResponseCode.OK get() = responseCode == BillingResponseCode.OK
@ -221,6 +232,6 @@ class BillingClientImpl(
get() = billingResult.responseCodeString get() = billingResult.responseCodeString
private val PurchasesResult.purchases: List<com.android.billingclient.api.Purchase> private val PurchasesResult.purchases: List<com.android.billingclient.api.Purchase>
get() = purchasesList ?: emptyList() get() = purchasesList
} }
} }

@ -1,16 +1,23 @@
package org.tasks.billing package org.tasks.billing
import com.android.billingclient.api.Purchase import com.android.billingclient.api.Purchase
import com.google.gson.GsonBuilder import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.tasks.billing.BillingClientImpl.Companion.STATE_PURCHASED import org.tasks.billing.BillingClientImpl.Companion.STATE_PURCHASED
import java.util.regex.Pattern import java.util.regex.Pattern
class Purchase(private val purchase: Purchase) { class Purchase(private val purchase: Purchase) {
constructor(json: String?) : this(GsonBuilder().create().fromJson<Purchase>(json, Purchase::class.java)) constructor(json: String) : this(
Json.parseToJsonElement(json).jsonObject.let {
Purchase(it["zza"]!!.jsonPrimitive.content, it["zzb"]!!.jsonPrimitive.content)
}
)
fun toJson(): String { fun toJson(): String {
return GsonBuilder().create().toJson(purchase) return Json.encodeToString(mapOf("zza" to purchase.originalJson, "zzb" to purchase.signature))
} }
override fun toString(): String { override fun toString(): String {

@ -28,7 +28,7 @@ package org.tasks.billing
* by implementing security measure X is greater than the money you would lose if you don't * by implementing security measure X is greater than the money you would lose if you don't
* implement X. Talk to a UX designer if you find yourself obsessing over security. * implement X. Talk to a UX designer if you find yourself obsessing over security.
* *
* The good news is, in implementing [BillingRepository], a number of measures is taken to help * The good news is, in implementing BillingRepository, a number of measures is taken to help
* prevent fraudulent activities in your app. We don't just focus on tech savvy hackers, but also * prevent fraudulent activities in your app. We don't just focus on tech savvy hackers, but also
* on fraudulent users who may want to exploit loopholes. Just to name an obvious case: * on fraudulent users who may want to exploit loopholes. Just to name an obvious case:
* triangulation using Google Play, your secure server, and a local cache helps against non-techie * triangulation using Google Play, your secure server, and a local cache helps against non-techie

@ -5,11 +5,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import com.google.android.gms.location.Geofence import com.google.android.gms.location.Geofence
import com.google.android.gms.location.GeofencingEvent import com.google.android.gms.location.GeofencingEvent
import com.todoroo.andlib.utility.DateUtilities
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.Notifier import org.tasks.Notifier
import org.tasks.data.LocationDao import org.tasks.data.dao.LocationDao
import org.tasks.injection.InjectingJobIntentService import org.tasks.injection.InjectingJobIntentService
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -45,9 +45,9 @@ class GoogleGeofenceTransitionIntentService : InjectingJobIntentService() {
return return
} }
val geofences = if (arrival) { val geofences = if (arrival) {
locationDao.getArrivalGeofences(place.uid!!, DateUtilities.now()) locationDao.getArrivalGeofences(place.uid!!, currentTimeMillis())
} else { } else {
locationDao.getDepartureGeofences(place.uid!!, DateUtilities.now()) locationDao.getDepartureGeofences(place.uid!!, currentTimeMillis())
} }
notifier.triggerNotifications(place.id, geofences, arrival) notifier.triggerNotifications(place.id, geofences, arrival)
} catch (e: Exception) { } catch (e: Exception) {

@ -10,7 +10,7 @@ import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.* import com.google.android.gms.maps.model.*
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R import org.tasks.R
import org.tasks.data.Place import org.tasks.data.entity.Place
import org.tasks.location.MapFragment.MapFragmentCallback import org.tasks.location.MapFragment.MapFragmentCallback
import javax.inject.Inject import javax.inject.Inject
@ -36,7 +36,7 @@ class GoogleMapFragment @Inject constructor(
} }
override val mapPosition: MapPosition? override val mapPosition: MapPosition?
get() = map?.cameraPosition?.let { it -> get() = map?.cameraPosition?.let {
val target = it.target val target = it.target
return MapPosition(target.latitude, target.longitude, it.zoom) return MapPosition(target.latitude, target.longitude, it.zoom)
} }

@ -12,7 +12,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.tasks.data.MergedGeofence import org.tasks.data.MergedGeofence
import org.tasks.data.Place import org.tasks.data.entity.Place
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine

@ -1,17 +1,19 @@
package org.tasks.play package org.tasks.play
import android.app.Activity
import android.content.Context import android.content.Context
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability.getInstance import com.google.android.gms.common.GoogleApiAvailability.getInstance
import com.google.android.play.core.ktx.launchReview import com.google.android.play.core.ktx.launchReview
import com.google.android.play.core.ktx.requestReview import com.google.android.play.core.ktx.requestReview
import com.google.android.play.core.review.ReviewManagerFactory import com.google.android.play.core.review.ReviewManagerFactory
import com.todoroo.andlib.utility.DateUtilities.now
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
class PlayServices @Inject constructor( class PlayServices @Inject constructor(
@ -22,17 +24,17 @@ class PlayServices @Inject constructor(
fun isAvailable() = fun isAvailable() =
getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
suspend fun requestReview(activity: Activity) { fun requestReview(activity: ComponentActivity) = activity.lifecycleScope.launch {
if (firebase.reviewCooldown) { if (firebase.reviewCooldown) {
return return@launch
} }
try { try {
with(ReviewManagerFactory.create(context)) { with(ReviewManagerFactory.create(context)) {
val request = requestReview() val request = requestReview()
launchReview(activity, request) launchReview(activity, request)
preferences.lastReviewRequest = now()
firebase.logEvent(R.string.event_request_review)
} }
preferences.lastReviewRequest = currentTimeMillis()
firebase.logEvent(R.string.event_request_review)
} catch (e: Exception) { } catch (e: Exception) {
firebase.reportException(e) firebase.reportException(e)
} }

@ -310,12 +310,15 @@
<meta-data <meta-data
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/scrollable_widget_provider_info"/> android:resource="@xml/scrollable_widget_provider_info"/>
<meta-data
android:name="com.samsung.android.appwidget.provider"
android:resource="@xml/samsung_scrollable_flex_window_widget_meta_info"/>
</receiver> </receiver>
<!-- ======================================================== Services = --> <!-- ======================================================== Services = -->
<service <service
android:name=".widget.ScrollableWidgetUpdateService" android:name=".widget.TasksWidgetAdapter"
android:permission="android.permission.BIND_REMOTEVIEWS"/> android:permission="android.permission.BIND_REMOTEVIEWS"/>
<service <service
@ -436,12 +439,6 @@
android:name=".activities.PlaceSettingsActivity" android:name=".activities.PlaceSettingsActivity"
android:theme="@style/Tasks" /> android:theme="@style/Tasks" />
<activity
android:name="com.todoroo.astrid.gcal.CalendarReminderActivity"
android:theme="@style/TasksDialog"/>
<receiver android:name="com.todoroo.astrid.gcal.CalendarAlarmReceiver"/>
<activity android:name=".activities.GoogleTaskListSettingsActivity"/> <activity android:name=".activities.GoogleTaskListSettingsActivity"/>
<activity <activity
@ -496,12 +493,6 @@
android:name=".scheduling.NotificationSchedulerIntentService" android:name=".scheduling.NotificationSchedulerIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"/> android:permission="android.permission.BIND_JOB_SERVICE"/>
<receiver android:name=".scheduling.CalendarNotificationIntentService$Broadcast"/>
<service
android:exported="false"
android:name=".scheduling.CalendarNotificationIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<receiver android:name=".notifications.NotificationClearedReceiver"/> <receiver android:name=".notifications.NotificationClearedReceiver"/>
<service <service
@ -541,10 +532,6 @@
android:value="org.tasks.dashclock.DashClockSettings"/> android:value="org.tasks.dashclock.DashClockSettings"/>
</service> </service>
<service
android:exported="false"
android:name=".jobs.NotificationService"/>
<activity <activity
android:exported="true" android:exported="true"
android:name=".dashclock.DashClockSettings"/> android:name=".dashclock.DashClockSettings"/>
@ -610,6 +597,8 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="org.tasks.jobs.NotificationReceiver" />
<activity <activity
android:name=".auth.MicrosoftAuthenticationActivity" android:name=".auth.MicrosoftAuthenticationActivity"
android:theme="@style/TranslucentDialog"/> android:theme="@style/TranslucentDialog"/>

@ -1,8 +0,0 @@
package com.todoroo.andlib.data
import com.todoroo.andlib.sql.Field
class Property internal constructor(val name: String?, expression: String) : Field(expression) {
constructor(table: Table, columnName: String) : this(columnName, "${table.name()}.$columnName")
}

@ -6,20 +6,17 @@
package com.todoroo.andlib.utility; package com.todoroo.andlib.utility;
import android.app.Activity;
import android.content.Context;
import android.os.Build; import android.os.Build;
import android.os.Build.VERSION; import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES; import android.os.Build.VERSION_CODES;
import android.os.Looper; import android.os.Looper;
import android.text.InputType; import android.text.InputType;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView; import android.widget.TextView;
import org.tasks.BuildConfig; import org.tasks.BuildConfig;
import java.io.Serializable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@ -84,12 +81,12 @@ public class AndroidUtilities {
result.append(SERIALIZATION_SEPARATOR); result.append(SERIALIZATION_SEPARATOR);
} }
public static Map<String, Object> mapFromSerializedString(String string) { public static Map<String, Serializable> mapFromSerializedString(String string) {
if (string == null) { if (string == null) {
return new HashMap<>(); return new HashMap<>();
} }
Map<String, Object> result = new HashMap<>(); Map<String, Serializable> result = new HashMap<>();
fromSerialized( fromSerialized(
string, string,
result, result,
@ -143,8 +140,12 @@ public class AndroidUtilities {
return !atLeastOreo(); return !atLeastOreo();
} }
public static boolean preS() {
return !atLeastS();
}
public static boolean preTiramisu() { public static boolean preTiramisu() {
return VERSION.SDK_INT < VERSION_CODES.TIRAMISU; return !atLeastTiramisu();
} }
public static boolean preUpsideDownCake() { public static boolean preUpsideDownCake() {
@ -159,6 +160,10 @@ public class AndroidUtilities {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
} }
public static boolean atLeastOreoMR1() {
return Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1;
}
public static boolean atLeastP() { public static boolean atLeastP() {
return VERSION.SDK_INT >= Build.VERSION_CODES.P; return VERSION.SDK_INT >= Build.VERSION_CODES.P;
} }
@ -195,38 +200,6 @@ public class AndroidUtilities {
return Thread.currentThread() == Looper.getMainLooper().getThread(); return Thread.currentThread() == Looper.getMainLooper().getThread();
} }
/** Capitalize the first character */
public static String capitalize(String string) {
return string.substring(0, 1).toUpperCase() + string.substring(1);
}
public static void hideKeyboard(Activity activity) {
try {
View currentFocus = activity.getCurrentFocus();
if (currentFocus != null) {
InputMethodManager inputMethodManager =
(InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0);
currentFocus.clearFocus();
}
} catch (Exception e) {
Timber.e(e);
}
}
/**
* Dismiss the keyboard if it is displayed by any of the listed views
*
* @param views - a list of views that might potentially be displaying the keyboard
*/
public static void hideSoftInputForViews(Context context, View... views) {
InputMethodManager imm =
(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
for (View v : views) {
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
}
interface SerializedPut<T> { interface SerializedPut<T> {
void put(T object, String key, char type, String value) throws NumberFormatException; void put(T object, String key, char type, String value) throws NumberFormatException;

@ -7,14 +7,13 @@
package com.todoroo.andlib.utility; package com.todoroo.andlib.utility;
import static org.tasks.date.DateTimeUtils.newDateTime; import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import android.content.Context; import android.content.Context;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.todoroo.astrid.data.Task; import org.tasks.data.entity.Task;
import org.tasks.BuildConfig; import org.tasks.BuildConfig;
import org.tasks.R; import org.tasks.R;
@ -38,11 +37,6 @@ public class DateUtilities {
static Boolean is24HourOverride = null; static Boolean is24HourOverride = null;
/** Returns unixtime for current time */
public static long now() {
return currentTimeMillis();
}
/* ====================================================================== /* ======================================================================
* =========================================================== formatters * =========================================================== formatters
* ====================================================================== */ * ====================================================================== */

@ -5,70 +5,74 @@
*/ */
package com.todoroo.astrid.activity package com.todoroo.astrid.activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.mandatorySystemGestures
import androidx.core.content.IntentCompat.getParcelableExtra
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment import com.todoroo.astrid.activity.TaskEditFragment.Companion.newTaskEditFragment
import com.todoroo.astrid.activity.TaskListFragment.TaskListFragmentCallbackHandler import com.todoroo.astrid.adapter.SubheaderClickHandler
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.timers.TimerControlSet.TimerControlSetCallback
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.data.AlarmDao import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.data.LocationDao import org.tasks.compose.drawer.TasksMenu
import org.tasks.data.Place import org.tasks.data.dao.AlarmDao
import org.tasks.data.TagDataDao import org.tasks.data.dao.LocationDao
import org.tasks.data.entity.Place
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Task
import org.tasks.data.getLocation
import org.tasks.databinding.TaskListActivityBinding import org.tasks.databinding.TaskListActivityBinding
import org.tasks.dialogs.SortSettingsActivity import org.tasks.dialogs.NewFilterDialog
import org.tasks.dialogs.WhatsNewDialog import org.tasks.dialogs.WhatsNewDialog
import org.tasks.extensions.Context.nightMode
import org.tasks.extensions.hideKeyboard
import org.tasks.filters.FilterProvider
import org.tasks.filters.PlaceFilter import org.tasks.filters.PlaceFilter
import org.tasks.injection.InjectingAppCompatActivity import org.tasks.location.LocationPickerActivity.Companion.EXTRA_PLACE
import org.tasks.intents.TaskIntents.getTaskListIntent
import org.tasks.location.LocationPickerActivity
import org.tasks.play.PlayServices
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.themes.ThemeColor
import org.tasks.ui.EmptyTaskEditFragment.Companion.newEmptyTaskEditFragment import org.tasks.ui.EmptyTaskEditFragment.Companion.newEmptyTaskEditFragment
import org.tasks.ui.MainActivityEvent import org.tasks.ui.MainActivityEvent
import org.tasks.ui.MainActivityEventBus import org.tasks.ui.MainActivityEventBus
import org.tasks.ui.NavigationDrawerFragment
import org.tasks.ui.NavigationDrawerFragment.Companion.newNavigationDrawer
import org.tasks.ui.TaskListEvent
import org.tasks.ui.TaskListEventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandler, TimerControlSetCallback { class MainActivity : AppCompatActivity() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider @Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var theme: Theme @Inject lateinit var theme: Theme
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var taskCreator: TaskCreator @Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var inventory: Inventory @Inject lateinit var inventory: Inventory
@Inject lateinit var colorProvider: ColorProvider @Inject lateinit var colorProvider: ColorProvider
@ -76,16 +80,19 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
@Inject lateinit var tagDataDao: TagDataDao @Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao @Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var eventBus: MainActivityEventBus @Inject lateinit var eventBus: MainActivityEventBus
@Inject lateinit var taskListEventBus: TaskListEventBus
@Inject lateinit var playServices: PlayServices
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
private val viewModel: MainActivityViewModel by viewModels()
private var currentNightMode = 0 private var currentNightMode = 0
private var currentPro = false private var currentPro = false
private var filter: Filter? = null
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private lateinit var binding: TaskListActivityBinding private lateinit var binding: TaskListActivityBinding
private val settingsRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
recreate()
}
/** @see android.app.Activity.onCreate /** @see android.app.Activity.onCreate
*/ */
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -95,48 +102,135 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
currentPro = inventory.hasPro currentPro = inventory.hasPro
binding = TaskListActivityBinding.inflate(layoutInflater) binding = TaskListActivityBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
if (savedInstanceState != null) { logIntent("onCreate")
filter = savedInstanceState.getParcelable(EXTRA_FILTER)
applyTheme()
}
handleIntent() handleIntent()
binding.composeView.setContent {
val state = viewModel.state.collectAsStateLifecycleAware().value
if (state.drawerOpen) {
MdcTheme {
TasksMenu(
bottomPadding = WindowInsets.mandatorySystemGestures
.asPaddingValues()
.calculateBottomPadding(),
items = state.drawerItems,
begForMoney = state.begForMoney,
isTopAppBar = preferences.isTopAppBar,
setFilter = { viewModel.setFilter(it) },
toggleCollapsed = { viewModel.toggleCollapsed(it) },
addFilter = {
val rc = it.addIntentRc
if (rc == FilterProvider.REQUEST_NEW_FILTER) {
NewFilterDialog.newFilterDialog().show(
supportFragmentManager,
SubheaderClickHandler.FRAG_TAG_NEW_FILTER
)
} else {
val intent = it.addIntent ?: return@TasksMenu
startActivityForResult(intent, rc)
}
},
dismiss = { viewModel.setDrawerOpen(false) },
)
}
}
}
eventBus eventBus
.onEach(this::process) .onEach(this::process)
.launchIn(lifecycleScope) .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
}
}
}
} 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
}
}
}
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 suspend fun process(event: MainActivityEvent) = when (event) { private fun process(event: MainActivityEvent) = when (event) {
is MainActivityEvent.OpenTask ->
onTaskListItemClicked(event.task)
is MainActivityEvent.RequestRating ->
playServices.requestReview(this)
is MainActivityEvent.ClearTaskEditFragment -> is MainActivityEvent.ClearTaskEditFragment ->
removeTaskEditFragment() viewModel.setTask(null)
} }
@Deprecated("Deprecated in Java")
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
NavigationDrawerFragment.REQUEST_SETTINGS -> recreate() REQUEST_NEW_LIST ->
NavigationDrawerFragment.REQUEST_NEW_LIST -> if (resultCode == RESULT_OK && data != null) {
if (resultCode == RESULT_OK) { getParcelableExtra(data, OPEN_FILTER, Filter::class.java)?.let {
data viewModel.setFilter(it)
?.getParcelableExtra<Filter>(OPEN_FILTER) }
?.let { startActivity(getTaskListIntent(this, it)) }
}
NavigationDrawerFragment.REQUEST_NEW_PLACE ->
if (resultCode == RESULT_OK) {
data
?.getParcelableExtra<Place>(LocationPickerActivity.EXTRA_PLACE)
?.let { startActivity(getTaskListIntent(this, PlaceFilter(it))) }
} }
TaskListFragment.REQUEST_SORT -> REQUEST_NEW_PLACE ->
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK && data != null) {
sortChanged( getParcelableExtra(data, EXTRA_PLACE, Place::class.java)?.let {
reload = data?.getBooleanExtra(SortSettingsActivity.EXTRA_FORCE_RELOAD, false) ?: false, viewModel.setFilter(PlaceFilter(it))
groupChange = data?.getBooleanExtra(SortSettingsActivity.EXTRA_CHANGED_GROUP, false) ?: false, }
)
} }
else -> else ->
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
} }
@ -145,197 +239,74 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
logIntent("onNewIntent")
handleIntent() handleIntent()
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(EXTRA_FILTER, filter)
}
private fun clearUi() { private fun clearUi() {
finishActionMode() actionMode?.finish()
navigationDrawer?.dismiss() actionMode = null
viewModel.setDrawerOpen(false)
} }
private suspend fun getTaskToLoad(filter: Filter?): Task? { private suspend fun getTaskToLoad(filter: Filter?): Task? = when {
val intent = intent intent.isFromHistory -> null
if (intent.isFromHistory) { intent.hasExtra(CREATE_TASK) -> {
return null
}
if (intent.hasExtra(CREATE_TASK)) {
val source = intent.getStringExtra(CREATE_SOURCE) val source = intent.getStringExtra(CREATE_SOURCE)
firebase.addTask(source ?: "unknown") firebase.addTask(source ?: "unknown")
intent.removeExtra(CREATE_TASK) intent.removeExtra(CREATE_TASK)
intent.removeExtra(CREATE_SOURCE) intent.removeExtra(CREATE_SOURCE)
return taskCreator.createWithValues(filter, "") taskCreator.createWithValues(filter, "")
} }
if (intent.hasExtra(OPEN_TASK)) {
val task: Task? = intent.getParcelableExtra(OPEN_TASK) intent.hasExtra(OPEN_TASK) -> {
val task = getParcelableExtra(intent, OPEN_TASK, Task::class.java)
intent.removeExtra(OPEN_TASK) intent.removeExtra(OPEN_TASK)
return task task
} }
return null
else -> null
} }
private fun openTask(filter: Filter?) = lifecycleScope.launch { private fun logIntent(caller: String) {
val task = getTaskToLoad(filter) if (BuildConfig.DEBUG) {
when { Timber.d("""
task != null -> onTaskListItemClicked(task) $caller
taskEditFragment == null -> hideDetailFragment() **********
else -> showDetailFragment() 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()
)
} }
} }
private fun handleIntent() { private fun handleIntent() {
val intent = intent lifecycleScope.launch {
val openFilter = intent.getFilter val filter = intent.getFilter
val loadFilter = intent.getFilterString ?: intent.getFilterString?.let { defaultFilterProvider.getFilterFromPreference(it) }
val openTask = !intent.isFromHistory ?: viewModel.state.value.filter
&& (intent.hasExtra(OPEN_TASK) || intent.hasExtra(CREATE_TASK)) val task = getTaskToLoad(filter)
val tef = taskEditFragment viewModel.setFilter(filter = filter, task = task)
Timber.d("""
**********
broughtToFront: ${intent.broughtToFront}
isFromHistory: ${intent.isFromHistory}
flags: ${intent.flagsToString}
OPEN_FILTER: ${openFilter?.let { "${it.listingTitle}: $it" }}
LOAD_FILTER: $loadFilter
OPEN_TASK: ${intent.getParcelableExtra<Task>(OPEN_TASK)}
CREATE_TASK: ${intent.hasExtra(CREATE_TASK)}
taskListFragment: ${taskListFragment?.getFilter()?.let { "${it.listingTitle}: $it" }}
taskEditFragment: ${taskEditFragment?.editViewModel?.task}
**********""")
if (!openTask && (openFilter != null || !loadFilter.isNullOrBlank())) {
tef?.let {
lifecycleScope.launch {
it.save()
}
}
}
if (!loadFilter.isNullOrBlank() || openFilter == null && filter == null) {
lifecycleScope.launch {
val filter = if (loadFilter.isNullOrBlank()) {
defaultFilterProvider.getStartupFilter()
} else {
defaultFilterProvider.getFilterFromPreference(loadFilter)
}
clearUi()
if (isSinglePaneLayout) {
if (openTask) {
setFilter(filter)
openTask(filter)
} else {
openTaskListFragment(filter, true)
}
} else {
openTaskListFragment(filter, true)
openTask(filter)
}
}
} else if (openFilter != null) {
clearUi()
if (isSinglePaneLayout) {
if (openTask) {
setFilter(openFilter)
openTask(openFilter)
} else {
openTaskListFragment(openFilter, true)
}
} else {
openTaskListFragment(openFilter, true)
openTask(openFilter)
}
} else {
val existing = taskListFragment
val target = if (existing == null || existing.getFilter() !== filter) {
TaskListFragment.newTaskListFragment(applicationContext, filter)
} else {
existing
}
if (isSinglePaneLayout) {
if (openTask || tef != null) {
openTask(filter)
} else {
openTaskListFragment(filter, false)
}
} else {
openTaskListFragment(target, false)
openTask(filter)
}
}
if (intent.hasExtra(TOKEN_CREATE_NEW_LIST_NAME)) {
val listName = intent.getStringExtra(TOKEN_CREATE_NEW_LIST_NAME)
intent.removeExtra(TOKEN_CREATE_NEW_LIST_NAME)
val activityIntent = Intent(this@MainActivity, TagSettingsActivity::class.java)
activityIntent.putExtra(TagSettingsActivity.TOKEN_AUTOPOPULATE_NAME, listName)
startActivityForResult(activityIntent, NavigationDrawerFragment.REQUEST_NEW_LIST)
} }
} }
private fun showDetailFragment() { private fun updateSystemBars(filter: Filter) {
if (isSinglePaneLayout) { with (getFilterColor(filter)) {
binding.detail.visibility = View.VISIBLE applyToNavigationBar(this@MainActivity)
binding.master.visibility = View.GONE applyTaskDescription(this@MainActivity, filter.title ?: getString(R.string.app_name))
} }
} }
private fun hideDetailFragment() { private fun getFilterColor(filter: Filter) =
supportFragmentManager if (filter.tint != 0)
.beginTransaction() colorProvider.getThemeColor(filter.tint, true)
.replace(R.id.detail, newEmptyTaskEditFragment()) else
.runOnCommit { theme.themeColor
if (isSinglePaneLayout) {
binding.master.visibility = View.VISIBLE
binding.detail.visibility = View.GONE
}
}
.commit()
}
private fun setFilter(newFilter: Filter?) {
filter = newFilter
applyTheme()
}
private fun openTaskListFragment(filter: Filter?, force: Boolean = false) {
openTaskListFragment(TaskListFragment.newTaskListFragment(applicationContext, filter), force)
}
private fun openTaskListFragment(taskListFragment: TaskListFragment, force: Boolean) {
AndroidUtilities.assertMainThread()
if (supportFragmentManager.isDestroyed) {
return
}
val newFilter = taskListFragment.getFilter()
if (filter != null
&& !force
&& filter!!.areItemsTheSame(newFilter)
&& filter!!.areContentsTheSame(newFilter)) {
return
}
filter = newFilter
defaultFilterProvider.lastViewedFilter = newFilter
applyTheme()
supportFragmentManager
.beginTransaction()
.replace(R.id.master, taskListFragment, FRAG_TAG_TASK_LIST)
.commitNowAllowingStateLoss()
}
private fun applyTheme() {
val filterColor = filterColor
filterColor.applyToNavigationBar(this)
filterColor.applyTaskDescription(this, filter?.listingTitle ?: getString(R.string.app_name))
theme.withThemeColor(filterColor).applyToContext(this)
}
private val filterColor: ThemeColor
get() = if (filter != null && filter!!.tint != 0) colorProvider.getThemeColor(filter!!.tint, true) else theme.themeColor
private val navigationDrawer: NavigationDrawerFragment?
get() = supportFragmentManager.findFragmentByTag(FRAG_TAG_NAV_DRAWER) as? NavigationDrawerFragment
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -354,137 +325,37 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
} }
} }
private val nightMode: Int private suspend fun newTaskEditFragment(task: Task): TaskEditFragment {
get() = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
override suspend fun onTaskListItemClicked(task: Task?) {
AndroidUtilities.assertMainThread() AndroidUtilities.assertMainThread()
if (task == null) {
return
}
taskEditFragment?.save(remove = false)
clearUi() clearUi()
coroutineScope { return coroutineScope {
val freshTask = async { if (task.isNew) task else taskDao.fetch(task.id) ?: task } withContext(Dispatchers.Default) {
val list = async { defaultFilterProvider.getList(task) } val freshTask = async { if (task.isNew) task else taskDao.fetch(task.id) ?: task }
val location = async { locationDao.getLocation(task, preferences) } val list = async { defaultFilterProvider.getList(task) }
val tags = async { tagDataDao.getTags(task) } val location = async { locationDao.getLocation(task, preferences) }
val alarms = async { alarmDao.getAlarms(task) } val tags = async { tagDataDao.getTags(task) }
val fragment = withContext(Dispatchers.Default) { val alarms = async { alarmDao.getAlarms(task) }
newTaskEditFragment( newTaskEditFragment(
freshTask.await(), freshTask.await(),
list.await(), list.await(),
location.await(), location.await(),
tags.await(), tags.await(),
alarms.await(), alarms.await(),
) )
} }
supportFragmentManager.beginTransaction()
.replace(R.id.detail, fragment, TaskEditFragment.TAG_TASKEDIT_FRAGMENT)
.runOnCommit { showDetailFragment() }
.commitNowAllowingStateLoss()
}
}
override fun onNavigationIconClicked() {
hideKeyboard()
newNavigationDrawer(filter).show(supportFragmentManager, FRAG_TAG_NAV_DRAWER)
}
override fun onBackPressed() {
taskEditFragment?.let {
if (preferences.backButtonSavesTask()) {
lifecycleScope.launch {
it.save()
}
} else {
it.discardButtonClick()
}
return@onBackPressed
}
if (taskListFragment?.collapseSearchView() == true) {
return
} }
finish()
}
private val taskListFragment: TaskListFragment?
get() = supportFragmentManager.findFragmentByTag(FRAG_TAG_TASK_LIST) as TaskListFragment?
private val taskEditFragment: TaskEditFragment?
get() = supportFragmentManager.findFragmentByTag(TaskEditFragment.TAG_TASKEDIT_FRAGMENT) as TaskEditFragment?
override suspend fun stopTimer(): Task {
return taskEditFragment!!.stopTimer()
}
override suspend fun startTimer(): Task {
return taskEditFragment!!.startTimer()
} }
private val isSinglePaneLayout: Boolean private val isSinglePaneLayout: Boolean
get() = !resources.getBoolean(R.bool.two_pane_layout) get() = !resources.getBoolean(R.bool.two_pane_layout)
private fun removeTaskEditFragment() {
val removeTask = intent.removeTask
val finishAffinity = intent.finishAffinity
if (finishAffinity || taskListFragment == null) {
finishAffinity()
} else {
if (removeTask && intent.broughtToFront) {
moveTaskToBack(true)
}
hideKeyboard()
hideDetailFragment()
taskListFragment?.let {
setFilter(it.getFilter())
it.loadTaskListContent()
}
}
}
private fun hideKeyboard() {
val view = currentFocus
if (view != null) {
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
}
}
private fun sortChanged(reload: Boolean, groupChange: Boolean) {
if (groupChange) {
taskListFragment?.clearCollapsed()
}
localBroadcastManager.broadcastRefresh()
if (reload) {
openTaskListFragment(filter, true)
}
}
override fun onSupportActionModeStarted(mode: ActionMode) { override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode) super.onSupportActionModeStarted(mode)
actionMode = mode actionMode = mode
} }
private fun finishActionMode() {
actionMode?.finish()
actionMode = null
}
override fun onStart() {
super.onStart()
lifecycleScope.launch {
if (!inventory.hasPro && !firebase.subscribeCooldown) {
taskListEventBus.tryEmit(TaskListEvent.BegForSubscription)
}
}
}
companion object { companion object {
/** For indicating the new list screen should be launched at fragment setup time */ /** For indicating the new list screen should be launched at fragment setup time */
const val TOKEN_CREATE_NEW_LIST_NAME = "newListName" // $NON-NLS-1$
const val OPEN_FILTER = "open_filter" // $NON-NLS-1$ const val OPEN_FILTER = "open_filter" // $NON-NLS-1$
const val LOAD_FILTER = "load_filter" const val LOAD_FILTER = "load_filter"
const val CREATE_TASK = "open_task" // $NON-NLS-1$ const val CREATE_TASK = "open_task" // $NON-NLS-1$
@ -494,16 +365,17 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
const val FINISH_AFFINITY = "finish_affinity" const val FINISH_AFFINITY = "finish_affinity"
private const val FRAG_TAG_TASK_LIST = "frag_tag_task_list" private const val FRAG_TAG_TASK_LIST = "frag_tag_task_list"
private const val FRAG_TAG_WHATS_NEW = "frag_tag_whats_new" private const val FRAG_TAG_WHATS_NEW = "frag_tag_whats_new"
private const val FRAG_TAG_NAV_DRAWER = "frag_tag_nav_drawer" private const val FRAG_TAG_TASK_EDIT = "frag_tag_task_edit"
private const val EXTRA_FILTER = "extra_filter"
private const val FLAG_FROM_HISTORY private const val FLAG_FROM_HISTORY
= Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
const val REQUEST_NEW_LIST = 10100
const val REQUEST_NEW_PLACE = 10104
val Intent.getFilter: Filter? val Intent.getFilter: Filter?
get() = if (isFromHistory) { get() = if (isFromHistory) {
null null
} else { } else {
getParcelableExtra<Filter?>(OPEN_FILTER)?.let { getParcelableExtra(this, OPEN_FILTER, Filter::class.java)?.let {
removeExtra(OPEN_FILTER) removeExtra(OPEN_FILTER)
it it
} }
@ -546,10 +418,9 @@ class MainActivity : InjectingAppCompatActivity(), TaskListFragmentCallbackHandl
get() = flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT > 0 get() = flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT > 0
val Intent.flagsToString val Intent.flagsToString
get() = if (BuildConfig.DEBUG) "" else get() = Intent::class.java.declaredFields
Intent::class.java.declaredFields .filter { it.name.startsWith("FLAG_") }
.filter { it.name.startsWith("FLAG_") } .filter { flags or it.getInt(null) == flags }
.filter { flags or it.getInt(null) == flags } .joinToString(" | ") { it.name }
.joinToString(" | ") { it.name }
} }
} }

@ -0,0 +1,205 @@
package com.todoroo.astrid.activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.todoroo.astrid.activity.MainActivity.Companion.LOAD_FILTER
import com.todoroo.astrid.activity.MainActivity.Companion.OPEN_FILTER
import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.CustomFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.api.TagFilter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Tasks.Companion.IS_GENERIC
import org.tasks.billing.Inventory
import org.tasks.compose.drawer.DrawerItem
import org.tasks.data.dao.CaldavDao
import org.tasks.data.NO_COUNT
import org.tasks.data.entity.Task
import org.tasks.data.dao.TaskDao
import org.tasks.data.count
import org.tasks.filters.FilterProvider
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.PlaceFilter
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.themes.ColorProvider
import org.tasks.themes.CustomIcons
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class MainActivityViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val defaultFilterProvider: DefaultFilterProvider,
private val filterProvider: FilterProvider,
private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
private val inventory: Inventory,
private val colorProvider: ColorProvider,
private val caldavDao: CaldavDao,
private val preferences: Preferences,
) : ViewModel() {
data class State(
val begForMoney: Boolean = false,
val filter: Filter,
val task: Task? = null,
val drawerOpen: Boolean = false,
val drawerItems: ImmutableList<DrawerItem> = persistentListOf(),
)
private val _state = MutableStateFlow(
State(
filter = savedStateHandle.get<Filter>(OPEN_FILTER)
?: savedStateHandle.get<String>(LOAD_FILTER)?.let {
runBlocking { defaultFilterProvider.getFilterFromPreference(it) }
}
?: runBlocking { defaultFilterProvider.getStartupFilter() },
begForMoney = if (IS_GENERIC) !inventory.hasTasksAccount else !inventory.hasPro,
)
)
val state = _state.asStateFlow()
private val refreshReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
LocalBroadcastManager.REFRESH,
LocalBroadcastManager.REFRESH_LIST -> updateFilters()
}
}
}
suspend fun resetFilter() {
setFilter(defaultFilterProvider.getDefaultOpenFilter())
}
fun setFilter(
filter: Filter,
task: Task? = null,
) {
if (filter == _state.value.filter && task == null) {
return
}
_state.update {
it.copy(
filter = filter,
task = task,
)
}
updateFilters()
defaultFilterProvider.setLastViewedFilter(filter)
}
fun setDrawerOpen(open: Boolean) {
_state.update { it.copy(drawerOpen = open) }
}
init {
localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
updateFilters()
}
override fun onCleared() {
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
fun updateFilters() = viewModelScope.launch(Dispatchers.Default) {
val selected = state.value.filter
filterProvider
.drawerItems()
.map { item ->
when (item) {
is Filter ->
DrawerItem.Filter(
title = item.title ?: "",
icon = getIcon(item),
color = getColor(item),
count = item.count.takeIf { it != NO_COUNT } ?: try {
taskDao.count(item)
} catch (e: Exception) {
Timber.e(e)
0
},
selected = item.areItemsTheSame(selected),
shareCount = if (item is CaldavFilter) item.principals else 0,
type = { item },
)
is NavigationDrawerSubheader ->
DrawerItem.Header(
title = item.title ?: "",
collapsed = item.isCollapsed,
hasError = item.error,
canAdd = item.addIntent != null,
type = { item },
)
else -> throw IllegalArgumentException()
}
}
.let { filters -> _state.update { it.copy(drawerItems = filters.toPersistentList()) } }
}
private fun getColor(filter: Filter): Int {
if (filter.tint != 0) {
val color = colorProvider.getThemeColor(filter.tint, true)
if (color.isFree || inventory.purchasedThemes()) {
return color.primaryColor
}
}
return 0
}
private fun getIcon(filter: Filter): Int {
if (filter.icon < 1000 || filter.icon == CustomIcons.PLACE || inventory.hasPro) {
val icon = CustomIcons.getIconResId(filter.icon)
if (icon != null) {
return icon
}
}
return when (filter) {
is TagFilter -> R.drawable.ic_outline_label_24px
is GtasksFilter,
is CaldavFilter -> R.drawable.ic_list_24px
is CustomFilter -> R.drawable.ic_outline_filter_list_24px
is PlaceFilter -> R.drawable.ic_outline_place_24px
else -> filter.icon
}
}
fun toggleCollapsed(subheader: NavigationDrawerSubheader) = viewModelScope.launch {
val collapsed = !subheader.isCollapsed
when (subheader.subheaderType) {
NavigationDrawerSubheader.SubheaderType.PREFERENCE -> {
preferences.setBoolean(subheader.id.toInt(), collapsed)
localBroadcastManager.broadcastRefreshList()
}
NavigationDrawerSubheader.SubheaderType.GOOGLE_TASKS,
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS,
NavigationDrawerSubheader.SubheaderType.ETESYNC -> {
caldavDao.setCollapsed(subheader.id, collapsed)
localBroadcastManager.broadcastRefreshList()
}
}
}
fun setTask(task: Task?) {
_state.update { it.copy(task = task) }
}
}

@ -4,16 +4,16 @@ import android.content.ContentResolver
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.utility.Constants import com.todoroo.astrid.utility.Constants
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.data.TaskAttachment import org.tasks.data.entity.TaskAttachment
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.intents.TaskIntents import org.tasks.intents.TaskIntents
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import timber.log.Timber import timber.log.Timber
@ -25,7 +25,7 @@ import javax.inject.Inject
* Create a new task based on incoming links from the "share" menu * Create a new task based on incoming links from the "share" menu
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class ShareLinkActivity : InjectingAppCompatActivity() { class ShareLinkActivity : AppCompatActivity() {
@Inject lateinit var taskCreator: TaskCreator @Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
@ -33,9 +33,8 @@ class ShareLinkActivity : InjectingAppCompatActivity() {
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val intent = intent val intent = intent
val action = intent.action when (intent.action) {
when { Intent.ACTION_PROCESS_TEXT -> lifecycleScope.launch {
Intent.ACTION_PROCESS_TEXT == action -> lifecycleScope.launch {
val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT) val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)
if (text != null) { if (text != null) {
val task = taskCreator.createWithValues(text.toString()) val task = taskCreator.createWithValues(text.toString())
@ -44,10 +43,9 @@ class ShareLinkActivity : InjectingAppCompatActivity() {
} }
finish() finish()
} }
Intent.ACTION_SEND == action -> lifecycleScope.launch {
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT) Intent.ACTION_SEND -> lifecycleScope.launch {
val task = taskCreator.createWithValues(subject) val task = taskCreator.create(intent)
task.notes = intent.getStringExtra(Intent.EXTRA_TEXT)
if (hasAttachments(intent)) { if (hasAttachments(intent)) {
task.putTransitory(TaskAttachment.KEY, copyAttachment(intent)) task.putTransitory(TaskAttachment.KEY, copyAttachment(intent))
firebase.addTask("share_attachment") firebase.addTask("share_attachment")
@ -57,9 +55,9 @@ class ShareLinkActivity : InjectingAppCompatActivity() {
editTask(task) editTask(task)
finish() finish()
} }
Intent.ACTION_SEND_MULTIPLE == action -> lifecycleScope.launch {
val task = taskCreator.createWithValues(intent.getStringExtra(Intent.EXTRA_SUBJECT)) Intent.ACTION_SEND_MULTIPLE -> lifecycleScope.launch {
task.notes = intent.getStringExtra(Intent.EXTRA_TEXT) val task = taskCreator.create(intent)
if (hasAttachments(intent)) { if (hasAttachments(intent)) {
task.putTransitory(TaskAttachment.KEY, copyMultipleAttachments(intent)) task.putTransitory(TaskAttachment.KEY, copyMultipleAttachments(intent))
firebase.addTask("share_multiple_attachments") firebase.addTask("share_multiple_attachments")
@ -69,11 +67,13 @@ class ShareLinkActivity : InjectingAppCompatActivity() {
editTask(task) editTask(task)
finish() finish()
} }
Intent.ACTION_VIEW == action -> lifecycleScope.launch {
Intent.ACTION_VIEW -> lifecycleScope.launch {
editTask(taskCreator.createWithValues("")) editTask(taskCreator.createWithValues(""))
firebase.addTask("action_view") firebase.addTask("action_view")
finish() finish()
} }
else -> { else -> {
Timber.e("Unhandled intent: %s", intent) Timber.e("Unhandled intent: %s", intent)
finish() finish()
@ -111,5 +111,16 @@ class ShareLinkActivity : InjectingAppCompatActivity() {
companion object { companion object {
private val ATTACHMENT_TYPES = listOf("image/", "application/", "audio/") private val ATTACHMENT_TYPES = listOf("image/", "application/", "audio/")
private suspend fun TaskCreator.create(intent: Intent): Task {
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
val hasSubject = subject?.isNotBlank() == true
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
val task = createWithValues(if (hasSubject) subject else text)
if (hasSubject) {
task.notes = text
}
return task
}
} }
} }

@ -10,13 +10,13 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Paint import android.graphics.Paint
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
@ -35,6 +35,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidViewBinding import androidx.compose.ui.viewinterop.AndroidViewBinding
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.BundleCompat
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@ -42,16 +43,15 @@ import androidx.lifecycle.lifecycleScope
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback
import com.google.android.material.composethemeadapter.MdcTheme import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities.atLeastOreoMR1
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.files.FilesControlSet import com.todoroo.astrid.files.FilesControlSet
import com.todoroo.astrid.repeats.RepeatControlSet import com.todoroo.astrid.repeats.RepeatControlSet
import com.todoroo.astrid.tags.TagsControlSet import com.todoroo.astrid.tags.TagsControlSet
import com.todoroo.astrid.timers.TimerControlSet import com.todoroo.astrid.timers.TimerControlSet
import com.todoroo.astrid.timers.TimerPlugin
import com.todoroo.astrid.ui.ReminderControlSet import com.todoroo.astrid.ui.ReminderControlSet
import com.todoroo.astrid.ui.StartDateControlSet import com.todoroo.astrid.ui.StartDateControlSet
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -71,10 +71,10 @@ import org.tasks.compose.edit.DueDateRow
import org.tasks.compose.edit.InfoRow import org.tasks.compose.edit.InfoRow
import org.tasks.compose.edit.ListRow import org.tasks.compose.edit.ListRow
import org.tasks.compose.edit.PriorityRow import org.tasks.compose.edit.PriorityRow
import org.tasks.data.Alarm import org.tasks.data.entity.Alarm
import org.tasks.data.Location import org.tasks.data.Location
import org.tasks.data.TagData import org.tasks.data.entity.TagData
import org.tasks.data.UserActivityDao import org.tasks.data.dao.UserActivityDao
import org.tasks.databinding.FragmentTaskEditBinding import org.tasks.databinding.FragmentTaskEditBinding
import org.tasks.databinding.TaskEditCalendarBinding import org.tasks.databinding.TaskEditCalendarBinding
import org.tasks.databinding.TaskEditFilesBinding import org.tasks.databinding.TaskEditFilesBinding
@ -91,6 +91,7 @@ import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.FilterPicker.Companion.newFilterPicker import org.tasks.dialogs.FilterPicker.Companion.newFilterPicker
import org.tasks.dialogs.FilterPicker.Companion.setFilterPickerResultListener import org.tasks.dialogs.FilterPicker.Companion.setFilterPickerResultListener
import org.tasks.dialogs.Linkify import org.tasks.dialogs.Linkify
import org.tasks.extensions.hideKeyboard
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.fragments.TaskEditControlSetFragmentManager import org.tasks.fragments.TaskEditControlSetFragmentManager
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_CREATION import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_CREATION
@ -100,6 +101,7 @@ import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_LIST
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_PRIORITY import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_PRIORITY
import org.tasks.markdown.MarkdownProvider import org.tasks.markdown.MarkdownProvider
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.play.PlayServices
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.ui.CalendarControlSet import org.tasks.ui.CalendarControlSet
import org.tasks.ui.ChipProvider import org.tasks.ui.ChipProvider
@ -124,12 +126,12 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var taskEditControlSetFragmentManager: TaskEditControlSetFragmentManager @Inject lateinit var taskEditControlSetFragmentManager: TaskEditControlSetFragmentManager
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
@Inject lateinit var timerPlugin: TimerPlugin
@Inject lateinit var linkify: Linkify @Inject lateinit var linkify: Linkify
@Inject lateinit var markdownProvider: MarkdownProvider @Inject lateinit var markdownProvider: MarkdownProvider
@Inject lateinit var taskEditEventBus: TaskEditEventBus @Inject lateinit var taskEditEventBus: TaskEditEventBus
@Inject lateinit var locale: Locale @Inject lateinit var locale: Locale
@Inject lateinit var chipProvider: ChipProvider @Inject lateinit var chipProvider: ChipProvider
@Inject lateinit var playServices: PlayServices
val editViewModel: TaskEditViewModel by viewModels() val editViewModel: TaskEditViewModel by viewModels()
lateinit var binding: FragmentTaskEditBinding lateinit var binding: FragmentTaskEditBinding
@ -139,8 +141,24 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
activity?.recreate() activity?.recreate()
} }
val task: Task?
get() = BundleCompat.getParcelable(requireArguments(), EXTRA_TASK, Task::class.java)
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
requireActivity().onBackPressedDispatcher.addCallback(owner = viewLifecycleOwner) {
if (preferences.backButtonSavesTask()) {
lifecycleScope.launch {
save()
}
} else {
discardButtonClick()
}
}
if (atLeastOreoMR1()) {
activity?.setShowWhenLocked(preferences.showEditScreenWithoutUnlock)
}
binding = FragmentTaskEditBinding.inflate(inflater) binding = FragmentTaskEditBinding.inflate(inflater)
val view: View = binding.root val view: View = binding.root
val model = editViewModel.task val model = editViewModel.task
@ -295,6 +313,13 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
return view return view
} }
override fun onDestroyView() {
super.onDestroyView()
if (atLeastOreoMR1()) {
activity?.setShowWhenLocked(false)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
taskEditEventBus taskEditEventBus
.onEach(this::process) .onEach(this::process)
@ -347,7 +372,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
AndroidUtilities.hideKeyboard(activity) activity?.hideKeyboard()
if (item.itemId == R.id.menu_delete) { if (item.itemId == R.id.menu_delete) {
deleteButtonClick() deleteButtonClick()
return true return true
@ -358,34 +383,12 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
return false return false
} }
suspend fun stopTimer(): Task { suspend fun save(remove: Boolean = true) {
val model = editViewModel.task editViewModel.save(remove)
timerPlugin.stopTimer(model) activity?.let { playServices.requestReview(it) }
val elapsedTime = DateUtils.formatElapsedTime(model.elapsedSeconds.toLong())
editViewModel.addComment(String.format(
"%s %s\n%s %s", // $NON-NLS-1$
getString(R.string.TEA_timer_comment_stopped),
DateUtilities.getTimeString(context, newDateTime()),
getString(R.string.TEA_timer_comment_spent),
elapsedTime),
null)
return model
}
suspend fun startTimer(): Task {
val model = editViewModel.task
timerPlugin.startTimer(model)
editViewModel.addComment(String.format(
"%s %s",
getString(R.string.TEA_timer_comment_started),
DateUtilities.getTimeString(context, newDateTime())),
null)
return model
} }
suspend fun save(remove: Boolean = true) = editViewModel.save(remove) private fun discardButtonClick() {
fun discardButtonClick() {
if (editViewModel.hasChanges()) { if (editViewModel.hasChanges()) {
dialogBuilder dialogBuilder
.newDialog(R.string.discard_confirmation) .newDialog(R.string.discard_confirmation)
@ -450,13 +453,14 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
onClick = { onClick = {
DateTimePicker DateTimePicker
.newDateTimePicker( .newDateTimePicker(
this@TaskEditFragment, target = this@TaskEditFragment,
REQUEST_DATE, rc = REQUEST_DATE,
editViewModel.dueDate.value, current = editViewModel.dueDate.value,
preferences.getBoolean( autoClose = preferences.getBoolean(
R.string.p_auto_dismiss_datetime_edit_screen, R.string.p_auto_dismiss_datetime_edit_screen,
false false
) ),
hideNoDate = editViewModel.recurrence.value?.isNotBlank() == true,
) )
.show(parentFragmentManager, FRAG_TAG_DATE_PICKER) .show(parentFragmentManager, FRAG_TAG_DATE_PICKER)
} }
@ -525,7 +529,6 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
companion object { companion object {
const val TAG_TASKEDIT_FRAGMENT = "taskedit_fragment"
const val EXTRA_TASK = "extra_task" const val EXTRA_TASK = "extra_task"
const val EXTRA_LIST = "extra_list" const val EXTRA_LIST = "extra_list"
const val EXTRA_LOCATION = "extra_location" const val EXTRA_LOCATION = "extra_location"

@ -6,6 +6,7 @@
package com.todoroo.astrid.activity package com.todoroo.astrid.activity
import android.app.Activity import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -18,22 +19,23 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import androidx.core.content.IntentCompat
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.setMargins import androidx.core.view.setMargins
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -47,42 +49,38 @@ import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.composethemeadapter.MdcTheme import com.google.android.material.composethemeadapter.MdcTheme
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.adapter.TaskAdapter import com.todoroo.astrid.adapter.TaskAdapter
import com.todoroo.astrid.adapter.TaskAdapterProvider import com.todoroo.astrid.adapter.TaskAdapterProvider
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_OLD_DUE_DATE
import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_TASK_ID import com.todoroo.astrid.api.AstridApiConstants.EXTRAS_TASK_ID
import com.todoroo.astrid.api.AstridOrderingFilter
import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.CustomFilter
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.FilterImpl
import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.api.IdListFilter
import com.todoroo.astrid.api.SearchFilter
import com.todoroo.astrid.api.TagFilter import com.todoroo.astrid.api.TagFilter
import com.todoroo.astrid.core.BuiltInFilterExposer import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.repeats.RepeatTaskHelper import com.todoroo.astrid.repeats.RepeatTaskHelper
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskDuplicator import com.todoroo.astrid.service.TaskDuplicator
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.timers.TimerPlugin import com.todoroo.astrid.timers.TimerPlugin
import com.todoroo.astrid.utility.Flags import com.todoroo.astrid.utility.Flags
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.ShortcutManager import org.tasks.ShortcutManager
import org.tasks.Tasks.Companion.IS_GOOGLE_PLAY import org.tasks.Tasks
import org.tasks.activities.FilterSettingsActivity import org.tasks.activities.FilterSettingsActivity
import org.tasks.activities.GoogleTaskListSettingsActivity import org.tasks.activities.GoogleTaskListSettingsActivity
import org.tasks.activities.PlaceSettingsActivity import org.tasks.activities.PlaceSettingsActivity
@ -91,38 +89,49 @@ import org.tasks.analytics.Firebase
import org.tasks.billing.PurchaseActivity import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.SubscriptionNagBanner import org.tasks.compose.SubscriptionNagBanner
import org.tasks.data.CaldavDao import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.data.TagDataDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.db.Database
import org.tasks.data.db.SuspendDbUtils.chunkedMap
import org.tasks.data.entity.Tag
import org.tasks.data.entity.Task
import org.tasks.data.listSettingsClass
import org.tasks.data.sql.Join
import org.tasks.data.sql.QueryTemplate
import org.tasks.data.withTransaction
import org.tasks.databinding.FragmentTaskListBinding import org.tasks.databinding.FragmentTaskListBinding
import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.FilterPicker.Companion.newFilterPicker import org.tasks.dialogs.FilterPicker.Companion.newFilterPicker
import org.tasks.dialogs.FilterPicker.Companion.setFilterPickerResultListener import org.tasks.dialogs.FilterPicker.Companion.setFilterPickerResultListener
import org.tasks.dialogs.PriorityPicker.Companion.newPriorityPicker
import org.tasks.dialogs.SortSettingsActivity import org.tasks.dialogs.SortSettingsActivity
import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.openUri
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.toast
import org.tasks.extensions.Fragment.safeStartActivityForResult import org.tasks.extensions.Fragment.safeStartActivityForResult
import org.tasks.extensions.formatNumber import org.tasks.extensions.hideKeyboard
import org.tasks.extensions.setOnQueryTextListener import org.tasks.extensions.setOnQueryTextListener
import org.tasks.filters.PlaceFilter import org.tasks.filters.PlaceFilter
import org.tasks.intents.TaskIntents import org.tasks.markdown.MarkdownProvider
import org.tasks.notifications.NotificationManager
import org.tasks.preferences.Device import org.tasks.preferences.Device
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters import org.tasks.sync.SyncAdapters
import org.tasks.tags.TagPickerActivity import org.tasks.tags.TagPickerActivity
import org.tasks.tasklist.DragAndDropRecyclerAdapter import org.tasks.tasklist.DragAndDropRecyclerAdapter
import org.tasks.tasklist.SectionedDataSource
import org.tasks.tasklist.TaskViewHolder import org.tasks.tasklist.TaskViewHolder
import org.tasks.tasklist.ViewHolderFactory import org.tasks.tasklist.ViewHolderFactory
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
import org.tasks.themes.ThemeColor import org.tasks.themes.ThemeColor
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.ui.TaskEditEvent import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskListEvent import org.tasks.ui.TaskListEvent
import org.tasks.ui.TaskListEventBus import org.tasks.ui.TaskListEventBus
import org.tasks.ui.TaskListViewModel import org.tasks.ui.TaskListViewModel
import org.tasks.ui.TaskListViewModel.Companion.createSearchQuery
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -132,11 +141,9 @@ import kotlin.math.max
class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickListener, class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickListener,
MenuItem.OnActionExpandListener, SearchView.OnQueryTextListener, ActionMode.Callback, MenuItem.OnActionExpandListener, SearchView.OnQueryTextListener, ActionMode.Callback,
TaskViewHolder.ViewHolderCallbacks { TaskViewHolder.ViewHolderCallbacks {
private val refreshReceiver = RefreshReceiver()
private val repeatConfirmationReceiver = RepeatConfirmationReceiver() private val repeatConfirmationReceiver = RepeatConfirmationReceiver()
@Inject lateinit var syncAdapters: SyncAdapters @Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var taskCreator: TaskCreator @Inject lateinit var taskCreator: TaskCreator
@ -152,7 +159,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var defaultThemeColor: ThemeColor @Inject lateinit var defaultThemeColor: ThemeColor
@Inject lateinit var colorProvider: ColorProvider @Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var shortcutManager: ShortcutManager @Inject lateinit var shortcutManager: ShortcutManager
@Inject lateinit var taskCompleter: TaskCompleter @Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var locale: Locale @Inject lateinit var locale: Locale
@ -160,25 +166,48 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
@Inject lateinit var repeatTaskHelper: RepeatTaskHelper @Inject lateinit var repeatTaskHelper: RepeatTaskHelper
@Inject lateinit var taskListEventBus: TaskListEventBus @Inject lateinit var taskListEventBus: TaskListEventBus
@Inject lateinit var taskEditEventBus: TaskEditEventBus @Inject lateinit var taskEditEventBus: TaskEditEventBus
@Inject lateinit var database: Database
private lateinit var swipeRefreshLayout: SwipeRefreshLayout @Inject lateinit var markdown: MarkdownProvider
private lateinit var emptyRefreshLayout: SwipeRefreshLayout
private lateinit var coordinatorLayout: CoordinatorLayout
private lateinit var recyclerView: RecyclerView
private val listViewModel: TaskListViewModel by viewModels() private val listViewModel: TaskListViewModel by viewModels()
private val mainViewModel: MainActivityViewModel by activityViewModels()
private lateinit var taskAdapter: TaskAdapter private lateinit var taskAdapter: TaskAdapter
private var recyclerAdapter: DragAndDropRecyclerAdapter? = null private var recyclerAdapter: DragAndDropRecyclerAdapter? = null
private lateinit var filter: Filter private lateinit var filter: Filter
private var searchJob: Job? = null
private lateinit var search: MenuItem private lateinit var search: MenuItem
private var searchQuery: String? = null
private var mode: ActionMode? = null private var mode: ActionMode? = null
lateinit var themeColor: ThemeColor lateinit var themeColor: ThemeColor
private lateinit var callbacks: TaskListFragmentCallbackHandler
private lateinit var binding: FragmentTaskListBinding private lateinit var binding: FragmentTaskListBinding
@OptIn(ExperimentalAnimationApi::class) private val sortRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
result.data?.let { data ->
if (data.getBooleanExtra(SortSettingsActivity.EXTRA_FORCE_RELOAD, false)) {
activity?.recreate()
}
if (data.getBooleanExtra(SortSettingsActivity.EXTRA_CHANGED_GROUP, false)) {
listViewModel.clearCollapsed()
}
listViewModel.invalidate()
}
}
}
private val listSettingsRequest =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode != RESULT_OK) return@registerForActivityResult
val data = result.data ?: return@registerForActivityResult
when (data.action) {
ACTION_DELETED ->
mainViewModel.setFilter(BuiltInFilterExposer.getMyTasksFilter(resources))
ACTION_RELOAD ->
IntentCompat.getParcelableExtra(data, MainActivity.OPEN_FILTER, Filter::class.java)?.let {
mainViewModel.setFilter(it)
}
}
}
private fun process(event: TaskListEvent) = when (event) { private fun process(event: TaskListEvent) = when (event) {
is TaskListEvent.TaskCreated -> is TaskListEvent.TaskCreated ->
onTaskCreated(event.uuid) onTaskCreated(event.uuid)
@ -186,51 +215,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
makeSnackbar(R.string.calendar_event_created, event.title) makeSnackbar(R.string.calendar_event_created, event.title)
?.setAction(R.string.action_open) { context?.openUri(event.uri) } ?.setAction(R.string.action_open) { context?.openUri(event.uri) }
?.show() ?.show()
is TaskListEvent.BegForSubscription -> {
binding.banner.setContent {
var showBanner by rememberSaveable { mutableStateOf(true) }
MdcTheme {
SubscriptionNagBanner(
visible = showBanner,
subscribe = {
showBanner = false
preferences.lastSubscribeRequest = now()
purchase()
firebase.logEvent(R.string.event_banner_sub, R.string.param_click to true)
},
dismiss = {
showBanner = false
preferences.lastSubscribeRequest = now()
firebase.logEvent(R.string.event_banner_sub, R.string.param_click to false)
},
)
}
}
}
}
private fun purchase() {
if (IS_GOOGLE_PLAY) {
startActivity(Intent(context, PurchaseActivity::class.java))
} else {
preferences.lastSubscribeRequest = now()
context?.openUri(R.string.url_donate)
}
} }
override fun onRefresh() { override fun onRefresh() {
syncAdapters.sync(true) syncAdapters.sync(true)
lifecycleScope.launch {
delay(1000)
refresh()
}
}
private fun setSyncOngoing() {
AndroidUtilities.assertMainThread()
val ongoing = preferences.isSyncOngoing
swipeRefreshLayout.isRefreshing = ongoing
emptyRefreshLayout.isRefreshing = ongoing
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) { override fun onViewStateRestored(savedInstanceState: Bundle?) {
@ -244,17 +232,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
} }
override fun onAttach(activity: Activity) {
super.onAttach(activity)
callbacks = activity as TaskListFragmentCallbackHandler
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
val selectedTaskIds: List<Long> = taskAdapter.getSelected() val selectedTaskIds: List<Long> = taskAdapter.getSelected()
outState.putLongArray(EXTRA_SELECTED_TASK_IDS, selectedTaskIds.toLongArray()) outState.putLongArray(EXTRA_SELECTED_TASK_IDS, selectedTaskIds.toLongArray())
outState.putString(EXTRA_SEARCH, searchQuery)
outState.putLongArray(EXTRA_COLLAPSED, taskAdapter.getCollapsed().toLongArray())
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -265,52 +246,67 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
.launchIn(viewLifecycleOwner.lifecycleScope) .launchIn(viewLifecycleOwner.lifecycleScope)
} }
@OptIn(ExperimentalAnimationApi::class)
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
requireActivity().onBackPressedDispatcher.addCallback(owner = viewLifecycleOwner) {
if (search.isActionViewExpanded) {
search.collapseActionView()
} else {
requireActivity().finish()
if (!preferences.getBoolean(R.string.p_open_last_viewed_list, true)) {
runBlocking {
mainViewModel.resetFilter()
}
}
}
}
binding = FragmentTaskListBinding.inflate(inflater, container, false) binding = FragmentTaskListBinding.inflate(inflater, container, false)
filter = getFilter() filter = getFilter()
val swipeRefreshLayout: SwipeRefreshLayout
val emptyRefreshLayout: SwipeRefreshLayout
val recyclerView: RecyclerView
with (binding) { with (binding) {
swipeRefreshLayout = bodyStandard.swipeLayout swipeRefreshLayout = bodyStandard.swipeLayout
emptyRefreshLayout = bodyEmpty.swipeLayoutEmpty emptyRefreshLayout = bodyEmpty.swipeLayoutEmpty
coordinatorLayout = taskListCoordinator
recyclerView = bodyStandard.recyclerView recyclerView = bodyStandard.recyclerView
fab.setOnClickListener { createNewTask() } fab.setOnClickListener { createNewTask() }
fab.isVisible = filter.isWritable fab.isVisible = filter.isWritable
} }
themeColor = if (filter.tint != 0) colorProvider.getThemeColor(filter.tint, true) else defaultThemeColor themeColor = if (filter.tint != 0) colorProvider.getThemeColor(filter.tint, true) else defaultThemeColor
filter.setFilterQueryOverride(null) (filter as? AstridOrderingFilter)?.filterOverride = null
// set up list adapters // set up list adapters
taskAdapter = taskAdapterProvider.createTaskAdapter(filter) taskAdapter = taskAdapterProvider.createTaskAdapter(filter)
taskAdapter.setCollapsed(savedInstanceState?.getLongArray(EXTRA_COLLAPSED)) listViewModel.setFilter(filter)
if (savedInstanceState != null) {
searchQuery = savedInstanceState.getString(EXTRA_SEARCH)
}
listViewModel.setFilter((if (searchQuery == null) filter else createSearchFilter(searchQuery!!)))
(recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false (recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.layoutManager = LinearLayoutManager(context)
lifecycleScope.launch { lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
listViewModel.tasks.collect { listViewModel.state.collect {
submitList(it) if (it.tasks is TaskListViewModel.TasksResults.Results) {
if (it.isEmpty()) { submitList(it.tasks.tasks)
swipeRefreshLayout.visibility = View.GONE if (it.tasks.tasks.isEmpty()) {
emptyRefreshLayout.visibility = View.VISIBLE swipeRefreshLayout.visibility = View.GONE
} else { emptyRefreshLayout.visibility = View.VISIBLE
swipeRefreshLayout.visibility = View.VISIBLE } else {
emptyRefreshLayout.visibility = View.GONE swipeRefreshLayout.visibility = View.VISIBLE
emptyRefreshLayout.visibility = View.GONE
}
swipeRefreshLayout.isRefreshing = it.syncOngoing
emptyRefreshLayout.isRefreshing = it.syncOngoing
} }
} }
} }
} }
setupRefresh(swipeRefreshLayout) setupRefresh(swipeRefreshLayout)
setupRefresh(emptyRefreshLayout) setupRefresh(emptyRefreshLayout)
binding.toolbar.title = filter.listingTitle binding.toolbar.title = filter.title
binding.appbarlayout.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> binding.appbarlayout.addOnOffsetChangedListener { _, verticalOffset ->
if (verticalOffset == 0 && binding.bottomAppBar.isScrolledDown) { if (verticalOffset == 0 && binding.bottomAppBar.isScrolledDown) {
binding.bottomAppBar.performShow() binding.bottomAppBar.performShow()
} }
}) }
val toolbar = if (preferences.isTopAppBar) { val toolbar = if (preferences.isTopAppBar) {
binding.bottomAppBar.isVisible = false binding.bottomAppBar.isVisible = false
with (binding.fab) { with (binding.fab) {
@ -333,7 +329,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
(binding.toolbar.layoutParams as AppBarLayout.LayoutParams).scrollFlags = 0 (binding.toolbar.layoutParams as AppBarLayout.LayoutParams).scrollFlags = 0
} }
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
toolbar.setNavigationOnClickListener { callbacks.onNavigationIconClicked() } toolbar.setNavigationOnClickListener {
activity?.hideKeyboard()
mainViewModel.setDrawerOpen(true)
}
setupMenu(toolbar) setupMenu(toolbar)
childFragmentManager.setFilterPickerResultListener(this) { childFragmentManager.setFilterPickerResultListener(this) {
val selected = taskAdapter.getSelected() val selected = taskAdapter.getSelected()
@ -342,14 +341,43 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
finishActionMode() finishActionMode()
} }
binding.banner.setContent {
val context = LocalContext.current
val showBanner = listViewModel.state.collectAsStateLifecycleAware().value.begForSubscription
MdcTheme {
SubscriptionNagBanner(
visible = showBanner,
subscribe = {
listViewModel.dismissBanner(clickedPurchase = true)
if (Tasks.IS_GOOGLE_PLAY) {
context.startActivity(Intent(context, PurchaseActivity::class.java))
} else {
preferences.lastSubscribeRequest = currentTimeMillis()
context.openUri(R.string.url_donate)
}
},
dismiss = {
listViewModel.dismissBanner(clickedPurchase = false)
},
)
}
}
return binding.root return binding.root
} }
private fun submitList(tasks: List<TaskContainer>) { private fun submitList(tasks: SectionedDataSource) {
if (recyclerAdapter !is DragAndDropRecyclerAdapter) { if (recyclerAdapter !is DragAndDropRecyclerAdapter) {
setAdapter( setAdapter(
DragAndDropRecyclerAdapter( DragAndDropRecyclerAdapter(
taskAdapter, recyclerView, viewHolderFactory, this, tasks, preferences)) adapter = taskAdapter,
recyclerView = binding.bodyStandard.recyclerView,
viewHolderFactory = viewHolderFactory,
taskList = this,
tasks = tasks,
preferences = preferences,
toggleCollapsed = { listViewModel.toggleCollapsed(it) },
)
)
} else { } else {
recyclerAdapter?.submitList(tasks) recyclerAdapter?.submitList(tasks)
} }
@ -357,19 +385,26 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
private fun setAdapter(adapter: DragAndDropRecyclerAdapter) { private fun setAdapter(adapter: DragAndDropRecyclerAdapter) {
recyclerAdapter = adapter recyclerAdapter = adapter
recyclerView.adapter = adapter binding.bodyStandard.recyclerView.adapter = adapter
taskAdapter.setDataSource(adapter) taskAdapter.setDataSource(adapter)
} }
private fun setupMenu(appBar: Toolbar) { private fun setupMenu(appBar: Toolbar) {
val menu = appBar.menu val menu = appBar.menu
menu.clear() menu.clear()
if (filter.hasBeginningMenu()) { if (filter is PlaceFilter) {
appBar.inflateMenu(filter.beginningMenu) appBar.inflateMenu(R.menu.menu_location_actions)
} }
appBar.inflateMenu(R.menu.menu_task_list_fragment_bottom) appBar.inflateMenu(R.menu.menu_task_list_fragment_bottom)
if (filter.hasMenu()) { when (filter) {
appBar.inflateMenu(filter.menu) is CaldavFilter -> R.menu.menu_caldav_list_fragment
is CustomFilter -> R.menu.menu_custom_filter
is GtasksFilter -> R.menu.menu_gtasks_list_fragment
is TagFilter -> R.menu.menu_tag_view_fragment
is PlaceFilter -> R.menu.menu_location_list_fragment
else -> null
}?.let {
appBar.inflateMenu(it)
} }
if (appBar is BottomAppBar) { if (appBar is BottomAppBar) {
menu.removeItem(R.id.menu_search) menu.removeItem(R.id.menu_search)
@ -399,33 +434,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
menu.findItem(R.id.menu_clear_completed).isVisible = filter.isWritable menu.findItem(R.id.menu_clear_completed).isVisible = filter.isWritable
} }
private fun openFilter(filter: Filter?) {
if (filter == null) {
startActivity(TaskIntents.getTaskListByIdIntent(context, null))
} else {
startActivity(TaskIntents.getTaskListIntent(context, filter))
}
}
private fun searchByQuery(query: String?) {
searchJob?.cancel()
searchJob = lifecycleScope.launch {
delay(SEARCH_DEBOUNCE_TIMEOUT)
searchQuery = query?.trim { it <= ' ' } ?: ""
if (searchQuery?.isEmpty() == true) {
listViewModel.setFilter(
BuiltInFilterExposer.getMyTasksFilter(requireContext().resources))
} else {
val savedFilter = createSearchFilter(searchQuery!!)
listViewModel.setFilter(savedFilter)
}
}
}
private fun createSearchFilter(query: String): Filter {
return SearchFilter(getString(R.string.FLA_search_filter, query), query)
}
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
R.id.menu_voice_add -> { R.id.menu_voice_add -> {
@ -446,13 +454,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
true true
} }
R.id.menu_sort -> { R.id.menu_sort -> {
requireActivity().startActivityForResult( sortRequest.launch(
SortSettingsActivity.getIntent( SortSettingsActivity.getIntent(
requireActivity(), requireActivity(),
filter.supportsManualSort(), filter.supportsManualSort(),
filter.supportsAstridSorting() && preferences.isAstridSortEnabled, filter is AstridOrderingFilter && preferences.isAstridSortEnabled,
), )
REQUEST_SORT
) )
true true
} }
@ -460,59 +467,78 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
preferences.showHidden = item.isChecked preferences.showHidden = item.isChecked
loadTaskListContent() loadTaskListContent()
localBroadcastManager.broadcastRefresh()
true true
} }
R.id.menu_show_completed -> { R.id.menu_show_completed -> {
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
preferences.showCompleted = item.isChecked preferences.showCompleted = item.isChecked
loadTaskListContent() loadTaskListContent()
localBroadcastManager.broadcastRefresh()
true true
} }
R.id.menu_clear_completed -> { R.id.menu_clear_completed -> {
dialogBuilder lifecycleScope.launch {
.newDialog(R.string.clear_completed_tasks_confirmation) val tasks = listViewModel.getTasksToClear()
.setPositiveButton(R.string.ok) { _, _ -> clearCompleted() } val countString = requireContext().resources.getQuantityString(R.plurals.Ntasks, tasks.size, tasks.size)
.setNegativeButton(R.string.cancel, null) if (tasks.isEmpty()) {
.show() context?.toast(R.string.delete_multiple_tasks_confirmation, countString)
} else {
dialogBuilder
.newDialog(R.string.clear_completed_tasks_confirmation)
.setMessage(R.string.clear_completed_tasks_count, countString)
.setPositiveButton(R.string.ok) { _, _ ->
lifecycleScope.launch {
listViewModel.markDeleted(tasks)
context?.toast(
R.string.delete_multiple_tasks_confirmation,
countString
)
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
true true
} }
R.id.menu_filter_settings -> { R.id.menu_filter_settings -> {
val filterSettings = Intent(activity, FilterSettingsActivity::class.java) listSettingsRequest.launch(
filterSettings.putExtra(FilterSettingsActivity.TOKEN_FILTER, filter) Intent(activity, FilterSettingsActivity::class.java)
startActivityForResult(filterSettings, REQUEST_LIST_SETTINGS) .putExtra(FilterSettingsActivity.TOKEN_FILTER, filter)
)
true true
} }
R.id.menu_caldav_list_fragment -> { R.id.menu_caldav_list_fragment -> {
val calendar = (filter as CaldavFilter).calendar val calendar = (filter as CaldavFilter).calendar
lifecycleScope.launch { lifecycleScope.launch {
val account = caldavDao.getAccountByUuid(calendar.account!!) val account = caldavDao.getAccountByUuid(calendar.account!!)
val caldavSettings = Intent(activity, account!!.listSettingsClass()) listSettingsRequest.launch(
.putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT, account) Intent(activity, account!!.listSettingsClass())
.putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_CALENDAR, calendar) .putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT, account)
startActivityForResult(caldavSettings, REQUEST_LIST_SETTINGS) .putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_CALENDAR, calendar)
)
} }
true true
} }
R.id.menu_location_settings -> { R.id.menu_location_settings -> {
val place = (filter as PlaceFilter).place val place = (filter as PlaceFilter).place
val intent = Intent(activity, PlaceSettingsActivity::class.java) listSettingsRequest.launch(
intent.putExtra(PlaceSettingsActivity.EXTRA_PLACE, place as Parcelable) Intent(activity, PlaceSettingsActivity::class.java)
startActivityForResult(intent, REQUEST_LIST_SETTINGS) .putExtra(PlaceSettingsActivity.EXTRA_PLACE, place as Parcelable)
)
true true
} }
R.id.menu_gtasks_list_settings -> { R.id.menu_gtasks_list_settings -> {
val gtasksSettings = Intent(activity, GoogleTaskListSettingsActivity::class.java) listSettingsRequest.launch(
gtasksSettings.putExtra( Intent(activity, GoogleTaskListSettingsActivity::class.java)
GoogleTaskListSettingsActivity.EXTRA_STORE_DATA, (filter as GtasksFilter).list) .putExtra(GoogleTaskListSettingsActivity.EXTRA_STORE_DATA, (filter as GtasksFilter).list)
startActivityForResult(gtasksSettings, REQUEST_LIST_SETTINGS) )
true true
} }
R.id.menu_tag_settings -> { R.id.menu_tag_settings -> {
val tagSettings = Intent(activity, TagSettingsActivity::class.java) listSettingsRequest.launch(
tagSettings.putExtra(TagSettingsActivity.EXTRA_TAG_DATA, (filter as TagFilter).tagData) Intent(activity, TagSettingsActivity::class.java)
startActivityForResult(tagSettings, REQUEST_LIST_SETTINGS) .putExtra(TagSettingsActivity.EXTRA_TAG_DATA, (filter as TagFilter).tagData)
)
true true
} }
R.id.menu_expand_subtasks -> { R.id.menu_expand_subtasks -> {
@ -543,11 +569,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
} }
private fun clearCompleted() = lifecycleScope.launch {
val count = taskDeleter.clearCompleted(filter)
context?.toast(R.string.delete_multiple_tasks_confirmation, locale.formatNumber(count))
}
private fun createNewTask() { private fun createNewTask() {
lifecycleScope.launch { lifecycleScope.launch {
shortcutManager.reportShortcutUsed(ShortcutManager.SHORTCUT_NEW_TASK) shortcutManager.reportShortcutUsed(ShortcutManager.SHORTCUT_NEW_TASK)
@ -571,9 +592,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
localBroadcastManager.registerRefreshListReceiver(refreshReceiver)
localBroadcastManager.registerTaskCompletedReceiver(repeatConfirmationReceiver) localBroadcastManager.registerTaskCompletedReceiver(repeatConfirmationReceiver)
refresh()
} }
private fun makeSnackbar(@StringRes res: Int, vararg args: Any?): Snackbar? { private fun makeSnackbar(@StringRes res: Int, vararg args: Any?): Snackbar? {
@ -581,7 +600,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
private fun makeSnackbar(text: String): Snackbar? = activity?.let { private fun makeSnackbar(text: String): Snackbar? = activity?.let {
Snackbar.make(coordinatorLayout, text, 4000) Snackbar.make(binding.taskListCoordinator, text, 4000)
.setAnchorView(R.id.fab) .setAnchorView(R.id.fab)
.setTextColor(it.getColor(R.color.snackbar_text_color)) .setTextColor(it.getColor(R.color.snackbar_text_color))
.setActionTextColor(it.getColor(R.color.snackbar_action_color)) .setActionTextColor(it.getColor(R.color.snackbar_action_color))
@ -593,16 +612,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
localBroadcastManager.unregisterReceiver(repeatConfirmationReceiver) localBroadcastManager.unregisterReceiver(repeatConfirmationReceiver)
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
fun collapseSearchView(): Boolean {
return (search.isActionViewExpanded
&& (search.collapseActionView() || !search.isActionViewExpanded))
}
private fun refresh() {
setSyncOngoing()
} }
fun loadTaskListContent() { fun loadTaskListContent() {
@ -639,7 +648,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
VOICE_RECOGNITION_REQUEST_CODE -> if (resultCode == Activity.RESULT_OK) { VOICE_RECOGNITION_REQUEST_CODE -> if (resultCode == Activity.RESULT_OK) {
lifecycleScope.launch { lifecycleScope.launch {
val match: List<String>? = data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) val match: List<String>? = data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
if (match != null && match.isNotEmpty() && match[0].isNotEmpty()) { if (!match.isNullOrEmpty() && match[0].isNotEmpty()) {
var recognizedSpeech = match[0] var recognizedSpeech = match[0]
recognizedSpeech = (recognizedSpeech.substring(0, 1) recognizedSpeech = (recognizedSpeech.substring(0, 1)
.uppercase(Locale.getDefault()) .uppercase(Locale.getDefault())
@ -649,15 +658,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
} }
} }
REQUEST_LIST_SETTINGS -> if (resultCode == Activity.RESULT_OK) { REQUEST_TAG_TASKS -> if (resultCode == RESULT_OK) {
val action = data!!.action
if (ACTION_DELETED == action) {
openFilter(BuiltInFilterExposer.getMyTasksFilter(resources))
} else if (ACTION_RELOAD == action) {
openFilter(data.getParcelableExtra(MainActivity.OPEN_FILTER))
}
}
REQUEST_TAG_TASKS -> if (resultCode == Activity.RESULT_OK) {
lifecycleScope.launch { lifecycleScope.launch {
val modified = tagDataDao.applyTags( val modified = tagDataDao.applyTags(
taskDao taskDao
@ -674,19 +675,13 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
} }
override fun onContextItemSelected(item: MenuItem): Boolean {
return onOptionsItemSelected(item)
}
private fun onTaskListItemClicked(task: Task?) = lifecycleScope.launch { private fun onTaskListItemClicked(task: Task?) = lifecycleScope.launch {
callbacks.onTaskListItemClicked(task) mainViewModel.setTask(task)
} }
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
search.setOnQueryTextListener(this) search.setOnQueryTextListener(this)
if (searchQuery == null) { listViewModel.setSearchQuery("")
searchByQuery("")
}
if (preferences.isTopAppBar) { if (preferences.isTopAppBar) {
binding.toolbar.menu.forEach { it.isVisible = false } binding.toolbar.menu.forEach { it.isVisible = false }
} }
@ -696,8 +691,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
search.setOnQueryTextListener(null) search.setOnQueryTextListener(null)
listViewModel.setFilter(filter) listViewModel.setFilter(filter)
searchJob?.cancel() listViewModel.setSearchQuery(null)
searchQuery = null
if (preferences.isTopAppBar) { if (preferences.isTopAppBar) {
setupMenu(binding.toolbar) setupMenu(binding.toolbar)
} }
@ -705,20 +699,16 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
openFilter(createSearchFilter(query.trim { it <= ' ' })) mainViewModel.setFilter(requireContext().createSearchQuery(query.trim()))
search.collapseActionView() search.collapseActionView()
return true return true
} }
override fun onQueryTextChange(query: String): Boolean { override fun onQueryTextChange(query: String): Boolean {
searchByQuery(query) listViewModel.setSearchQuery(query)
return true return true
} }
fun broadcastRefresh() {
localBroadcastManager.broadcastRefresh()
}
override fun onCreateActionMode(actionMode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(actionMode: ActionMode, menu: Menu): Boolean {
val inflater = actionMode.menuInflater val inflater = actionMode.menuInflater
inflater.inflate(R.menu.menu_multi_select, menu) inflater.inflate(R.menu.menu_multi_select, menu)
@ -750,6 +740,19 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
true true
} }
R.id.edit_priority -> {
lifecycleScope.launch {
taskDao
.fetch(selected)
.filterNot { it.readOnly }
.takeIf { it.isNotEmpty() }
?.let {
newPriorityPicker(preferences.getBoolean(R.string.p_desaturate_colors, false), it)
.show(parentFragmentManager, FRAG_TAG_PRIORITY_PICKER)
}
}
true
}
R.id.move_tasks -> { R.id.move_tasks -> {
lifecycleScope.launch { lifecycleScope.launch {
val singleFilter = taskMover.getSingleFilter(selected) val singleFilter = taskMover.getSingleFilter(selected)
@ -776,17 +779,26 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
R.id.menu_select_all -> { R.id.menu_select_all -> {
lifecycleScope.launch { lifecycleScope.launch {
taskAdapter.setSelected(taskDao.fetchTasks(preferences, filter) setSelected(taskDao.fetchTasks(preferences, filter)
.map(TaskContainer::id)) .map(TaskContainer::id))
updateModeTitle()
recyclerAdapter?.notifyDataSetChanged()
} }
true true
} }
R.id.menu_share -> { R.id.menu_share -> {
lifecycleScope.launch { lifecycleScope.launch {
selected.chunkedMap { taskDao.fetchTasks(preferences, IdListFilter(it)) } selected
.apply { send(this) } .chunkedMap {
taskDao.fetchTasks(
preferences,
FilterImpl(
sql = QueryTemplate()
.join(Join.left(Tag.TABLE, Tag.TASK.eq(Task.ID)))
.where(Task.ID.`in`(it))
.toString()
)
)
}
.let { send(it) }
} }
true true
} }
@ -819,7 +831,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
val intent = ShareCompat val intent = ShareCompat
.IntentBuilder(requireContext()) .IntentBuilder(requireContext())
.setType("text/plain") .setType("text/plain")
.setSubject(filter.listingTitle) .setSubject(filter.title)
.setText(output) .setText(output)
.createChooserIntent() .createChooserIntent()
startActivity(intent) startActivity(intent)
@ -844,11 +856,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
} }
interface TaskListFragmentCallbackHandler {
suspend fun onTaskListItemClicked(task: Task?)
fun onNavigationIconClicked()
}
val isActionModeActive: Boolean val isActionModeActive: Boolean
get() = mode != null get() = mode != null
@ -875,30 +882,34 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
finishActionMode() finishActionMode()
val result = withContext(NonCancellable) { val result = withContext(NonCancellable) {
taskDeleter.markDeleted(tasks) listViewModel.markDeleted(tasks)
} }
result.forEach { onTaskDelete(it) } result.forEach { onTaskDelete(it) }
makeSnackbar(R.string.delete_multiple_tasks_confirmation, result.size.toString())?.show() makeSnackbar(R.string.delete_multiple_tasks_confirmation, result.size.toString())?.show()
} }
private fun setSelected(tasks: List<Long>) {
taskAdapter.setSelected(tasks)
updateModeTitle()
recyclerAdapter?.notifyDataSetChanged()
}
private fun copySelectedItems(tasks: List<Long>) = lifecycleScope.launch { private fun copySelectedItems(tasks: List<Long>) = lifecycleScope.launch {
finishActionMode()
val duplicates = withContext(NonCancellable) { val duplicates = withContext(NonCancellable) {
taskDuplicator.duplicate(tasks) taskDuplicator.duplicate(tasks)
} }
onTaskCreated(duplicates) onTaskCreated(duplicates)
setSelected(duplicates.map(Task::id))
makeSnackbar(R.string.copy_multiple_tasks_confirmation, duplicates.size.toString())?.show() makeSnackbar(R.string.copy_multiple_tasks_confirmation, duplicates.size.toString())?.show()
} }
fun clearCollapsed() = taskAdapter.clearCollapsed()
override fun onCompletedTask(task: TaskContainer, newState: Boolean) { override fun onCompletedTask(task: TaskContainer, newState: Boolean) {
if (task.isReadOnly) { if (task.isReadOnly) {
return return
} }
lifecycleScope.launch { lifecycleScope.launch {
taskCompleter.setComplete(task.task, newState) taskCompleter.setComplete(task.task, newState)
taskAdapter.onCompletedTask(task, newState) taskAdapter.onCompletedTask(task.uuid, newState)
loadTaskListContent() loadTaskListContent()
} }
} }
@ -921,8 +932,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
override fun onClick(filter: Filter) { override fun onClick(filter: Filter) {
if (!isActionModeActive) { if (!isActionModeActive) {
val context = activity mainViewModel.setFilter(filter)
context?.startActivity(TaskIntents.getTaskListIntent(context, filter))
} }
} }
@ -949,18 +959,17 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
} }
private inner class RefreshReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
refresh()
}
}
private inner class RepeatConfirmationReceiver : BroadcastReceiver() { private inner class RepeatConfirmationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
lifecycleScope.launch { lifecycleScope.launch {
val tasks = val tasks =
(intent.getSerializableExtra(EXTRAS_TASK_ID) as? ArrayList<Long>) (intent.getSerializableExtra(EXTRAS_TASK_ID) as? ArrayList<Long>)
?.let { taskDao.fetch(it) } ?.let {
// hack to wait for task save transaction to complete
database.withTransaction {
taskDao.fetch(it)
}
}
?.filterNot { it.readOnly } ?.filterNot { it.readOnly }
?.takeIf { it.isNotEmpty() } ?.takeIf { it.isNotEmpty() }
?: return@launch ?: return@launch
@ -983,9 +992,10 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
if (isRecurringCompletion) { if (isRecurringCompletion) {
val task = tasks.first() val task = tasks.first()
val title = markdown.markdown(force = true).toMarkdown(task.title)
val text = getString( val text = getString(
R.string.repeat_snackbar, R.string.repeat_snackbar,
task.title, title,
DateUtilities.getRelativeDateTime( DateUtilities.getRelativeDateTime(
context, task.dueDate, locale, FormatStyle.LONG, true context, task.dueDate, locale, FormatStyle.LONG, true
) )
@ -1004,29 +1014,22 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
companion object { companion object {
const val TAGS_METADATA_JOIN = "for_tags" // $NON-NLS-1$
const val CALDAV_METADATA_JOIN = "for_caldav" // $NON-NLS-1$
const val ACTION_RELOAD = "action_reload" const val ACTION_RELOAD = "action_reload"
const val ACTION_DELETED = "action_deleted" const val ACTION_DELETED = "action_deleted"
private const val EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids" private const val EXTRA_SELECTED_TASK_IDS = "extra_selected_task_ids"
private const val EXTRA_SEARCH = "extra_search"
private const val EXTRA_COLLAPSED = "extra_collapsed"
private const val VOICE_RECOGNITION_REQUEST_CODE = 1234 private const val VOICE_RECOGNITION_REQUEST_CODE = 1234
private const val EXTRA_FILTER = "extra_filter" private const val EXTRA_FILTER = "extra_filter"
private const val FRAG_TAG_REMOTE_LIST_PICKER = "frag_tag_remote_list_picker" private const val FRAG_TAG_REMOTE_LIST_PICKER = "frag_tag_remote_list_picker"
private const val FRAG_TAG_DATE_TIME_PICKER = "frag_tag_date_time_picker" private const val FRAG_TAG_DATE_TIME_PICKER = "frag_tag_date_time_picker"
private const val REQUEST_LIST_SETTINGS = 10101 private const val FRAG_TAG_PRIORITY_PICKER = "frag_tag_priority_picker"
private const val REQUEST_TAG_TASKS = 10106 private const val REQUEST_TAG_TASKS = 10106
const val REQUEST_SORT = 10107
private const val SEARCH_DEBOUNCE_TIMEOUT = 300L fun newTaskListFragment(filter: Filter): TaskListFragment {
fun newTaskListFragment(context: Context, filter: Filter?): TaskListFragment {
val fragment = TaskListFragment() val fragment = TaskListFragment()
val bundle = Bundle() val bundle = Bundle()
bundle.putParcelable( bundle.putParcelable(EXTRA_FILTER, filter)
EXTRA_FILTER,
filter ?: BuiltInFilterExposer.getMyTasksFilter(context.resources))
fragment.arguments = bundle fragment.arguments = bundle
return fragment return fragment
} }
} }
} }

@ -1,38 +0,0 @@
package com.todoroo.astrid.adapter
import android.content.Context
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.todoroo.astrid.api.FilterListItem
import org.tasks.databinding.FilterAdapterActionBinding
import org.tasks.themes.DrawableUtil
class ActionViewHolder internal constructor(
private val context: Context,
itemView: View,
private val onClick: ((FilterListItem?) -> Unit)?) : RecyclerView.ViewHolder(itemView) {
private val row: View
private val text: TextView
private val icon: ImageView
init {
FilterAdapterActionBinding.bind(itemView).let {
row = it.row
text = it.text
icon = it.icon
}
}
fun bind(filter: FilterListItem) {
text.text = filter.listingTitle
icon.setImageDrawable(DrawableUtil.getWrapped(context, filter.icon))
if (onClick != null) {
row.setOnClickListener {
onClick.invoke(filter)
}
}
}
}

@ -1,31 +1,31 @@
package com.todoroo.astrid.adapter package com.todoroo.astrid.adapter
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.astrid.api.AstridOrderingFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.subtasks.SubtasksFilterUpdater import com.todoroo.astrid.subtasks.SubtasksFilterUpdater
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListMetadata import org.tasks.data.entity.TaskListMetadata
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.Collections
import kotlin.math.abs import kotlin.math.abs
@Deprecated("legacy astrid manual sorting") @Deprecated("legacy astrid manual sorting")
class AstridTaskAdapter internal constructor( class AstridTaskAdapter internal constructor(
private val list: TaskListMetadata, private val list: TaskListMetadata,
private val filter: Filter, private val filter: AstridOrderingFilter,
private val updater: SubtasksFilterUpdater, private val updater: SubtasksFilterUpdater,
googleTaskDao: GoogleTaskDao, googleTaskDao: GoogleTaskDao,
caldavDao: CaldavDao, caldavDao: CaldavDao,
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager, private val localBroadcastManager: LocalBroadcastManager,
taskMover: TaskMover, taskMover: TaskMover,
) : TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) { ) : TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) {
private val chainedCompletions = Collections.synchronizedMap(HashMap<String, ArrayList<String>>()) private val chainedCompletions = Collections.synchronizedMap(HashMap<String, ArrayList<String>>())
@ -66,11 +66,10 @@ class AstridTaskAdapter internal constructor(
override suspend fun onTaskDeleted(task: Task) = updater.onDeleteTask(list, filter, task.uuid) override suspend fun onTaskDeleted(task: Task) = updater.onDeleteTask(list, filter, task.uuid)
override suspend fun onCompletedTask(task: TaskContainer, newState: Boolean) { override suspend fun onCompletedTask(uuid: String, newState: Boolean) {
val itemId = task.uuid val completionDate = if (newState) currentTimeMillis() else 0
val completionDate = if (newState) DateUtilities.now() else 0
if (!newState) { if (!newState) {
val chained = chainedCompletions[itemId] val chained = chainedCompletions[uuid]
if (chained != null) { if (chained != null) {
for (taskId in chained) { for (taskId in chained) {
taskDao.setCompletionDate(taskId, completionDate) taskDao.setCompletionDate(taskId, completionDate)
@ -79,7 +78,7 @@ class AstridTaskAdapter internal constructor(
return return
} }
val chained = ArrayList<String>() val chained = ArrayList<String>()
updater.applyToDescendants(itemId) { node: SubtasksFilterUpdater.Node -> updater.applyToDescendants(uuid) { node: SubtasksFilterUpdater.Node ->
val uuid = node.uuid val uuid = node.uuid
taskDao.setCompletionDate(uuid, completionDate) taskDao.setCompletionDate(uuid, completionDate)
chained.add(node.uuid) chained.add(node.uuid)
@ -90,14 +89,14 @@ class AstridTaskAdapter internal constructor(
var madeChanges = false var madeChanges = false
for (t in tasks) { for (t in tasks) {
if (!isNullOrEmpty(t.recurrence)) { if (!isNullOrEmpty(t.recurrence)) {
updater.moveToParentOf(t.uuid, itemId) updater.moveToParentOf(t.uuid, uuid)
madeChanges = true madeChanges = true
} }
} }
if (madeChanges) { if (madeChanges) {
updater.writeSerialization(list, updater.serializeTree()) updater.writeSerialization(list, updater.serializeTree())
} }
chainedCompletions[itemId] = chained chainedCompletions[uuid] = chained
} }
} }

@ -3,15 +3,15 @@ package com.todoroo.astrid.adapter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
class CaldavManualSortTaskAdapter internal constructor( class CaldavManualSortTaskAdapter internal constructor(
googleTaskDao: GoogleTaskDao, googleTaskDao: GoogleTaskDao,
caldavDao: CaldavDao, caldavDao: CaldavDao,
taskDao: TaskDao, taskDao: TaskDao,
localBroadcastManager: LocalBroadcastManager, localBroadcastManager: LocalBroadcastManager,
taskMover: TaskMover, taskMover: TaskMover,
) : TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) { ) : TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) {
override suspend fun moved(from: Int, to: Int, indent: Int) { override suspend fun moved(from: Int, to: Int, indent: Int) {

@ -7,7 +7,11 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.todoroo.astrid.api.* import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.CustomFilter
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.api.TagFilter
import org.tasks.R import org.tasks.R
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.databinding.FilterAdapterRowBinding import org.tasks.databinding.FilterAdapterRowBinding
@ -16,7 +20,7 @@ import org.tasks.filters.PlaceFilter
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
import org.tasks.themes.CustomIcons.getIconResId import org.tasks.themes.CustomIcons.getIconResId
import org.tasks.themes.DrawableUtil import org.tasks.themes.DrawableUtil
import java.util.* import java.util.Locale
class FilterViewHolder internal constructor( class FilterViewHolder internal constructor(
itemView: View, itemView: View,
@ -25,7 +29,7 @@ class FilterViewHolder internal constructor(
private val context: Context, private val context: Context,
private val inventory: Inventory, private val inventory: Inventory,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val onClick: ((FilterListItem?) -> Unit)? private val onClick: (Filter) -> Unit,
) : RecyclerView.ViewHolder(itemView) { ) : RecyclerView.ViewHolder(itemView) {
private val row: View private val row: View
@ -34,7 +38,7 @@ class FilterViewHolder internal constructor(
private val size: TextView private val size: TextView
private val shareIndicator: ImageView private val shareIndicator: ImageView
lateinit var filter: FilterListItem lateinit var filter: Filter
init { init {
FilterAdapterRowBinding.bind(itemView).let { FilterAdapterRowBinding.bind(itemView).let {
@ -53,7 +57,7 @@ class FilterViewHolder internal constructor(
itemView.isSelected = moving itemView.isSelected = moving
} }
fun bind(filter: FilterListItem, selected: Boolean, count: Int?) { fun bind(filter: Filter, selected: Boolean, count: Int?) {
this.filter = filter this.filter = filter
if (navigationDrawer) { if (navigationDrawer) {
itemView.isSelected = selected itemView.isSelected = selected
@ -63,7 +67,7 @@ class FilterViewHolder internal constructor(
val icon = getIcon(filter) val icon = getIcon(filter)
this.icon.setImageDrawable(DrawableUtil.getWrapped(context, icon)) this.icon.setImageDrawable(DrawableUtil.getWrapped(context, icon))
this.icon.drawable.setTint(getColor(filter)) this.icon.drawable.setTint(getColor(filter))
text.text = filter.listingTitle text.text = filter.title
if (count == null || count == 0) { if (count == null || count == 0) {
size.visibility = View.INVISIBLE size.visibility = View.INVISIBLE
} else { } else {
@ -71,21 +75,20 @@ class FilterViewHolder internal constructor(
size.visibility = View.VISIBLE size.visibility = View.VISIBLE
} }
shareIndicator.apply { shareIndicator.apply {
isVisible = filter.principals > 0 isVisible = filter is CaldavFilter && filter.principals > 0
setImageResource(when { setImageResource(when {
filter !is CaldavFilter -> 0
filter.principals <= 0 -> 0 filter.principals <= 0 -> 0
filter.principals == 1 -> R.drawable.ic_outline_perm_identity_24px filter.principals == 1 -> R.drawable.ic_outline_perm_identity_24px
else -> R.drawable.ic_outline_people_outline_24 else -> R.drawable.ic_outline_people_outline_24
}) })
} }
if (onClick != null) { row.setOnClickListener {
row.setOnClickListener { onClick.invoke(filter)
onClick.invoke(filter)
}
} }
} }
private fun getColor(filter: FilterListItem): Int { private fun getColor(filter: Filter): Int {
if (filter.tint != 0) { if (filter.tint != 0) {
val color = colorProvider.getThemeColor(filter.tint, true) val color = colorProvider.getThemeColor(filter.tint, true)
if (color.isFree || inventory.purchasedThemes()) { if (color.isFree || inventory.purchasedThemes()) {
@ -95,7 +98,7 @@ class FilterViewHolder internal constructor(
return context.getColor(R.color.text_primary) return context.getColor(R.color.text_primary)
} }
private fun getIcon(filter: FilterListItem): Int { private fun getIcon(filter: Filter): Int {
if (filter.icon < 1000 || inventory.hasPro) { if (filter.icon < 1000 || inventory.hasPro) {
val icon = getIconResId(filter.icon) val icon = getIconResId(filter.icon)
if (icon != null) { if (icon != null) {

@ -3,15 +3,15 @@ package com.todoroo.astrid.adapter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
class GoogleTaskManualSortAdapter internal constructor( class GoogleTaskManualSortAdapter internal constructor(
googleTaskDao: GoogleTaskDao, googleTaskDao: GoogleTaskDao,
caldavDao: CaldavDao, caldavDao: CaldavDao,
taskDao: TaskDao, taskDao: TaskDao,
localBroadcastManager: LocalBroadcastManager, localBroadcastManager: LocalBroadcastManager,
taskMover: TaskMover, taskMover: TaskMover,
) : TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) { ) : TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) {
override suspend fun moved(from: Int, to: Int, indent: Int) { override suspend fun moved(from: Int, to: Int, indent: Int) {

@ -20,7 +20,9 @@ import org.tasks.activities.DragAndDropDiffer
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.filters.NavigationDrawerSubheader import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
import java.util.* import java.util.LinkedList
import java.util.Locale
import java.util.Queue
import java.util.concurrent.Executors import java.util.concurrent.Executors
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
@ -32,12 +34,11 @@ class NavigationDrawerAdapter @Inject constructor(
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val subheaderClickHandler: SubheaderClickHandler, private val subheaderClickHandler: SubheaderClickHandler,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>(), ) : RecyclerView.Adapter<RecyclerView.ViewHolder>(),
DragAndDropDiffer<FilterListItem, MutableList<FilterListItem>> { DragAndDropDiffer<FilterListItem, ArrayList<FilterListItem>> {
private lateinit var onClick: (FilterListItem?) -> Unit private lateinit var onClick: (FilterListItem?) -> Unit
private var selected: Filter? = null override val channel = Channel<ArrayList<FilterListItem>>(Channel.UNLIMITED)
override val channel = Channel<List<FilterListItem>>(Channel.UNLIMITED) override val updates: Queue<Pair<ArrayList<FilterListItem>, DiffUtil.DiffResult?>> = LinkedList()
override val updates: Queue<Pair<MutableList<FilterListItem>, DiffUtil.DiffResult?>> = LinkedList()
override val scope: CoroutineScope = override val scope: CoroutineScope =
CoroutineScope(Executors.newSingleThreadExecutor().asCoroutineDispatcher() + Job()) CoroutineScope(Executors.newSingleThreadExecutor().asCoroutineDispatcher() + Job())
override var items = initializeDiffer(ArrayList()) override var items = initializeDiffer(ArrayList())
@ -55,11 +56,6 @@ class NavigationDrawerAdapter @Inject constructor(
override fun getItemCount() = items.size override fun getItemCount() = items.size
fun setSelected(selected: Filter?) {
this.selected = selected
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val type = FilterListItem.Type.values()[viewType] val type = FilterListItem.Type.values()[viewType]
val view = LayoutInflater.from(parent.context).inflate(type.layout, parent, false) val view = LayoutInflater.from(parent.context).inflate(type.layout, parent, false)
@ -67,20 +63,17 @@ class NavigationDrawerAdapter @Inject constructor(
FilterListItem.Type.ITEM -> FilterViewHolder( FilterListItem.Type.ITEM -> FilterViewHolder(
view, true, locale, activity, inventory, colorProvider) { onClickFilter(it) } view, true, locale, activity, inventory, colorProvider) { onClickFilter(it) }
FilterListItem.Type.SUBHEADER -> SubheaderViewHolder(view, subheaderClickHandler) FilterListItem.Type.SUBHEADER -> SubheaderViewHolder(view, subheaderClickHandler)
FilterListItem.Type.ACTION -> ActionViewHolder(activity, view) { onClickFilter(it) }
else -> SeparatorViewHolder(view)
} }
} }
private fun onClickFilter(filter: FilterListItem?) = private fun onClickFilter(filter: FilterListItem?) =
onClick(if (filter == selected) null else filter) onClick(filter)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
when (item.itemType) { when (item.itemType) {
FilterListItem.Type.ITEM -> FilterListItem.Type.ITEM ->
(holder as FilterViewHolder).bind(item, item == selected, max(item.count, 0)) (holder as FilterViewHolder).bind(item as Filter, false, max(item.count, 0))
FilterListItem.Type.ACTION -> (holder as ActionViewHolder).bind(item)
FilterListItem.Type.SUBHEADER -> FilterListItem.Type.SUBHEADER ->
(holder as SubheaderViewHolder).bind((item as NavigationDrawerSubheader)) (holder as SubheaderViewHolder).bind((item as NavigationDrawerSubheader))
else -> {} else -> {}
@ -91,9 +84,7 @@ class NavigationDrawerAdapter @Inject constructor(
private fun getItem(position: Int) = items[position] private fun getItem(position: Int) = items[position]
override fun transform(list: List<FilterListItem>) = list.toMutableList() override fun diff(last: ArrayList<FilterListItem>, next: ArrayList<FilterListItem>) =
override fun diff(last: MutableList<FilterListItem>, next: MutableList<FilterListItem>) =
DiffUtil.calculateDiff(DiffCallback(last, next)) DiffUtil.calculateDiff(DiffCallback(last, next))
private class DiffCallback(val old: List<FilterListItem>, val new: List<FilterListItem>) : DiffUtil.Callback() { private class DiffCallback(val old: List<FilterListItem>, val new: List<FilterListItem>) : DiffUtil.Callback() {
@ -105,7 +96,7 @@ class NavigationDrawerAdapter @Inject constructor(
old[oldPosition].areItemsTheSame(new[newPosition]) old[oldPosition].areItemsTheSame(new[newPosition])
override fun areContentsTheSame(oldPosition: Int, newPosition: Int) = override fun areContentsTheSame(oldPosition: Int, newPosition: Int) =
old[oldPosition].areContentsTheSame(new[newPosition]) old[oldPosition] == new[newPosition]
} }
override fun onChanged(position: Int, count: Int, payload: Any?) = override fun onChanged(position: Int, count: Int, payload: Any?) =

@ -1,6 +0,0 @@
package com.todoroo.astrid.adapter
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class SeparatorViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView)

@ -6,13 +6,17 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.dialogs.NewFilterDialog import org.tasks.dialogs.NewFilterDialog
import org.tasks.filters.FilterProvider
import org.tasks.filters.NavigationDrawerSubheader import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType.* import org.tasks.filters.NavigationDrawerSubheader.SubheaderType.CALDAV
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType.ETESYNC
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType.GOOGLE_TASKS
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType.PREFERENCE
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType.TASKS
import org.tasks.preferences.MainPreferences import org.tasks.preferences.MainPreferences
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.ui.NavigationDrawerFragment
import javax.inject.Inject import javax.inject.Inject
class SubheaderClickHandler @Inject constructor( class SubheaderClickHandler @Inject constructor(
@ -30,6 +34,7 @@ class SubheaderClickHandler @Inject constructor(
CALDAV, CALDAV,
TASKS, TASKS,
ETESYNC -> caldavDao.setCollapsed(subheader.id, collapsed) ETESYNC -> caldavDao.setCollapsed(subheader.id, collapsed)
else -> throw IllegalArgumentException()
} }
localBroadcastManager.broadcastRefreshList() localBroadcastManager.broadcastRefreshList()
} }
@ -37,7 +42,7 @@ class SubheaderClickHandler @Inject constructor(
override fun onAdd(subheader: NavigationDrawerSubheader) { override fun onAdd(subheader: NavigationDrawerSubheader) {
when (subheader.addIntentRc) { when (subheader.addIntentRc) {
NavigationDrawerFragment.REQUEST_NEW_FILTER -> FilterProvider.REQUEST_NEW_FILTER ->
NewFilterDialog.newFilterDialog().show( NewFilterDialog.newFilterDialog().show(
(activity as AppCompatActivity).supportFragmentManager, (activity as AppCompatActivity).supportFragmentManager,
FRAG_TAG_NEW_FILTER FRAG_TAG_NEW_FILTER
@ -50,6 +55,6 @@ class SubheaderClickHandler @Inject constructor(
activity.startActivity(Intent(activity, MainPreferences::class.java)) activity.startActivity(Intent(activity, MainPreferences::class.java))
companion object { companion object {
private const val FRAG_TAG_NEW_FILTER = "frag_tag_new_filter" const val FRAG_TAG_NEW_FILTER = "frag_tag_new_filter"
} }
} }

@ -33,7 +33,7 @@ internal class SubheaderViewHolder(
fun bind(subheader: NavigationDrawerSubheader) { fun bind(subheader: NavigationDrawerSubheader) {
add.isVisible = subheader.addIntent != null add.isVisible = subheader.addIntent != null
this.subheader = subheader this.subheader = subheader
text.text = subheader.listingTitle text.text = subheader.title
when { when {
subheader.error || subheader.subheaderType == ETESYNC -> subheader.error || subheader.subheaderType == ETESYNC ->
with(errorIcon) { with(errorIcon) {

@ -11,30 +11,30 @@ import com.todoroo.astrid.core.SortHelper.SORT_LIST
import com.todoroo.astrid.core.SortHelper.SORT_MANUAL import com.todoroo.astrid.core.SortHelper.SORT_MANUAL
import com.todoroo.astrid.core.SortHelper.SORT_START import com.todoroo.astrid.core.SortHelper.SORT_START
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.CaldavTask import org.tasks.data.dao.CaldavDao.Companion.toAppleEpoch
import org.tasks.data.GoogleTaskDao import org.tasks.data.entity.CaldavTask
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.Task
import org.tasks.data.entity.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.date.DateTimeUtils.toAppleEpoch import org.tasks.data.createDueDate
import org.tasks.data.createHideUntil
import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.tasklist.SectionedDataSource.Companion.HEADER_COMPLETED
import org.tasks.time.DateTimeUtils.millisOfDay import org.tasks.time.DateTimeUtils.millisOfDay
open class TaskAdapter( open class TaskAdapter(
private val newTasksOnTop: Boolean, private val newTasksOnTop: Boolean,
private val googleTaskDao: GoogleTaskDao, private val googleTaskDao: GoogleTaskDao,
private val caldavDao: CaldavDao, private val caldavDao: CaldavDao,
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager, private val localBroadcastManager: LocalBroadcastManager,
private val taskMover: TaskMover, private val taskMover: TaskMover,
) { ) {
private val selected = HashSet<Long>() private val selected = HashSet<Long>()
private val collapsed = mutableSetOf(HEADER_COMPLETED)
private lateinit var dataSource: TaskAdapterDataSource private lateinit var dataSource: TaskAdapterDataSource
val count: Int val count: Int
@ -56,15 +56,6 @@ open class TaskAdapter(
fun clearSelections() = selected.clear() fun clearSelections() = selected.clear()
fun getCollapsed() = HashSet(collapsed)
fun setCollapsed(groups: LongArray?) {
clearCollapsed()
groups?.toList()?.let(collapsed::addAll)
}
fun clearCollapsed() = collapsed.retainAll(listOf(HEADER_COMPLETED))
open fun getIndent(task: TaskContainer): Int = task.indent open fun getIndent(task: TaskContainer): Int = task.indent
open fun canMove(source: TaskContainer, from: Int, target: TaskContainer, to: Int): Boolean { open fun canMove(source: TaskContainer, from: Int, target: TaskContainer, to: Int): Boolean {
@ -125,20 +116,12 @@ open class TaskAdapter(
} }
} }
fun toggleCollapsed(group: Long) {
if (collapsed.contains(group)) {
collapsed.remove(group)
} else {
collapsed.add(group)
}
}
open fun supportsAstridSorting(): Boolean = false open fun supportsAstridSorting(): Boolean = false
open suspend fun moved(from: Int, to: Int, indent: Int) { open suspend fun moved(from: Int, to: Int, indent: Int) {
val task = getTask(from) val task = getTask(from)
val newParent = findParent(indent, to) val newParent = findParent(indent, to)
if ((newParent?.id ?: 0) == task.parent) { if ((newParent?.id ?: 0) == task.parent || (indent > 0 && dataSource.subtaskSortMode == SORT_MANUAL)) {
if (indent == 0) { if (indent == 0) {
changeSortGroup(task, if (from < to) to - 1 else to) changeSortGroup(task, if (from < to) to - 1 else to)
} else if (dataSource.subtaskSortMode == SORT_MANUAL) { } else if (dataSource.subtaskSortMode == SORT_MANUAL) {
@ -170,7 +153,7 @@ open class TaskAdapter(
fun getItemUuid(position: Int): String = getTask(position).uuid fun getItemUuid(position: Int): String = getTask(position).uuid
open suspend fun onCompletedTask(task: TaskContainer, newState: Boolean) {} open suspend fun onCompletedTask(uuid: String, newState: Boolean) {}
open suspend fun onTaskCreated(uuid: String) {} open suspend fun onTaskCreated(uuid: String) {}
@ -192,7 +175,7 @@ open class TaskAdapter(
return false return false
} }
internal fun findParent(indent: Int, to: Int): TaskContainer? { private fun findParent(indent: Int, to: Int): TaskContainer? {
if (indent == 0 || to == 0) { if (indent == 0 || to == 0) {
return null return null
} }
@ -210,9 +193,7 @@ open class TaskAdapter(
SORT_IMPORTANCE -> { SORT_IMPORTANCE -> {
val newPriority = dataSource.nearestHeader(if (pos == 0) 1 else pos).toInt() val newPriority = dataSource.nearestHeader(if (pos == 0) 1 else pos).toInt()
if (newPriority != task.priority) { if (newPriority != task.priority) {
val t = task.task taskDao.save(task.task.copy(priority = newPriority))
t.priority = newPriority
taskDao.save(t)
} }
} }
SORT_LIST -> taskMover.move(task.id, dataSource.nearestHeader(if (pos == 0) 1 else pos)) SORT_LIST -> taskMover.move(task.id, dataSource.nearestHeader(if (pos == 0) 1 else pos))
@ -226,7 +207,7 @@ open class TaskAdapter(
task.setDueDateAdjustingHideUntil(when { task.setDueDateAdjustingHideUntil(when {
date == 0L -> 0L date == 0L -> 0L
task.hasDueTime() -> date.toDateTime().withMillisOfDay(original.millisOfDay()).millis task.hasDueTime() -> date.toDateTime().withMillisOfDay(original.millisOfDay()).millis
else -> Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, date) else -> createDueDate(Task.URGENCY_SPECIFIC_DAY, date)
}) })
if (original != task.dueDate) { if (original != task.dueDate) {
taskDao.save(task) taskDao.save(task)
@ -281,7 +262,7 @@ open class TaskAdapter(
private suspend fun changeCaldavParent(task: TaskContainer, newParent: TaskContainer?) { private suspend fun changeCaldavParent(task: TaskContainer, newParent: TaskContainer?) {
val list = newParent?.caldav ?: task.caldav!! val list = newParent?.caldav ?: task.caldav!!
val caldavTask = task.caldavTask ?: CaldavTask( val caldavTask = task.caldavTask.takeIf { list == task.caldav } ?: CaldavTask(
task = task.id, task = task.id,
calendar = list, calendar = list,
) )
@ -289,9 +270,8 @@ open class TaskAdapter(
if (newParentId == 0L) { if (newParentId == 0L) {
caldavTask.remoteParent = "" caldavTask.remoteParent = ""
} else { } else {
val parentTask = caldavDao.getTask(newParentId) ?: return
caldavTask.calendar = list caldavTask.calendar = list
caldavTask.remoteParent = parentTask.remoteId caldavTask.remoteParent = newParent?.caldavTask?.remoteId ?: return
} }
task.task.order = if (newTasksOnTop) { task.task.order = if (newTasksOnTop) {
caldavDao.findFirstTask(list, newParentId) caldavDao.findFirstTask(list, newParentId)
@ -303,12 +283,13 @@ open class TaskAdapter(
?.plus(1) ?.plus(1)
} }
if (caldavTask.id == 0L) { if (caldavTask.id == 0L) {
val newTask = CaldavTask( caldavDao.insert(
task = task.id, CaldavTask(
calendar = list, task = task.id,
calendar = list,
remoteParent = caldavTask.remoteParent,
)
) )
newTask.remoteParent = caldavTask.remoteParent
caldavDao.insert(newTask)
} else { } else {
caldavDao.update(caldavTask) caldavDao.update(caldavTask)
} }
@ -419,7 +400,12 @@ open class TaskAdapter(
indent == previous.indent -> previous.caldavSortOrder + 1 indent == previous.indent -> previous.caldavSortOrder + 1
else -> getTask((to - 1 downTo 0).find { getTask(it).indent == indent }!!).caldavSortOrder + 1 else -> getTask((to - 1 downTo 0).find { getTask(it).indent == indent }!!).caldavSortOrder + 1
} }
caldavDao.move(task, oldParent, newParent, newPosition) caldavDao.move(
task = task,
previousParent = oldParent,
newParent = newParent,
newPosition = newPosition,
)
taskDao.touch(task.id) taskDao.touch(task.id)
localBroadcastManager.broadcastRefresh() localBroadcastManager.broadcastRefresh()
} }
@ -436,13 +422,24 @@ open class TaskAdapter(
val caldavTask = task.caldavTask ?: return val caldavTask = task.caldavTask ?: return
if (newParent == 0L) { if (newParent == 0L) {
caldavTask.remoteParent = "" caldavTask.remoteParent = ""
task.parent = 0 caldavDao.update(caldavTask.id, caldavTask.remoteParent)
} else { } else {
val parentTask = caldavDao.getTask(newParent) ?: return val parentTask = caldavDao.getTask(newParent) ?: return
caldavTask.remoteParent = parentTask.remoteId if (parentTask.calendar == caldavTask.calendar) {
task.parent = newParent caldavTask.remoteParent = parentTask.remoteId
caldavDao.update(caldavTask.id, caldavTask.remoteParent)
} else {
caldavDao.markDeleted(listOf(task.id))
caldavDao.insert(
CaldavTask(
task = task.id,
calendar = parentTask.calendar,
remoteParent = parentTask.remoteId,
)
)
}
} }
caldavDao.update(caldavTask.id, caldavTask.remoteParent) task.parent = newParent
taskDao.save(task.task, null) taskDao.save(task.task, null)
} }
} }

@ -1,13 +1,14 @@
package com.todoroo.astrid.adapter package com.todoroo.astrid.adapter
import android.content.Context import android.content.Context
import com.todoroo.astrid.api.AstridOrderingFilter
import com.todoroo.astrid.api.CaldavFilter import com.todoroo.astrid.api.CaldavFilter
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.api.TagFilter import com.todoroo.astrid.api.TagFilter
import com.todoroo.astrid.core.BuiltInFilterExposer import com.todoroo.astrid.core.BuiltInFilterExposer
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task.Companion.isUuidEmpty import org.tasks.data.entity.Task.Companion.isUuidEmpty
import com.todoroo.astrid.service.TaskMover import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.subtasks.SubtasksFilterUpdater import com.todoroo.astrid.subtasks.SubtasksFilterUpdater
import com.todoroo.astrid.subtasks.SubtasksHelper import com.todoroo.astrid.subtasks.SubtasksHelper
@ -15,25 +16,25 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.GoogleTaskDao import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.TaskListMetadata import org.tasks.data.entity.TaskListMetadata
import org.tasks.data.TaskListMetadataDao import org.tasks.data.dao.TaskListMetadataDao
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
class TaskAdapterProvider @Inject constructor( class TaskAdapterProvider @Inject constructor(
@param:ApplicationContext private val context: Context, @param:ApplicationContext private val context: Context,
private val preferences: Preferences, private val preferences: Preferences,
private val taskListMetadataDao: TaskListMetadataDao, private val taskListMetadataDao: TaskListMetadataDao,
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val googleTaskDao: GoogleTaskDao, private val googleTaskDao: GoogleTaskDao,
private val caldavDao: CaldavDao, private val caldavDao: CaldavDao,
private val localBroadcastManager: LocalBroadcastManager, private val localBroadcastManager: LocalBroadcastManager,
private val taskMover: TaskMover, private val taskMover: TaskMover,
) { ) {
fun createTaskAdapter(filter: Filter): TaskAdapter { fun createTaskAdapter(filter: Filter): TaskAdapter {
if (filter.supportsAstridSorting() && preferences.isAstridSort) { if (filter is AstridOrderingFilter && preferences.isAstridSort) {
when (filter) { when (filter) {
is TagFilter -> return createManualTagTaskAdapter(filter) is TagFilter -> return createManualTagTaskAdapter(filter)
else -> { else -> {
@ -67,7 +68,7 @@ class TaskAdapterProvider @Inject constructor(
AstridTaskAdapter(list!!, filter, updater, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) AstridTaskAdapter(list!!, filter, updater, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover)
} }
private fun createManualFilterTaskAdapter(filter: Filter): TaskAdapter? = runBlocking { private fun createManualFilterTaskAdapter(filter: AstridOrderingFilter): TaskAdapter? = runBlocking {
var filterId: String? = null var filterId: String? = null
var prefId: String? = null var prefId: String? = null
if (BuiltInFilterExposer.isInbox(context, filter)) { if (BuiltInFilterExposer.isInbox(context, filter)) {

@ -1,9 +1,8 @@
package com.todoroo.astrid.alarms package com.todoroo.astrid.alarms
import com.todoroo.andlib.utility.DateUtilities import org.tasks.data.entity.Alarm
import com.todoroo.astrid.data.Task import org.tasks.data.entity.Notification
import org.tasks.data.Alarm import org.tasks.data.entity.Task
import org.tasks.jobs.AlarmEntry
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.reminders.Random import org.tasks.reminders.Random
import org.tasks.time.DateTimeUtils.withMillisOfDay import org.tasks.time.DateTimeUtils.withMillisOfDay
@ -19,7 +18,7 @@ class AlarmCalculator(
preferences: Preferences preferences: Preferences
) : this(preferences.isDefaultDueTimeEnabled, Random(), preferences.defaultDueTime) ) : this(preferences.isDefaultDueTimeEnabled, Random(), preferences.defaultDueTime)
fun toAlarmEntry(task: Task, alarm: Alarm): AlarmEntry? { fun toAlarmEntry(task: Task, alarm: Alarm): Notification? {
val trigger = when (alarm.type) { val trigger = when (alarm.type) {
Alarm.TYPE_SNOOZE, Alarm.TYPE_SNOOZE,
Alarm.TYPE_DATE_TIME -> Alarm.TYPE_DATE_TIME ->
@ -51,12 +50,12 @@ class AlarmCalculator(
trigger <= AlarmService.NO_ALARM -> trigger <= AlarmService.NO_ALARM ->
null null
trigger > task.reminderLast || alarm.type == Alarm.TYPE_SNOOZE -> trigger > task.reminderLast || alarm.type == Alarm.TYPE_SNOOZE ->
AlarmEntry(alarm.id, alarm.task, trigger, alarm.type) Notification(taskId = alarm.task, timestamp = trigger, type = alarm.type)
alarm.repeat > 0 -> { alarm.repeat > 0 -> {
val past = (task.reminderLast - trigger) / alarm.interval val past = (task.reminderLast - trigger) / alarm.interval
val next = trigger + (past + 1) * alarm.interval val next = trigger + (past + 1) * alarm.interval
if (past < alarm.repeat && next > task.reminderLast) { if (past < alarm.repeat && next > task.reminderLast) {
AlarmEntry(alarm.id, alarm.task, next, alarm.type) Notification(taskId = alarm.task, timestamp = next, type = alarm.type)
} else { } else {
null null
} }
@ -81,11 +80,7 @@ class AlarmCalculator(
`when` = task.creationDate `when` = task.creationDate
} }
`when` += (reminderPeriod * (0.85f + 0.3f * random.nextFloat())).toLong() `when` += (reminderPeriod * (0.85f + 0.3f * random.nextFloat())).toLong()
if (`when` < DateUtilities.now()) { return Math.max(`when`, task.hideUntil)
`when` =
DateUtilities.now() + ((0.5f + 6 * random.nextFloat()) * DateUtilities.ONE_HOUR).toLong()
}
return `when`
} }
return AlarmService.NO_ALARM return AlarmService.NO_ALARM
} }

@ -5,14 +5,19 @@
*/ */
package com.todoroo.astrid.alarms package com.todoroo.astrid.alarms
import com.todoroo.astrid.data.Task
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.data.Alarm import org.tasks.data.dao.AlarmDao
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.dao.TaskDao
import org.tasks.data.AlarmDao import org.tasks.data.db.DbUtils
import org.tasks.data.TaskDao import org.tasks.data.entity.Alarm
import org.tasks.jobs.NotificationQueue import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.Notification
import org.tasks.jobs.WorkManager
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -22,11 +27,12 @@ import javax.inject.Inject
*/ */
class AlarmService @Inject constructor( class AlarmService @Inject constructor(
private val alarmDao: AlarmDao, private val alarmDao: AlarmDao,
private val jobs: NotificationQueue,
private val taskDao: TaskDao, private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager, private val localBroadcastManager: LocalBroadcastManager,
private val notificationManager: NotificationManager, private val notificationManager: NotificationManager,
private val workManager: WorkManager,
private val alarmCalculator: AlarmCalculator, private val alarmCalculator: AlarmCalculator,
private val preferences: Preferences,
) { ) {
suspend fun getAlarms(taskId: Long): List<Alarm> = alarmDao.getAlarms(taskId) suspend fun getAlarms(taskId: Long): List<Alarm> = alarmDao.getAlarms(taskId)
@ -36,70 +42,80 @@ class AlarmService @Inject constructor(
* @return true if data was changed * @return true if data was changed
*/ */
suspend fun synchronizeAlarms(taskId: Long, alarms: MutableSet<Alarm>): Boolean { suspend fun synchronizeAlarms(taskId: Long, alarms: MutableSet<Alarm>): Boolean {
val task = taskDao.fetch(taskId) ?: return false
var changed = false var changed = false
for (existing in alarmDao.getAlarms(taskId)) { for (existing in alarmDao.getAlarms(taskId)) {
if (!alarms.removeIf { if (!alarms.removeIf { it.same(existing)}) {
it.type == existing.type &&
it.time == existing.time &&
it.repeat == existing.repeat &&
it.interval == existing.interval
}) {
alarmDao.delete(existing) alarmDao.delete(existing)
changed = true changed = true
} }
} }
for (alarm in alarms) { alarmDao.insert(alarms.map { it.copy(task = taskId) })
alarm.task = taskId if (alarms.isNotEmpty()) {
alarmDao.insert(alarm)
changed = true changed = true
} }
if (changed) { if (changed) {
scheduleAlarms(task)
localBroadcastManager.broadcastRefreshList() localBroadcastManager.broadcastRefreshList()
} }
return changed return changed
} }
suspend fun scheduleAllAlarms() {
alarmDao
.getActiveAlarms()
.groupBy { it.task }
.forEach { (taskId, alarms) ->
val task = taskDao.fetch(taskId) ?: return@forEach
scheduleAlarms(task, alarms)
}
}
fun cancelAlarms(taskId: Long) {
jobs.cancelForTask(taskId)
}
suspend fun snooze(time: Long, taskIds: List<Long>) { suspend fun snooze(time: Long, taskIds: List<Long>) {
notificationManager.cancel(taskIds) notificationManager.cancel(taskIds)
alarmDao.getSnoozed(taskIds).let { alarmDao.delete(it) } alarmDao.deleteSnoozed(taskIds)
taskIds.map { Alarm(it, time, TYPE_SNOOZE) }.let { alarmDao.insert(it) } alarmDao.insert(taskIds.map { Alarm(task = it, time = time, type = TYPE_SNOOZE) })
taskDao.touch(taskIds) taskDao.touch(taskIds)
scheduleAlarms(taskIds) workManager.triggerNotifications()
} }
suspend fun scheduleAlarms(taskIds: List<Long>) { suspend fun triggerAlarms(
taskDao.fetch(taskIds).forEach { scheduleAlarms(it) } trigger: suspend (List<Notification>) -> Unit
} ): Long {
if (preferences.isCurrentlyQuietHours) {
/** Schedules alarms for a single task */ return preferences.adjustForQuietHours(currentTimeMillis())
suspend fun scheduleAlarms(task: Task) { }
scheduleAlarms(task, alarmDao.getActiveAlarms(task.id)) val (overdue, _) = getAlarms()
overdue
.sortedBy { it.timestamp }
.also { alarms ->
alarms
.map { it.taskId }
.chunked(DbUtils.MAX_SQLITE_ARGS)
.onEach { alarmDao.deleteSnoozed(it) }
}
.map { it.copy(timestamp = currentTimeMillis()) }
.let { trigger(it) }
val alreadyTriggered = overdue.map { it.taskId }.toSet()
val (moreOverdue, future) = getAlarms()
return moreOverdue
.filterNot { it.type == Alarm.TYPE_RANDOM || alreadyTriggered.contains(it.taskId) }
.plus(future)
.minOfOrNull { it.timestamp }
?: 0
} }
private fun scheduleAlarms(task: Task, alarms: List<Alarm>) { internal suspend fun getAlarms(): Pair<List<Notification>, List<Notification>> {
jobs.cancelForTask(task.id) val start = currentTimeMillis()
val alarmEntries = alarms.mapNotNull { val overdue = ArrayList<Notification>()
alarmCalculator.toAlarmEntry(task, it) val future = ArrayList<Notification>()
} alarmDao.getActiveAlarms()
val next = .groupBy { it.task }
alarmEntries.find { it.type == TYPE_SNOOZE } ?: alarmEntries.minByOrNull { it.time } .forEach { (taskId, alarms) ->
next?.let { jobs.add(it) } val task = taskDao.fetch(taskId) ?: return@forEach
val alarmEntries = alarms.mapNotNull {
alarmCalculator.toAlarmEntry(task, it)
}
val (now, later) = alarmEntries.partition { it.timestamp <= DateTime().startOfMinute().plusMinutes(1).millis }
later
.filter { it.type == TYPE_SNOOZE }
.maxByOrNull { it.timestamp }
?.let { future.add(it) }
?: run {
now.firstOrNull()?.let { overdue.add(it) }
later.minByOrNull { it.timestamp }?.let { future.add(it) }
}
}
Timber.d("took ${currentTimeMillis() - start}ms overdue=${overdue.size} future=${future.size}")
return overdue to future
} }
companion object { companion object {

@ -3,7 +3,7 @@ package com.todoroo.astrid.api
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
class BooleanCriterion constructor() : CustomFilterCriterion(), Parcelable { class BooleanCriterion() : CustomFilterCriterion(), Parcelable {
constructor(identifier: String, title: String, sql: String): this() { constructor(identifier: String, title: String, sql: String): this() {
this.identifier = identifier this.identifier = identifier

@ -1,122 +0,0 @@
package com.todoroo.astrid.api;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Join;
import com.todoroo.andlib.sql.QueryTemplate;
import com.todoroo.astrid.data.Task;
import org.tasks.R;
import org.tasks.data.CaldavCalendar;
import org.tasks.data.CaldavTask;
import org.tasks.data.TaskDao;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class CaldavFilter extends Filter {
/** Parcelable Creator Object */
public static final Parcelable.Creator<CaldavFilter> CREATOR =
new Parcelable.Creator<>() {
/** {@inheritDoc} */
@Override
public CaldavFilter createFromParcel(Parcel source) {
CaldavFilter item = new CaldavFilter();
item.readFromParcel(source);
return item;
}
/** {@inheritDoc} */
@Override
public CaldavFilter[] newArray(int size) {
return new CaldavFilter[size];
}
};
private CaldavCalendar calendar;
private CaldavFilter() {
super();
}
public CaldavFilter(CaldavCalendar calendar) {
super(calendar.getName(), queryTemplate(calendar), getValuesForNewTask(calendar));
this.calendar = calendar;
id = calendar.getId();
tint = calendar.getColor();
icon = calendar.getIcon();
order = calendar.getOrder();
}
private static QueryTemplate queryTemplate(CaldavCalendar caldavCalendar) {
return new QueryTemplate()
.join(Join.left(CaldavTask.TABLE, Task.ID.eq(CaldavTask.TASK)))
.where(getCriterion(caldavCalendar));
}
private static Criterion getCriterion(CaldavCalendar caldavCalendar) {
return Criterion.and(
TaskDao.TaskCriteria.activeAndVisible(),
CaldavTask.DELETED.eq(0),
CaldavTask.CALENDAR.eq(caldavCalendar.getUuid()));
}
private static Map<String, Object> getValuesForNewTask(CaldavCalendar caldavCalendar) {
Map<String, Object> result = new HashMap<>();
result.put(CaldavTask.KEY, caldavCalendar.getUuid());
return result;
}
public String getUuid() {
return calendar.getUuid();
}
public String getAccount() {
return calendar.getAccount();
}
public CaldavCalendar getCalendar() {
return calendar;
}
@Override
public boolean isReadOnly() {
return calendar.getAccess() == CaldavCalendar.ACCESS_READ_ONLY;
}
/** {@inheritDoc} */
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(calendar, 0);
}
@Override
protected void readFromParcel(Parcel source) {
super.readFromParcel(source);
calendar = source.readParcelable(getClass().getClassLoader());
}
@Override
public boolean supportsManualSort() {
return true;
}
@Override
public int getMenu() {
return R.menu.menu_caldav_list_fragment;
}
@Override
public boolean areContentsTheSame(@NonNull FilterListItem other) {
return super.areContentsTheSame(other)
&& Objects.equals(calendar, ((CaldavFilter) other).calendar);
}
}

@ -0,0 +1,57 @@
package com.todoroo.astrid.api
import org.tasks.data.sql.Criterion.Companion.and
import org.tasks.data.sql.Join.Companion.left
import org.tasks.data.sql.QueryTemplate
import com.todoroo.andlib.utility.AndroidUtilities
import kotlinx.parcelize.Parcelize
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavTask
import org.tasks.data.NO_COUNT
import org.tasks.data.entity.Task
import org.tasks.data.dao.TaskDao.TaskCriteria.activeAndVisible
@Parcelize
data class CaldavFilter(
val calendar: CaldavCalendar,
val principals: Int = 0,
override val count: Int = NO_COUNT,
) : Filter {
override val title: String?
get() = calendar.name
override val sql: String
get() = QueryTemplate()
.join(left(CaldavTask.TABLE, Task.ID.eq(CaldavTask.TASK)))
.where(
and(
activeAndVisible(),
CaldavTask.DELETED.eq(0),
CaldavTask.CALENDAR.eq(calendar.uuid)
)
)
.toString()
override val valuesForNewTasks: String
get() = AndroidUtilities.mapToSerializedString(mapOf(CaldavTask.KEY to calendar.uuid!!))
override val order: Int
get() = calendar.order
override val tint: Int
get() = calendar.color
override val icon: Int
get() = calendar.getIcon()!!
val uuid: String
get() = calendar.uuid!!
val account: String
get() = calendar.account!!
override val isReadOnly: Boolean
get() = calendar.access == CaldavCalendar.ACCESS_READ_ONLY
override fun supportsManualSort() = true
override fun areItemsTheSame(other: FilterListItem): Boolean {
return other is CaldavFilter && calendar.id == other.calendar.id
}
}

@ -1,73 +0,0 @@
package com.todoroo.astrid.api;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.tasks.R;
import java.util.Objects;
public class CustomFilter extends Filter {
/** Parcelable Creator Object */
public static final Parcelable.Creator<CustomFilter> CREATOR =
new Parcelable.Creator<>() {
/** {@inheritDoc} */
@Override
public CustomFilter createFromParcel(Parcel source) {
return new CustomFilter(source);
}
/** {@inheritDoc} */
@Override
public CustomFilter[] newArray(int size) {
return new CustomFilter[size];
}
};
private String criterion;
public CustomFilter(@NonNull org.tasks.data.Filter filter) {
super(filter.getTitle(), filter.getSql(), filter.getValuesAsMap());
id = filter.getId();
criterion = filter.getCriterion();
tint = filter.getColor();
icon = filter.getIcon();
order = filter.getOrder();
}
private CustomFilter(Parcel parcel) {
readFromParcel(parcel);
}
public String getCriterion() {
return criterion;
}
/** {@inheritDoc} */
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeString(criterion);
}
@Override
protected void readFromParcel(Parcel source) {
super.readFromParcel(source);
criterion = source.readString();
}
@Override
public int getMenu() {
return getId() > 0 ? R.menu.menu_custom_filter : 0;
}
@Override
public boolean areContentsTheSame(@NonNull FilterListItem other) {
return super.areContentsTheSame(other)
&& Objects.equals(criterion, ((CustomFilter) other).criterion);
}
}

@ -0,0 +1,34 @@
package com.todoroo.astrid.api
import kotlinx.parcelize.Parcelize
import org.tasks.themes.CustomIcons
@Parcelize
data class CustomFilter(
val filter: org.tasks.data.entity.Filter,
) : Filter {
override val title: String?
get() = filter.title
override val sql: String
get() = filter.sql!!
override val valuesForNewTasks: String?
get() = filter.values
val criterion: String?
get() = filter.criterion
override val order: Int
get() = filter.order
val id: Long
get() = filter.id
override val icon: Int
get() = filter.icon ?: CustomIcons.FILTER
override val tint: Int
get() = filter.color ?: 0
override fun areItemsTheSame(other: FilterListItem): Boolean {
return other is CustomFilter && id == other.id
}
}

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

Loading…
Cancel
Save