Compare commits

...

578 Commits
13.4 ... main

Author SHA1 Message Date
Alex Baker a4cd0829b0 AGP 8.4.2 15 hours ago
Alex Baker 9a693177db Drawer updates
* Fix content overlapping navigation bars
* Move manage drawer to settings page
* Move drawer actions next to new settings button
1 day ago
Alex Baker 58955bd0a1 Fix reminder dialog button colors 1 day ago
renovate[bot] 95ecac8e7c Update dependency gradle to v8.8 1 day ago
renovate[bot] 57395423c6 Update dependency com.google.gms:google-services to v4.4.2 1 day ago
renovate[bot] b1613e9845 Update dependency com.google.firebase:firebase-bom to v33.1.0 1 day ago
renovate[bot] dcd70c7bc2 Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.6.8 1 day ago
Alex Baker c1e2eb7cd0 Update version and changelog 2 days ago
Alex Baker 145ea03714 Set chip icon color 2 days ago
Alex Baker bada09f5c2 Use new search bar in filter picker 2 days ago
Alex Baker 007c536312 Add search bar to drawer 2 days ago
renovate[bot] 28de989a05 Update dependency ruby to v3.3.3 2 days ago
Alex Baker 97c3852f2f Add searchable filter picker 3 days ago
hugoalh f739cac8b4 Translated using Weblate (Chinese (Traditional))
Currently translated at 99.8% (664 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hant/
3 days ago
min7-i cd6a474cce Translated using Weblate (German)
Currently translated at 98.6% (656 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
3 days ago
Jose Delvani 4ffc11903e Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (665 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
3 days ago
Jose Delvani 70d6cc63ca Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (665 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
3 days ago
islam2hamy 6e97e602c9 Translated using Weblate (Arabic)
Currently translated at 94.7% (630 of 665 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ar/
3 days ago
Alex Baker 3251becf9b Fix filter settings activity sqlite crash 4 days ago
Alex Baker d9293c7262 Move some filters to kmp 4 days ago
Alex Baker 929a01cd8c Add jvm target to kmp 4 days ago
Alex Baker 0d5803b9ca Move filter value serialization to kmp 4 days ago
Alex Baker bbaaf27386 Move filters to multiplatform library 5 days ago
Alex Baker 4c1121869d Migrate to compose material3 6 days ago
Alex Baker b918e87e05 Prevent firing notifications one minute early 1 week ago
Alex Baker df8f637239 Fix deleting new subtasks from edit screen 1 week ago
Alex Baker 9fad43c6c9 Refresh when returning to foreground 1 week ago
Alex Baker eb95cd24d7 Disable AVD cache
Its causing actions to hang
1 week ago
Alex Baker 30cb374a21 Revert "chore(deps): update actions/cache action to v4"
This reverts commit f8bb045d76.
2 weeks ago
Alex Baker b09a8967e4 Merge tag '13.9.9' 2 weeks ago
Alex Baker 39b56296bd Replace lifecycle-*-ktx deps 2 weeks ago
Alex Baker 1d8d2efce6 Update version and changelog 2 weeks ago
Alex Baker c9af39b6ba Fix import backup crashes 2 weeks ago
renovate[bot] f8bb045d76 chore(deps): update actions/cache action to v4 2 weeks ago
renovate[bot] 4040a2379b fix(deps): update dependency com.google.android.gms:play-services-oss-licenses to v17.1.0 2 weeks ago
Alex Baker c4ee7479ca
Cache avd 2 weeks ago
Alex Baker be861597ef Merge branch '13.9.8' 2 weeks ago
Alex Baker f27332595d Set root project name for project accessors 2 weeks ago
Alex Baker ea7f051d85 Revert "Restore @Transaction annotations"
This reverts commit b35090cd43.
2 weeks ago
Alex Baker 8be7fab033 Update version and changelog 2 weeks ago
Alex Baker d6e0c0bdcf Fix showing completed tasks in subtask filter 2 weeks ago
Alex Baker 5ec02011f8 Fix backup import crashes 2 weeks ago
Alex Baker b35090cd43 Restore @Transaction annotations 2 weeks ago
renovate[bot] 92f62450ae fix(deps): update accompanist to v0.34.0 2 weeks ago
renovate[bot] 3ca6912492 fix(deps): update dependency androidx.appcompat:appcompat to v1.7.0 2 weeks ago
renovate[bot] 080b1428dd fix(deps): update room to v2.7.0-alpha03 2 weeks ago
Alex Baker f67c3bc56c Enable typesafe project accessors 2 weeks ago
renovate[bot] 5d0e88a620 fix(deps): update dependency com.google.android.gms:play-services-location to v21.3.0 2 weeks ago
renovate[bot] c5d5795fe2 fix(deps): update lifecycle to v2.8.1 2 weeks ago
renovate[bot] 3bbc0e0ab0 fix(deps): update dependency androidx.sqlite:sqlite-bundled to v2.5.0-alpha03 2 weeks ago
Alex Baker 009a195580 Update plugin definitions 2 weeks ago
renovate[bot] 772f69d8c0 fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20240521-2.0.0 3 weeks ago
renovate[bot] 4229bf7067 fix(deps): update dependency com.google.apis:google-api-services-tasks to v1-rev20240526-2.0.0 3 weeks ago
Alex Baker 212a4b0a3d Delete transaction check
This was using platform sqlite
3 weeks ago
Alex Baker 4ddfe937b0 Finish converting data module to kmp 3 weeks ago
Alex Baker 19de0e08a5 Migrate to bundled sqlite 3 weeks ago
Alex Baker 60211355e0 Remove Geofence constructors 3 weeks ago
Alex Baker 17d218aa4e Add CommonParcelize 3 weeks ago
Alex Baker 505c8c29d5 Make sure dao methods are suspending 3 weeks ago
Alex Baker 7149308c97 Move Android platform stuff out of data 3 weeks ago
Alex Baker 2c5a497007 Fix backup import crash 3 weeks ago
Alex Baker 09f53fe1e5 Ignore multiplatform agp warnings 3 weeks ago
Alex Baker 5da4183aed Move ksp to gradle catalog 3 weeks ago
Alex Baker d35912e503 Kotlin 2.0 3 weeks ago
renovate[bot] 82fd99f83e fix(deps): update lifecycle to v2.8.0 3 weeks ago
renovate[bot] f944becea1 fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20240509-2.0.0 3 weeks ago
Alex Baker acd713dc5b AGP 8.4.1 3 weeks ago
Alex Baker 1a93c87ad9 Update version and changelog 3 weeks ago
Alex Baker c4e25b8b15 Fix tests 3 weeks ago
Alex Baker e11c0d2528 Add default reminders when adding due/start date 3 weeks ago
Alex Baker 2fc6833854 Don't crash on missing vtodo value 3 weeks ago
Alex Baker 4a2fb13d10 Converting data module to kmp - WIP 4 weeks ago
Alex Baker a2572e2dee Remove CaldavCalendarMaker 4 weeks ago
Alex Baker 64e05c9f8f Convert Tag to data class 4 weeks ago
Alex Baker ad833b5f49 Convert TagData to data class 4 weeks ago
Alex Baker eea944cc7b Update version and changelog 4 weeks ago
Alex Baker c82dfc7d39 Fix test? 4 weeks ago
Alex Baker 8607f9556a Fix test 4 weeks ago
Alex Baker f338e84d46 Fix warnings in Migrations 4 weeks ago
Alex Baker 9ee739627e Remove AlarmEntry 4 weeks ago
Alex Baker a49c233584 Make notification immutable 4 weeks ago
Alex Baker 74fca07c1b Make alarm immutable 4 weeks ago
Alex Baker 5bd0cef42e Remove extra alarm constructor 4 weeks ago
Alex Baker 4c245edbb4 Fix snooze causing duplicate notifications 4 weeks ago
Alex Baker 97a3f074d0 Update alarms after completion transaction 4 weeks ago
Alex Baker 86ecd3cf81 Synchronize alarms before saving 4 weeks ago
Alex Baker 07a2eda5ea Cancel notifications in TaskCompleter 4 weeks ago
renovate[bot] 09ffbdd036 fix(deps): update dependency com.google.firebase:firebase-crashlytics-gradle to v3 1 month ago
renovate[bot] 60f22146ca fix(deps): update dependency androidx.fragment:fragment-ktx to v1.7.1 1 month ago
renovate[bot] c11225abaf fix(deps): update kotlin 1 month 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>
1 month ago
Alex Baker 0ba901be69 Remove livedata from data module 1 month ago
Alex Baker ebe5e5c009 Replace gson with kotlin serialization 1 month ago
Alex Baker d556863fda Use kotlin serialization for backups 1 month ago
Alex Baker 55adbc2025 Reorganized data module 1 month ago
Alex Baker 06c4255886 Remove androidx.core from data module 1 month ago
renovate[bot] 4734a99bae fix(deps): update mockito monorepo to v5.12.0 1 month ago
Alex Baker a6a8cac8e4 Update dependencies 1 month ago
Alex Baker c3fc9a57cc Replace now with currentTimeMillis 1 month ago
Alex Baker 6e14d07d0c Move Room to data module 1 month ago
Alex Baker 6118121698 Moving some code out of TimerPlugin 1 month ago
Alex Baker 6bf3bd4d08 Update version and changelog 1 month ago
Alex Baker 065be79355 Update notification work logic 1 month ago
Alex Baker f8f8ba3c51 Don't adjust random reminder time 1 month 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 2 months ago
renovate[bot] da146723e5 chore(deps): update dependency ruby to v3.3.1 2 months 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
2 months ago
Alex Baker c534632c52 Pass uuid to TaskAdapter.onCompletedTask 2 months ago
Alex Baker c1347a7455 Update version and changelog 2 months ago
renovate[bot] 9544909a58 Update dependency androidx.activity:activity-compose to v1.9.0 2 months ago
Yurt Page 5c10dce2b9 fastlane: i18n ru
Signed-off-by: Yurt Page <yurtpage@gmail.com>
2 months ago
Alex Baker 584d4a5cbb Move after update work inside transaction 2 months ago
Alex Baker 7c68a7fa59 AGP 8.4.0 2 months ago
purushottamyadavbattula 215cc838ef Sending local broadcast refresh event for refreshing nav drawer menu to communicate about update events 2 months ago
Alex Baker d60472d1bc Remove RefreshScheduler 2 months ago
Alex Baker f84a37a60a Revert "Replace refresh work with coroutines"
Widgets 😢
2 months ago
Alex Baker 7fb85b6da1 Replace refresh work with coroutines 2 months ago
Alex Baker dc90e583e4 Fix hiding empty items in drawer 2 months 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/
3 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/
3 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/
3 months ago
Alex Baker 26ab3d5866 Exclude past snooze times from Snooze Filter
This should exclude tasks that were completed before their snooze time
lapsed
3 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/
3 months ago
Alex Baker 72bfda9224 Fix subtasks row for new tasks 3 months ago
Alex Baker 1067de4183 Emit SectionedDataSource from TaskListViewModel 3 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 4 months ago
renovate[bot] 2d7145cde3 Update plugin com.google.devtools.ksp to v1.9.22-1.0.18 4 months ago
renovate[bot] f2ab8bed95 Update dependency com.google.firebase:firebase-bom to v32.7.3 4 months ago
renovate[bot] a5bc4cf536 Update dependency com.android.tools.build:gradle to v8.3.0 4 months ago
renovate[bot] 1b35372b3a Update dependency com.google.apis:google-api-services-tasks to v1-rev20240225-2.0.0 4 months ago
Alex Baker c0fd4bf66a Convert LocalBroadcastManager to Kotlin 4 months ago
renovate[bot] 5d366f0d61 Update dependency io.coil-kt:coil-gif to v2.6.0 4 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/
5 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/
5 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/
5 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/
5 months ago
renovate[bot] 64af955ea7 Update flipper to v0.246.0 5 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/
5 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/
5 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/
5 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/
5 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/
5 months ago
Alex Baker e9afacb595 Include hidden subtasks when clearing completed 5 months ago
Alex Baker cf182aceab Display number of tasks to be cleared 5 months ago
Alex Baker db889d233a Remove AfterSaveWork 5 months ago
Alex Baker 457b89c092 Remove cleanup work 5 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/
5 months ago
renovate[bot] 2c32b08c97 Update dependency androidx.compose:compose-bom to v2024 5 months ago
renovate[bot] a2fcf57c9e Update mockito monorepo to v5.10.0 5 months ago
renovate[bot] 59a61325f2 Update dependency org.osmdroid:osmdroid-android to v6.1.18 5 months ago
vulewuxe86 38a6064677 Reverted code
Reverted Code involving the action bar search function
5 months ago
renovate[bot] 67daccf3e8 Update lifecycle to v2.7.0 5 months ago
renovate[bot] dfe829d2a1 Update dependency com.google.android.gms:play-services-location to v21.1.0 5 months ago
renovate[bot] 23c64f4d28 Update dependency com.google.apis:google-api-services-drive to v3-rev20240123-2.0.0 5 months ago
renovate[bot] e4b8f694f3 Update dependency com.google.firebase:firebase-bom to v32.7.1 5 months ago
renovate[bot] e667c80731 Update kotlin 5 months ago
renovate[bot] 909b077e25 Update dependency com.android.tools.build:gradle to v8.2.2 5 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 7 months ago
Alex Baker 52c54b1eac Fix excessive querying (again) 7 months ago
Alex Baker c8d81b44b6 Fix excessive querying 7 months ago
renovate[bot] ef27a50e42 Update mockito monorepo to v5.8.0 7 months ago
Alex Baker bde1356e7f Add task to MainActivityViewModel state 7 months ago
Alex Baker 6c031925ba Replace some setter usage with constructors 7 months ago
Alex Baker 8058414137 Use release build for compose metrics 7 months ago
Alex Baker 3e37ea50f0 Update compose-compiler to v1.5.5 7 months ago
renovate[bot] 62f5a9c492 Update actions/setup-java action to v4 7 months ago
renovate[bot] a84fd65722 Update dependency androidx.room:room-ktx to v2.6.1 7 months ago
renovate[bot] 517b2d8f1b Update dependency gradle to v8.5 7 months ago
renovate[bot] 90942bf0be Update dependency com.google.dagger:hilt-android to v2.49 7 months ago
Alex Baker 83c3d1c4ba AGP 8.2.0 7 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/
7 months ago
renovate[bot] 8df85041b8 Update flipper to v0.240.0 7 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/
7 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/
7 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 8 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>
8 months ago
renovate[bot] a980cd75cc Update hilt to v1.1.0 8 months ago
renovate[bot] 7eac4ac223 Update dependency androidx.fragment:fragment-ktx to v1.6.2 8 months ago
renovate[bot] 82cb2f7d3f
Update dependency com.android.tools:desugar_jdk_libs to v2.0.4 8 months ago
renovate[bot] da2646597c
Update flipper to v0.235.0 (#2608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 months ago
renovate[bot] 495855133c Update dependency com.google.firebase:firebase-bom to v32.5.0 8 months ago
renovate[bot] 242cb61662 Update coil to v2.5.0 8 months ago
renovate[bot] ab8886f3dc Update flipper to v0.234.0 8 months ago
Alex Baker e48e92d2e6 Fix dates 🤦 8 months ago
Alex Baker 5f22f5cd38 AGP 8.2.0-rc02 8 months ago
Alex Baker 8a47cc2934 Don't set local only notifications on Android 14+ 8 months ago
Alex Baker 0d94729d37 Merge branch '13.6.2' 8 months ago
Alex Baker 14599eb3c0 Update version and changelog 8 months ago
Alex Baker b477623524 Update timestamp on edits
Fix bugs introduced by 775289b05
8 months ago
Alex Baker c8bfb67b50 Allow multi-select for gallery picker 8 months ago
Alex Baker 0a36e58525 Allow multi-select in storage picker 8 months ago
Alex Baker 94a719cb66 Improve menu dismissal
Copy M3 ModalBottomSheet to add 'skipPartiallyCollapsed' support 😕
8 months ago
Alex Baker b5748aa8e6 New drawer 8 months ago
Alex Baker 7fd5647cb8 Exclude hidden and completed from snoozed filter 8 months ago
Alex Baker 2545832d67 Update version and changelog 8 months ago
Alex Baker 738bf435db Fix some back handlers 8 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 9 months ago
Alex Baker da8467ac56 Remove constructor 9 months ago
renovate[bot] 434d067822 Update dependency gradle to v8.4 9 months ago
renovate[bot] 04af310285 Update hilt to v1.1.0-beta01 9 months ago
renovate[bot] 5555771f45 Update dependency com.google.dagger:hilt-android-testing to v2.48.1 9 months ago
renovate[bot] 35b60df0ff Update dependency androidx.activity:activity-compose to v1.8.0 9 months ago
renovate[bot] fef19b4995 Update dependency androidx.compose:compose-bom to v2023.10.00 9 months ago
Alex Baker 4c25b81a4d Move Parcelable 9 months ago
Alex Baker 0f37f4859e Update compose reports 9 months ago
Alex Baker ee3d3fa4f5 Convert FilterListItem to interface 9 months ago
Alex Baker a32d35720a Refresh after changing sort mode 9 months ago
Alex Baker bf6fe02fe3 Convert FilterListItems to Kotlin 9 months ago
Alex Baker 6664defc16 Minor refactoring 9 months ago
renovate[bot] b318b930a5 Update flipper to v0.223.0 9 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/
9 months ago
Alex Baker 94b6d7569b Move search to viewmodel 9 months ago
Alex Baker e70f5f3b24 Move query constants 9 months ago
renovate[bot] 68c21c4b1f Update dependency androidx.compose:compose-bom to v2023.09.02 9 months ago
renovate[bot] cbcc7f9bee Update dependency com.android.tools.build:gradle to v8.2.0-beta06 9 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 10 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/
10 months ago
renovate[bot] 68b7bef1ca Update flipper to v0.215.1 10 months ago
renovate[bot] f6a6b0716f Update flipper to v0.215.0 10 months ago
renovate[bot] dced669176 Update flipper to v0.214.0 10 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/
10 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/
10 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 11 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/
11 months ago
renovate[bot] 9f2364867b Update dependency com.google.firebase:firebase-bom to v32.2.2 11 months ago
renovate[bot] cd638bba71
Update dependency com.google.firebase:firebase-crashlytics-gradle to v2.9.8 11 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/
11 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/
11 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/
11 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/
11 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/
11 months ago
Alex Baker dbcaa36812 Update version and changelog 11 months ago
Alex Baker 83a42c9d8f Fix crash on google task import 11 months ago
Alex Baker ec97722857 Track compose metrics 11 months ago
Alex Baker dd78acadcd Remove deprecated setting 11 months ago
Htet Oo Hlaing 454faa234e
Translated using Weblate (Burmese)
Currently translated at 9.7% (65 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/my/
11 months ago
bruh 55027ad625
Translated using Weblate (Vietnamese)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/vi/
11 months ago
Alex Tereschenko 13740c3d0d
Translated using Weblate (Russian)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ru/
11 months ago
Alex Tereschenko 6037ee70e5
Translated using Weblate (Polish)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pl/
11 months ago
Kazushi Hayama 0f2e659e6f
Translated using Weblate (Japanese)
Currently translated at 98.5% (657 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ja/
11 months ago
Milo Ivir 7945ecb9c4
Translated using Weblate (Croatian)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hr/
11 months ago
Poesty Li 56f0be50ff
Translated using Weblate (Chinese (Simplified))
Currently translated at 99.4% (663 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
11 months ago
Ihor Hordiichuk 91d46c5f11
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/uk/
11 months ago
Htet Oo Hlaing bab3898a7f
Added translation using Weblate (Burmese) 11 months ago
Alex Baker 7b12e491ad Replace isGoogleTask with accountType 11 months ago
Alex Baker ce2bc81276 Update version and changelog 11 months ago
Frits van Bommel 39ddc8d0d6
Translated using Weblate (Dutch)
Currently translated at 100.0% (667 of 667 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/nl/
11 months ago
Pierfrancesco Passerini 8081da3a36
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/
11 months ago
Florian Trayon 766d5fa043
Translated using Weblate (French)
Currently translated at 100.0% (667 of 667 strings)

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

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
11 months ago
109247019824 d1afb5891a
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/
11 months ago
Alex Baker dff522437d New recurrence activity 11 months ago
Alex Baker 5308404ed6 Fix some random inspections 11 months ago
renovate[bot] 320b399ab3 Update dependency androidx.recyclerview:recyclerview to v1.3.1 11 months ago
renovate[bot] 28c1ecaebc Update dependency androidx.preference:preference to v1.2.1 11 months ago
renovate[bot] 7083eb2ede Update dependency androidx.fragment:fragment-ktx to v1.6.1 11 months ago
renovate[bot] 194edd2084 Update dependency com.android.tools.build:gradle to v8.1.0 11 months ago
Michal Šmahel e21637cb3c Translated using Weblate (Czech)
Currently translated at 100.0% (670 of 670 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/cs/
11 months ago
Alex Baker ee82f683bd Update microsoft converter
Add due dates, creation dates, and modification dates
11 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@v4
# id: avd-cache
# with:
# path: |
# ~/.android/avd/*
# ~/.android/adb*
# key: avd-${{ matrix.api-level }}
#
# - name: create AVD and generate snapshot for caching
# if: steps.avd-cache.outputs.cache-hit != 'true'
# uses: reactivecircus/android-emulator-runner@v2
# with:
# api-level: ${{ matrix.api-level }}
# force-avd-creation: false
# emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
# disable-animations: false
# script: echo "Generated AVD snapshot for caching."
- name: run tests - 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/**

2
.gitignore vendored

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

@ -1 +1 @@
3.2.2 3.3.3

@ -1,3 +1,206 @@
### 13.10 (2024-06-14)
* Add search bar to drawer
* Add search bar to list picker
* Fix deleting new subtasks from edit screen
* Update translations
* Arabic - @islam2hamy
* Brazilian Portuguese - Jose Delvani
* Chinese (Traditional) - hugoalh
* German - min7-i
### 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)
* Fix crash when importing Google Tasks from a backup file
* Added Burmese translations - @htetoh
* Update translations
* Chinese (Simplified) - Poesty Li
* Croatian - @milotype
* Japanese - Kazushi Hayama
* Polish - @alex-ter
* Russian - @alex-ter
* Ukrainian - @IhorHordiichuk
* Vietnamese - @unbiaseduser
### 13.5 (2023-07-28)
* New custom recurrence picker
* Update translations
* Bulgarian - @StoyanDimitrov
* Czech - @ceskyDJ
* Dutch - @fvbommel
* French - @FlorianLeChat
* Italian - @ppasserini
* Spanish - @FlorianLeChat
### 13.4 - (2023-07-16) ### 13.4 - (2023-07-16)
* Sorting improvements * Sorting improvements

@ -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 = 130400 versionCode = 131000
versionName = "13.4" versionName = "13.10"
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,13 +81,6 @@ android {
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
} }
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
}
kotlinOptions {
jvmTarget = "17"
}
flavorDimensions += listOf("store") flavorDimensions += listOf("store")
@Suppress("LocalVariableName") @Suppress("LocalVariableName")
@ -131,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 {
@ -162,6 +154,8 @@ val genericImplementation by configurations
val googleplayImplementation by configurations val googleplayImplementation by configurations
dependencies { dependencies {
implementation(projects.data)
implementation(projects.kmp)
coreLibraryDesugaring(libs.desugar.jdk.libs) coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bitfire.dav4jvm) { implementation(libs.bitfire.dav4jvm) {
exclude(group = "junit") exclude(group = "junit")
@ -182,29 +176,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)
@ -225,7 +227,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))
@ -233,13 +236,15 @@ dependencies {
implementation("androidx.compose.foundation:foundation") implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material") implementation("androidx.compose.material:material")
implementation("androidx.compose.runtime:runtime-livedata") implementation("androidx.compose.runtime:runtime-livedata")
implementation(libs.compose.theme.adapter)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation("androidx.compose.material:material-icons-extended") implementation("androidx.compose.material:material-icons-extended")
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation("androidx.compose.ui:ui-viewbinding") implementation("androidx.compose.ui:ui-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)
@ -259,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>

@ -2,9 +2,9 @@ package com.todoroo.astrid.adapter
import com.natpryce.makeiteasy.MakeItEasy.with import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
import com.todoroo.astrid.api.CaldavFilter import org.tasks.filters.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,290 @@
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)) }
@Test
fun futureAlarmWithNoPastAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 18).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
verify(AlarmEntry(alarm, task, DateTime(2017, 9, 24, 19, 57).millis, TYPE_DATE_TIME)) testResults(emptyList(), DateTime(2024, 5, 18, 18, 0).millis)
}
} }
@Test @Test
fun ignoreStaleAlarm() = runBlocking { fun pastAlarmWithNoFutureAlarm() = 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, 17).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
verify() testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
0
)
}
} }
@Test @Test
fun dontScheduleReminderForCompletedTask() = runBlocking { fun pastRecurringAlarmWithFutureRecurrence() = 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,
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 alarmsOneMinuteApart() = 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_TIME,
DateTime(2024, 5, 17, 23, 20).millis
)
)
)
alarmService.synchronizeAlarms(1, mutableSetOf(Alarm(type = Alarm.TYPE_REL_END)))
taskDao.insert(Task())
alarmService.synchronizeAlarms(
taskId = 2,
alarms = mutableSetOf(
Alarm(
type = Alarm.TYPE_SNOOZE,
time = DateTime(2024, 5, 17, 23, 21).millis)
)
)
testResults(
listOf(
Notification(
taskId = 1L,
timestamp = DateTimeUtils2.currentTimeMillis(),
type = Alarm.TYPE_REL_END
)
),
DateTime(2024, 5, 17, 23, 21).millis
)
}
}
alarmDao.insert(whenDue(task)) @Test
alarmDao.insert(whenOverdue(task)) fun futureSnoozeOverrideOverdue() = runBlocking {
alarmDao.insert(Alarm(task, DateUtilities.ONE_HOUR, TYPE_RANDOM)) freezeAt(DateTime(2024, 5, 17, 23, 20)) {
val alarm = alarmDao.insert(Alarm(task, now.plusMonths(12).millis, TYPE_SNOOZE)) 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
)
)
)
verify(AlarmEntry(alarm, task, now.plusMonths(12).millis, TYPE_SNOOZE)) testResults(
emptyList(),
DateTimeUtils2.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5)
)
}
}
@Test
fun ignoreStaleAlarm() = runBlocking {
freezeAt(DateTime(2024, 5, 17, 23, 20)) {
taskDao.insert(
Task(
dueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime(2024, 5, 17).millis
),
reminderLast = DateTime(2024, 5, 17, 18, 0).millis,
)
)
alarmService.synchronizeAlarms(
1,
mutableSetOf(Alarm(type = Alarm.TYPE_REL_END))
)
testResults(
emptyList(),
0
)
}
} }
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))
}

@ -1,7 +1,7 @@
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.api.CaldavFilter import org.tasks.filters.CaldavFilter
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 dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
@ -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.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.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.data.entity.Task
import org.tasks.filters.AstridOrderingFilter
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)
} }

@ -211,7 +211,7 @@
<activity <activity
android:exported="true" android:exported="true"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:name=".widget.WidgetFilterSelectionActivity" android:name=".compose.FilterSelectionActivity"
android:taskAffinity="" android:taskAffinity=""
android:theme="@style/TranslucentDialog"/> android:theme="@style/TranslucentDialog"/>
@ -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"/>
@ -663,7 +652,11 @@
android:name="org.tasks.dialogs.SortSettingsActivity" android:name="org.tasks.dialogs.SortSettingsActivity"
android:theme="@style/TranslucentWindow" /> android:theme="@style/TranslucentWindow" />
<!-- launcher icons --> <activity
android:name="org.tasks.repeats.CustomRecurrenceActivity"
android:theme="@style/Tasks" />
<!-- launcher icons -->
<activity-alias <activity-alias
android:enabled="true" android:enabled="true"

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

@ -1,234 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.andlib.utility;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Looper;
import android.text.InputType;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import org.tasks.BuildConfig;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import timber.log.Timber;
/**
* Android Utility Classes
*
* @author Tim Su <tim@todoroo.com>
*/
public class AndroidUtilities {
public static final String SEPARATOR_ESCAPE = "!PIPE!"; // $NON-NLS-1$
public static final String SERIALIZATION_SEPARATOR = "|"; // $NON-NLS-1$
// --- utility methods
/** Suppress virtual keyboard until user's first tap */
public static void suppressVirtualKeyboard(final TextView editor) {
final int inputType = editor.getInputType();
editor.setInputType(InputType.TYPE_NULL);
editor.setOnTouchListener(
(v, event) -> {
editor.setInputType(inputType);
editor.setOnTouchListener(null);
return false;
});
}
// --- serialization
/** Serializes a content value into a string */
public static String mapToSerializedString(Map<String, Object> source) {
StringBuilder result = new StringBuilder();
for (Entry<String, Object> entry : source.entrySet()) {
addSerialized(result, entry.getKey(), entry.getValue());
}
return result.toString();
}
/** add serialized helper */
private static void addSerialized(StringBuilder result, String key, Object value) {
result
.append(key.replace(SERIALIZATION_SEPARATOR, SEPARATOR_ESCAPE))
.append(SERIALIZATION_SEPARATOR);
if (value instanceof Integer) {
result.append('i').append(value);
} else if (value instanceof Double) {
result.append('d').append(value);
} else if (value instanceof Long) {
result.append('l').append(value);
} else if (value instanceof String) {
result
.append('s')
.append(value.toString().replace(SERIALIZATION_SEPARATOR, SEPARATOR_ESCAPE));
} else if (value instanceof Boolean) {
result.append('b').append(value);
} else {
throw new UnsupportedOperationException(value.getClass().toString());
}
result.append(SERIALIZATION_SEPARATOR);
}
public static Map<String, Object> mapFromSerializedString(String string) {
if (string == null) {
return new HashMap<>();
}
Map<String, Object> result = new HashMap<>();
fromSerialized(
string,
result,
(object, key, type, value) -> {
switch (type) {
case 'i':
object.put(key, Integer.parseInt(value));
break;
case 'd':
object.put(key, Double.parseDouble(value));
break;
case 'l':
object.put(key, Long.parseLong(value));
break;
case 's':
object.put(key, value.replace(SEPARATOR_ESCAPE, SERIALIZATION_SEPARATOR));
break;
case 'b':
object.put(key, Boolean.parseBoolean(value));
break;
}
});
return result;
}
private static <T> void fromSerialized(String string, T object, SerializedPut<T> putter) {
String[] pairs = string.split("\\" + SERIALIZATION_SEPARATOR); // $NON-NLS-1$
for (int i = 0; i < pairs.length; i += 2) {
try {
String key = pairs[i].replaceAll(SEPARATOR_ESCAPE, SERIALIZATION_SEPARATOR);
String value = pairs[i + 1].substring(1);
try {
putter.put(object, key, pairs[i + 1].charAt(0), value);
} catch (NumberFormatException e) {
// failed parse to number
putter.put(object, key, 's', value);
Timber.e(e);
}
} catch (IndexOutOfBoundsException e) {
Timber.e(e);
}
}
}
public static int convertDpToPixels(DisplayMetrics displayMetrics, int dp) {
// developer.android.com/guide/practices/screens_support.html#dips-pels
return (int) (dp * displayMetrics.density + 0.5f);
}
public static boolean preOreo() {
return !atLeastOreo();
}
public static boolean preTiramisu() {
return VERSION.SDK_INT < VERSION_CODES.TIRAMISU;
}
public static boolean preUpsideDownCake() {
return VERSION.SDK_INT <= VERSION_CODES.TIRAMISU;
}
public static boolean atLeastNougatMR1() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1;
}
public static boolean atLeastOreo() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
public static boolean atLeastP() {
return VERSION.SDK_INT >= Build.VERSION_CODES.P;
}
public static boolean atLeastQ() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
public static boolean atLeastR() {
return VERSION.SDK_INT >= VERSION_CODES.R;
}
public static boolean atLeastS() {
return VERSION.SDK_INT >= VERSION_CODES.S;
}
public static boolean atLeastTiramisu() {
return VERSION.SDK_INT >= VERSION_CODES.TIRAMISU;
}
public static void assertMainThread() {
if (BuildConfig.DEBUG && !isMainThread()) {
throw new IllegalStateException("Should be called from main thread");
}
}
public static void assertNotMainThread() {
if (BuildConfig.DEBUG && isMainThread()) {
throw new IllegalStateException("Should not be called from main thread");
}
}
private static boolean isMainThread() {
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> {
void put(T object, String key, char type, String value) throws NumberFormatException;
}
}

@ -0,0 +1,105 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.andlib.utility
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.Looper
import android.text.InputType
import android.util.DisplayMetrics
import android.view.MotionEvent
import android.view.View
import android.widget.TextView
import org.tasks.BuildConfig
/**
* Android Utility Classes
*
* @author Tim Su <tim></tim>@todoroo.com>
*/
object AndroidUtilities {
// --- utility methods
/** Suppress virtual keyboard until user's first tap */
@JvmStatic
fun suppressVirtualKeyboard(editor: TextView) {
val inputType = editor.inputType
editor.inputType = InputType.TYPE_NULL
editor.setOnTouchListener { v: View?, event: MotionEvent? ->
editor.inputType = inputType
editor.setOnTouchListener(null)
false
}
}
fun convertDpToPixels(displayMetrics: DisplayMetrics, dp: Int): Int {
// developer.android.com/guide/practices/screens_support.html#dips-pels
return (dp * displayMetrics.density + 0.5f).toInt()
}
fun preOreo(): Boolean {
return !atLeastOreo()
}
fun preS(): Boolean {
return !atLeastS()
}
@JvmStatic
fun preTiramisu(): Boolean {
return !atLeastTiramisu()
}
fun preUpsideDownCake(): Boolean {
return Build.VERSION.SDK_INT <= VERSION_CODES.TIRAMISU
}
fun atLeastNougatMR1(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.N_MR1
}
@JvmStatic
fun atLeastOreo(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.O
}
fun atLeastOreoMR1(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1
}
fun atLeastP(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.P
}
@JvmStatic
fun atLeastQ(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.Q
}
@JvmStatic
fun atLeastR(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.R
}
@JvmStatic
fun atLeastS(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.S
}
fun atLeastTiramisu(): Boolean {
return Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU
}
fun assertMainThread() {
check(!(BuildConfig.DEBUG && !isMainThread)) { "Should be called from main thread" }
}
fun assertNotMainThread() {
check(!(BuildConfig.DEBUG && isMainThread)) { "Should not be called from main thread" }
}
private val isMainThread: Boolean
get() = Thread.currentThread() === Looper.getMainLooper().thread
}

@ -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,77 @@
*/ */
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.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
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.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.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.GoogleTaskListSettingsActivity
import org.tasks.activities.TagSettingsActivity 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.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.LocationDao import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.data.Place import org.tasks.compose.drawer.TasksMenu
import org.tasks.data.TagDataDao import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.LocationDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Place
import org.tasks.data.entity.Task
import org.tasks.data.getLocation
import org.tasks.data.listSettingsClass
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.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.PlaceFilter import org.tasks.filters.PlaceFilter
import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.intents.TaskIntents.getTaskListIntent
import org.tasks.location.LocationPickerActivity import org.tasks.location.LocationPickerActivity
import org.tasks.play.PlayServices import org.tasks.location.LocationPickerActivity.Companion.EXTRA_PLACE
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.TasksTheme
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,13 +83,12 @@ 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
@Inject lateinit var caldavDao: CaldavDao
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
@ -95,48 +101,162 @@ 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) {
TasksTheme {
TasksMenu(
items = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems,
begForMoney = state.begForMoney,
isTopAppBar = preferences.isTopAppBar,
setFilter = { viewModel.setFilter(it) },
toggleCollapsed = { viewModel.toggleCollapsed(it) },
addFilter = {
when (it.addIntentRc) {
FilterProvider.REQUEST_NEW_FILTER ->
NewFilterDialog.newFilterDialog().show(
supportFragmentManager,
SubheaderClickHandler.FRAG_TAG_NEW_FILTER
)
REQUEST_NEW_PLACE ->
startActivityForResult(
Intent(this, LocationPickerActivity::class.java),
REQUEST_NEW_PLACE
)
REQUEST_NEW_TAGS ->
startActivityForResult(
Intent(this, TagSettingsActivity::class.java),
REQUEST_NEW_LIST
)
REQUEST_NEW_LIST -> lifecycleScope.launch {
val account = caldavDao.getAccount(it.id) ?: return@launch
when (it.subheaderType) {
NavigationDrawerSubheader.SubheaderType.GOOGLE_TASKS ->
startActivityForResult(
Intent(this@MainActivity, GoogleTaskListSettingsActivity::class.java)
.putExtra(GoogleTaskListSettingsActivity.EXTRA_ACCOUNT, account),
REQUEST_NEW_LIST
)
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS,
NavigationDrawerSubheader.SubheaderType.ETESYNC ->
startActivityForResult(
Intent(this@MainActivity, account.listSettingsClass())
.putExtra(BaseCaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT, account),
REQUEST_NEW_LIST
)
else -> {}
}
}
else -> Timber.e("Unhandled request code: $it")
}
},
dismiss = { viewModel.setDrawerOpen(false) },
query = state.menuQuery,
onQueryChange = { viewModel.queryMenu(it) },
)
}
}
}
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 +265,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
}
private fun openTask(filter: Filter?) = lifecycleScope.launch { else -> null
val task = getTaskToLoad(filter)
when {
task != null -> onTaskListItemClicked(task)
taskEditFragment == null -> hideDetailFragment()
else -> showDetailFragment()
}
} }
private fun handleIntent() { private fun logIntent(caller: String) {
val intent = intent if (BuildConfig.DEBUG) {
val openFilter = intent.getFilter Timber.d("""
val loadFilter = intent.getFilterString $caller
val openTask = !intent.isFromHistory **********
&& (intent.hasExtra(OPEN_TASK) || intent.hasExtra(CREATE_TASK)) broughtToFront: ${intent.broughtToFront}
val tef = taskEditFragment isFromHistory: ${intent.isFromHistory}
Timber.d(""" flags: ${intent.flagsToString}
OPEN_FILTER: ${getParcelableExtra(intent, OPEN_FILTER, Filter::class.java)?.let { "${it.title}: $it" }}
********** LOAD_FILTER: ${intent.getStringExtra(LOAD_FILTER)}
broughtToFront: ${intent.broughtToFront} OPEN_TASK: ${getParcelableExtra(intent, OPEN_TASK, Task::class.java)}
isFromHistory: ${intent.isFromHistory} CREATE_TASK: ${intent.hasExtra(CREATE_TASK)}
flags: ${intent.flagsToString} **********""".trimIndent()
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 handleIntent() {
if (isSinglePaneLayout) { lifecycleScope.launch {
binding.detail.visibility = View.VISIBLE val filter = intent.getFilter
binding.master.visibility = View.GONE ?: intent.getFilterString?.let { defaultFilterProvider.getFilterFromPreference(it) }
?: viewModel.state.value.filter
val task = getTaskToLoad(filter)
viewModel.setFilter(filter = filter, task = task)
} }
} }
private fun hideDetailFragment() { private fun updateSystemBars(filter: Filter) {
supportFragmentManager with (getFilterColor(filter)) {
.beginTransaction() applyToNavigationBar(this@MainActivity)
.replace(R.id.detail, newEmptyTaskEditFragment()) applyTaskDescription(this@MainActivity, filter.title ?: getString(R.string.app_name))
.runOnCommit {
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() { private fun getFilterColor(filter: Filter) =
val filterColor = filterColor if (filter.tint != 0)
filterColor.applyToNavigationBar(this) colorProvider.getThemeColor(filter.tint, true)
filterColor.applyTaskDescription(this, filter?.listingTitle ?: getString(R.string.app_name)) else
theme.withThemeColor(filterColor).applyToContext(this) theme.themeColor
}
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 +351,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 +391,18 @@ 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_TAGS = 10101
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 +445,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,238 @@
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.CustomFilter
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.NO_COUNT
import org.tasks.data.count
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.Task
import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter
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(),
val searchItems: ImmutableList<DrawerItem> = persistentListOf(),
val menuQuery: String = "",
)
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,
menuQuery = if (!open) "" else it.menuQuery,
)
}
}
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.addIntentRc != 0,
type = { item },
)
else -> throw IllegalArgumentException()
}
}
.let { filters -> _state.update { it.copy(drawerItems = filters.toPersistentList()) } }
val query = _state.value.menuQuery
filterProvider
.allFilters()
.filter { it.title!!.contains(query, ignoreCase = true) }
.map { item ->
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 },
)
}
.let { filters -> _state.update { it.copy(searchItems = 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) }
}
fun queryMenu(query: String) {
_state.update { it.copy(menuQuery = query) }
updateFilters()
}
}

@ -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,20 +10,20 @@ 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
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Divider import androidx.compose.material3.Divider
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -35,23 +35,20 @@ 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
import androidx.lifecycle.lifecycleScope 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.todoroo.andlib.utility.AndroidUtilities.atLeastOreoMR1
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
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.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
@ -64,6 +61,8 @@ import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.calendars.CalendarPicker import org.tasks.calendars.CalendarPicker
import org.tasks.compose.BeastModeBanner import org.tasks.compose.BeastModeBanner
import org.tasks.compose.FilterSelectionActivity.Companion.launch
import org.tasks.compose.FilterSelectionActivity.Companion.registerForListPickerResult
import org.tasks.compose.collectAsStateLifecycleAware import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.CommentsRow import org.tasks.compose.edit.CommentsRow
import org.tasks.compose.edit.DescriptionRow import org.tasks.compose.edit.DescriptionRow
@ -71,10 +70,11 @@ 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.Location import org.tasks.data.Location
import org.tasks.data.TagData import org.tasks.data.dao.UserActivityDao
import org.tasks.data.UserActivityDao import org.tasks.data.entity.Alarm
import org.tasks.data.entity.TagData
import org.tasks.data.entity.Task
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
@ -88,10 +88,10 @@ import org.tasks.databinding.TaskEditTimerBinding
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.DateTimePicker import org.tasks.dialogs.DateTimePicker
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.FilterPicker.Companion.newFilterPicker
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.filters.Filter
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
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_DESCRIPTION import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_DESCRIPTION
@ -100,7 +100,9 @@ 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.themes.TasksTheme
import org.tasks.ui.CalendarControlSet import org.tasks.ui.CalendarControlSet
import org.tasks.ui.ChipProvider import org.tasks.ui.ChipProvider
import org.tasks.ui.LocationControlSet import org.tasks.ui.LocationControlSet
@ -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
@ -138,9 +140,28 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
activity?.recreate() activity?.recreate()
} }
private val listPickerLauncher = registerForListPickerResult { filter ->
editViewModel.selectedList.update { filter }
}
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
@ -254,7 +275,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
} }
binding.composeView.setContent { binding.composeView.setContent {
MdcTheme { TasksTheme {
Column(modifier = Modifier.gesturesDisabled(editViewModel.isReadOnly)) { Column(modifier = Modifier.gesturesDisabled(editViewModel.isReadOnly)) {
taskEditControlSetFragmentManager.displayOrder.forEachIndexed { index, tag -> taskEditControlSetFragmentManager.displayOrder.forEachIndexed { index, tag ->
if (index < taskEditControlSetFragmentManager.visibleSize) { if (index < taskEditControlSetFragmentManager.visibleSize) {
@ -289,12 +310,16 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
} }
} }
childFragmentManager.setFilterPickerResultListener(this) { filter ->
editViewModel.selectedList.update { filter }
}
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)
@ -315,7 +340,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
binding.banner.setContent { binding.banner.setContent {
var visible by rememberSaveable { mutableStateOf(true) } var visible by rememberSaveable { mutableStateOf(true) }
val context = LocalContext.current val context = LocalContext.current
MdcTheme { TasksTheme {
BeastModeBanner( BeastModeBanner(
visible, visible,
showSettings = { showSettings = {
@ -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)
} }
@ -489,11 +493,11 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
list = list, list = list,
colorProvider = { chipProvider.getColor(it) }, colorProvider = { chipProvider.getColor(it) },
onClick = { onClick = {
newFilterPicker(list, true) listPickerLauncher.launch(
.show( context = context,
childFragmentManager, selectedFilter = list,
FRAG_TAG_GOOGLE_TASK_LIST_SELECTION listsOnly = true
) )
} }
) )
} }
@ -525,15 +529,12 @@ 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"
const val EXTRA_TAGS = "extra_tags" const val EXTRA_TAGS = "extra_tags"
const val EXTRA_ALARMS = "extra_alarms" const val EXTRA_ALARMS = "extra_alarms"
private const val FRAG_TAG_GOOGLE_TASK_LIST_SELECTION =
"frag_tag_google_task_list_selection"
const val FRAG_TAG_CALENDAR_PICKER = "frag_tag_calendar_picker" const val FRAG_TAG_CALENDAR_PICKER = "frag_tag_calendar_picker"
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker" private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
const val REQUEST_CODE_PICK_CALENDAR = 70 const val REQUEST_CODE_PICK_CALENDAR = 70

@ -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
@ -45,44 +47,35 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.appbar.AppBarLayout 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.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.CaldavFilter import com.todoroo.astrid.api.CustomFilter
import com.todoroo.astrid.api.Filter
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
@ -90,39 +83,55 @@ import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase 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.FilterSelectionActivity.Companion.launch
import org.tasks.compose.FilterSelectionActivity.Companion.registerForListPickerResult
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.PriorityPicker.Companion.newPriorityPicker
import org.tasks.dialogs.FilterPicker.Companion.setFilterPickerResultListener
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.AstridOrderingFilter
import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter
import org.tasks.filters.FilterImpl
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.TasksTheme
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,55 @@ 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
private val listPickerLauncher = registerForListPickerResult {
val selected = taskAdapter.getSelected()
lifecycleScope.launch {
taskMover.move(selected, it)
}
finishActionMode()
}
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)
}
}
}
@OptIn(ExperimentalAnimationApi::class)
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 +222,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 +239,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 +253,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,23 +336,48 @@ 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) { binding.banner.setContent {
val selected = taskAdapter.getSelected() val context = LocalContext.current
lifecycleScope.launch { val showBanner = listViewModel.state.collectAsStateLifecycleAware().value.begForSubscription
taskMover.move(selected, it) TasksTheme {
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)
},
)
} }
finishActionMode()
} }
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,11 +740,27 @@ 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)
newFilterPicker(singleFilter, true) listPickerLauncher.launch(
.show(childFragmentManager, FRAG_TAG_REMOTE_LIST_PICKER) context = requireActivity(),
selectedFilter = singleFilter,
listsOnly = true,
)
} }
true true
} }
@ -776,17 +782,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 +834,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 +859,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 +885,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 +935,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 +962,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 +995,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 +1017,21 @@ 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_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.Filter
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 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.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskListMetadata import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.entity.Task
import org.tasks.data.entity.TaskListMetadata
import org.tasks.filters.AstridOrderingFilter
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,16 +7,20 @@ 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 org.tasks.filters.CaldavFilter
import com.todoroo.astrid.api.CustomFilter
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
import org.tasks.extensions.formatNumber import org.tasks.extensions.formatNumber
import org.tasks.filters.Filter
import org.tasks.filters.PlaceFilter 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) {

@ -10,17 +10,20 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.FilterListItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import org.tasks.R
import org.tasks.activities.DragAndDropDiffer import org.tasks.activities.DragAndDropDiffer
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.filters.Filter
import org.tasks.filters.FilterListItem
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 +35,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,32 +57,28 @@ 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 layout = when (type) {
FilterListItem.Type.ITEM -> R.layout.filter_adapter_row
FilterListItem.Type.SUBHEADER -> R.layout.filter_adapter_subheader
}
val view = LayoutInflater.from(parent.context).inflate(layout, parent, false)
return when (type) { return when (type) {
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 +89,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 +101,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,15 @@ 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.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(
@ -35,21 +37,10 @@ class SubheaderClickHandler @Inject constructor(
} }
} }
override fun onAdd(subheader: NavigationDrawerSubheader) {
when (subheader.addIntentRc) {
NavigationDrawerFragment.REQUEST_NEW_FILTER ->
NewFilterDialog.newFilterDialog().show(
(activity as AppCompatActivity).supportFragmentManager,
FRAG_TAG_NEW_FILTER
)
else -> activity.startActivityForResult(subheader.addIntent, subheader.addIntentRc)
}
}
override fun showError() = override fun showError() =
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"
} }
} }

@ -18,7 +18,6 @@ internal class SubheaderViewHolder(
interface ClickHandler { interface ClickHandler {
fun onClick(subheader: NavigationDrawerSubheader) fun onClick(subheader: NavigationDrawerSubheader)
fun onAdd(subheader: NavigationDrawerSubheader)
fun showError() fun showError()
} }
@ -31,9 +30,9 @@ internal class SubheaderViewHolder(
private lateinit var subheader: NavigationDrawerSubheader private lateinit var subheader: NavigationDrawerSubheader
fun bind(subheader: NavigationDrawerSubheader) { fun bind(subheader: NavigationDrawerSubheader) {
add.isVisible = subheader.addIntent != null add.isVisible = false
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) {
@ -63,6 +62,5 @@ internal class SubheaderViewHolder(
} }
} }
errorIcon.setOnClickListener { clickHandler.showError() } errorIcon.setOnClickListener { clickHandler.showError() }
add.setOnClickListener { clickHandler.onAdd(subheader) }
} }
} }

@ -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,19 +56,10 @@ 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 {
if (target.isGoogleTask) { if (target.isSingleLevelSubtask) {
return if (!source.hasChildren() || to <= 0 || to >= count - 1) { return if (!source.hasChildren() || to <= 0 || to >= count - 1) {
true true
} else if (from < to) { } else if (from < to) {
@ -91,7 +82,7 @@ open class TaskAdapter(
open fun maxIndent(previousPosition: Int, task: TaskContainer): Int { open fun maxIndent(previousPosition: Int, task: TaskContainer): Int {
val previous = getTask(previousPosition) val previous = getTask(previousPosition)
return if (previous.isGoogleTask) { return if (previous.isSingleLevelSubtask) {
if (task.hasChildren()) 0 else 1 if (task.hasChildren()) 0 else 1
} else { } else {
previous.indent + 1 previous.indent + 1
@ -104,7 +95,7 @@ open class TaskAdapter(
return 0 return 0
} }
val next = getTask(it) val next = getTask(it)
if (next.isGoogleTask) { if (next.isSingleLevelSubtask) {
return if (task.hasChildren() || !next.hasParent()) 0 else 1 return if (task.hasChildren() || !next.hasParent()) 0 else 1
} }
if (!taskIsChild(task, it)) { if (!taskIsChild(task, it)) {
@ -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,11 @@
package com.todoroo.astrid.adapter package com.todoroo.astrid.adapter
import android.content.Context import android.content.Context
import com.todoroo.astrid.api.CaldavFilter import org.tasks.filters.CaldavFilter
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 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 +13,28 @@ 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.dao.TaskListMetadataDao
import org.tasks.data.TaskListMetadataDao import org.tasks.data.entity.Task.Companion.isUuidEmpty
import org.tasks.data.entity.TaskListMetadata
import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.Filter
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
} }
@ -74,19 +73,15 @@ class AlarmCalculator(
* We take the last reminder time and add approximately the reminder period. If it's still in * We take the last reminder time and add approximately the reminder period. If it's still in
* the past, we set it to some time in the near future. * the past, we set it to some time in the near future.
*/ */
private fun calculateNextRandomReminder(random: Random, task: Task, reminderPeriod: Long): Long { private fun calculateNextRandomReminder(random: Random, task: Task, reminderPeriod: Long) =
if (reminderPeriod > 0) { if (reminderPeriod > 0) {
var `when` = task.reminderLast maxOf(
if (`when` == 0L) { task.reminderLast
`when` = task.creationDate .coerceAtLeast(task.creationDate)
} .plus((reminderPeriod * (0.85f + 0.3f * random.nextFloat())).toLong()),
`when` += (reminderPeriod * (0.85f + 0.3f * random.nextFloat())).toLong() task.hideUntil
if (`when` < DateUtilities.now()) { )
`when` = } else {
DateUtilities.now() + ((0.5f + 6 * random.nextFloat()) * DateUtilities.ONE_HOUR).toLong() AlarmService.NO_ALARM
}
return `when`
} }
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,82 @@ 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<CaldavFilter>() {
/** {@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);
}
}

@ -1,70 +0,0 @@
package com.todoroo.astrid.api;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import java.util.Objects;
import org.tasks.R;
public class CustomFilter extends Filter {
/** Parcelable Creator Object */
public static final Parcelable.Creator<CustomFilter> CREATOR =
new Parcelable.Creator<CustomFilter>() {
/** {@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,36 @@
package com.todoroo.astrid.api
import kotlinx.parcelize.Parcelize
import org.tasks.filters.Filter
import org.tasks.filters.FilterListItem
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