Compare commits

...

449 Commits
14.6.2 ... main

Author SHA1 Message Date
renovate[bot] fbfcbdc555
Update dependency androidx.sqlite:sqlite-bundled to v2.6.2 (#4006)
* Update dependency androidx.sqlite:sqlite-bundled to v2.6.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2 weeks ago
renovate[bot] 51e347f22b
Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.9.5 (#4005)
* Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.9.5

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2 weeks ago
renovate[bot] d0c28baf7b
Update dependency androidx.compose:compose-bom to v2025.11.01 (#4002)
* Update dependency androidx.compose:compose-bom to v2025.11.01

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2 weeks ago
Pierfrancesco Passerini 3d4d44849e Translated using Weblate (Italian)
Currently translated at 99.6% (654 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
3 weeks ago
renovate[bot] dfa41c515a
Update protobuf monorepo to v4.33.1 (#3998)
* Update protobuf monorepo to v4.33.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 weeks ago
M a539b3a3e4
Remove codebeat badge (#3996) 3 weeks ago
renovate[bot] 8d6de19b2a
Update agp to v8.13.1 (#3997)
* Update agp to v8.13.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 weeks ago
Alex Baker 2a6e1638c9 Update version and changelog 3 weeks ago
Alex Baker 9190930745 Random reminder fixes
- Make random reminder calculation deterministic
- Don't fire reminders immediately on recurring tasks
3 weeks ago
Alex Baker 40961dad87 Refactor custom and random reminder dialogs 3 weeks ago
renovate[bot] 9fbe27345d
Update dependency com.google.android.gms:oss-licenses-plugin to v0.10.9 (#3968)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 5f67e0ea3a
Update okhttp monorepo to v5 (major) (#3710)
* Update okhttp monorepo to v5

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] d51171b17e
Update plugin jetbrains-compose to v1.9.3 (#3983)
* Update plugin jetbrains-compose to v1.9.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] b657773a2d
Update wearCompose to v1.5.5 (#3973)
* Update wearCompose to v1.5.5

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] d84effc447
Update dependency androidx.compose.material3.adaptive:adaptive-navigation-android to v1.2.0 (#3976)
* Update dependency androidx.compose.material3.adaptive:adaptive-navigation-android to v1.2.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 21db540614
Update dependency androidx.core:core-splashscreen to v1.2.0 (#3977)
* Update dependency androidx.core:core-splashscreen to v1.2.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
Alex Baker 8e9f27c46e Fall back to internal storage...
if external private storage is unavailable
4 weeks ago
Alex Baker 94ad2a381e Remove use of internal padding 4 weeks ago
renovate[bot] 747928c8c7
Update dependency io.grpc:grpc-kotlin-stub to v1.5.0 (#3981)
* Update dependency io.grpc:grpc-kotlin-stub to v1.5.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 9a28f1062b
Update dependency com.google.apis:google-api-services-tasks to v1-rev20251102-2.0.0 (#3970)
* Update dependency com.google.apis:google-api-services-tasks to v1-rev20251102-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] ffe749bf0c
Update dependency androidx.compose.material3.adaptive:adaptive-layout-android to v1.2.0 (#3975)
* Update dependency androidx.compose.material3.adaptive:adaptive-layout-android to v1.2.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] b0ae0129ae
Update ktor monorepo to v3.3.2 (#3702)
* Update ktor monorepo to v3.3.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 6715369d93
Update dependency androidx.work:work-runtime-ktx to v2.11.0 (#3979)
* Update dependency androidx.work:work-runtime-ktx to v2.11.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 4efb678699
Update dependency androidx.sqlite:sqlite-bundled to v2.6.1 (#3978)
* Update dependency androidx.sqlite:sqlite-bundled to v2.6.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 581b789a0b
Update lifecycle to v2.9.4 (#3972)
* Update lifecycle to v2.9.4

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 15d8b3aa59
Update dependency com.google.gms:google-services to v4.4.4 (#3971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] dcd5d8c094
Update mockito monorepo to v5.20.0 (#3982)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] f3253e6188
Update protobuf monorepo to v4.33.0 (#3984)
* Update protobuf monorepo to v4.33.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 34b0c62ef8
Update dependency androidx.navigation:navigation-compose to v2.9.6 (#3967)
* Update dependency androidx.navigation:navigation-compose to v2.9.6

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 3754196714
Update room to v2.8.3 (#3985)
* Update room to v2.8.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
Igor Sorocean b94a91efbe Translated using Weblate (Romanian)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ro/
4 weeks ago
renovate[bot] 2c6066c378
Update dependency androidx.compose:compose-bom to v2025.11.00 (#3974)
* Update dependency androidx.compose:compose-bom to v2025.11.00

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 03e15a8c35
Update GitHub Artifact Actions (major) (#3987)
Update GitHub Artifact Actions

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] e63add73bc
Update dagger.hilt to v2.57.2 (#3965)
* Update dagger.hilt to v2.57.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] bf676bcea7
Update dependency ruby to v3.4.7 (#3896)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 6de8fe2fa0
Update dependency com.google.apis:google-api-services-drive to v3-rev20251019-2.0.0 (#3969)
* Update dependency com.google.apis:google-api-services-drive to v3-rev20251019-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
renovate[bot] 5907f27172
Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.9.4 (#3966)
* Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.9.4

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 weeks ago
Alex Baker 38119d4560 Update version and changelog 4 weeks ago
Alex Baker 52848a5308 Attempt to fix flashing widgets 4 weeks ago
Alex Baker 4c492120b3 Update Google Play Billing to 7.1.1 4 weeks ago
Alex Baker 20e995b19b Fix locale support for Hebrew and Indonesian 4 weeks ago
Alex Baker 2b63e33de2 Fix reentrant deadlock 4 weeks ago
hasak 5980bd497d Translated using Weblate (Bosnian)
Currently translated at 17.0% (112 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bs/
2 months ago
Erigmac X e16b5cd6cd Translated using Weblate (Indonesian)
Currently translated at 84.8% (28 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/id/
2 months ago
Akihiko Suzuki (array) 3d9945c798 Translated using Weblate (Japanese)
Currently translated at 87.8% (29 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/ja/
2 months ago
Petri Hämäläinen 5c9eb1c35f Translated using Weblate (Finnish)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/fi/
2 months ago
Petri Hämäläinen 6b594a3213 Translated using Weblate (Finnish)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fi/
2 months ago
Alex Baker 9e0e01f89b Custom filter creation improvements 2 months ago
Alex Baker 932b8b0540 Update version and changelog 3 months ago
Alex Baker 152a9684e5 Downgrade Room and BundledSQLite
https://issuetracker.google.com/issues/442032108
3 months ago
renovate[bot] 88c817b770
Update room to v2.8.0 (#3895)
* Update room to v2.8.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] a368960073
Update hilt to v1.3.0 (#3891)
* Update hilt to v1.3.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
Alex Baker 4f1cc5ab8e Update version and changelog 3 months ago
Alex Baker 17f54b6d32 Disable updatePeriodMillis, limit to 50 items 3 months ago
Alex Baker 6a44bed0e8 Update version and changelog 3 months ago
Alex Baker 7fa90396b3 Limit widgets to 175 rows
200 rows gave me binder size warnings
3 months ago
Alex Baker d1df39d12c Remove limit on widget items 3 months ago
Alex Baker d9ddd45f13 Add WidgetIconProvider 3 months ago
renovate[bot] 27b21118eb
Update dependency com.google.android.gms:play-services-oss-licenses to v17.3.0 (#3889)
* Update dependency com.google.android.gms:play-services-oss-licenses to v17.3.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] f725365f87
Update dependency androidx.sqlite:sqlite-bundled to v2.6.0 (#3888)
* Update dependency androidx.sqlite:sqlite-bundled to v2.6.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
Alex Baker 7377e4672d Fix tests 3 months ago
renovate[bot] 8c90b1ec87
Update dependency androidx.compose:compose-bom to v2025.09.00 (#3887)
* Update dependency androidx.compose:compose-bom to v2025.09.00

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
Alex Baker 5b50f45a5b Fix nextcloud sharing by username 3 months ago
Alex Baker 882338f554 Fix microsoft sync issues
- fix sync on empty categories
- apply parent completion time to checklist items
3 months ago
renovate[bot] 930e980550
Update dependency androidx.activity:activity-compose to v1.11.0 (#3886)
* Update dependency androidx.activity:activity-compose to v1.11.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] fb243c7aaf
Update protobuf monorepo to v4.32.1 (#3884)
* Update protobuf monorepo to v4.32.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] c3842fd2f7
Update wearCompose to v1.5.1 (#3885)
* Update wearCompose to v1.5.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 81ecb322e9
Update dependency androidx.wear:wear-input to v1.2.0 (#3882)
* Update dependency androidx.wear:wear-input to v1.2.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 1b386458b8
Update dependency androidx.work:work-runtime-ktx to v2.10.4 (#3883)
* Update dependency androidx.work:work-runtime-ktx to v2.10.4

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 7dca092831
Update dependency androidx.navigation:navigation-compose to v2.9.4 (#3881)
* Update dependency androidx.navigation:navigation-compose to v2.9.4

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 2749b029c5
Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.9.1 (#3880) 3 months ago
Alex Baker 0cb19221c4 Update version and changelog 3 months ago
Alex Baker d900f72a5c Fix Microsoft To Do patching
Encode null in patch so you can remove values
3 months ago
Alex Baker 0d8979b72c Fix moving data to a Google Tasks account
When moving data to a Google Tasks account from any other synchronized
account, the app could fail to delete the data from the source account
3 months ago
Alex Baker 703322f510 Force refresh on resume
Previously visible unstarted or not-due tasks that are now started or
overdue need to be updated
3 months ago
Alex Baker 192351a4b8 Add ScreenUnlockReceiver 3 months ago
Alex Baker 844a3a0ff8 Update version and changelog 3 months ago
Alex Baker e6bbc8d361 Attempt to address Android 16 widget changes 3 months ago
Alex Baker 04ab41f622 Use task ids for widget click intents 3 months ago
Alex Baker 4946b0ca06 Apply missing obj fix to etebase, caldav deletion 3 months ago
Alex Baker 2c29194ff2 Try to recover from missing caldav obj 3 months ago
renovate[bot] 1aaaad86da
Update dependency com.google.apis:google-api-services-drive to v3-rev20250829-2.0.0 (#3860)
* Update dependency com.google.apis:google-api-services-drive to v3-rev20250829-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
Alex Baker 68601873fd Add RefreshBroadcaster interface 3 months ago
Alex Baker c9721790ce Remove broadcastRefreshList 3 months ago
renovate[bot] 6f16a29fd7
Update dependency com.google.auth:google-auth-library-oauth2-http to v1.39.0 (#3863)
* Update dependency com.google.auth:google-auth-library-oauth2-http to v1.39.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
happy happy a501b81bfc Added translation using Weblate (Hindi) 3 months ago
happy happy fded7fbdd5 Added translation using Weblate (Hindi) 3 months ago
Alex Baker 3ff4a2339b Move KeyStoreEncryption to kmp 3 months ago
vale-decem e400594e5b Translated using Weblate (Serbian)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
3 months ago
Alex Baker 8bbbc1dcac Update version and changelog 3 months ago
github-actions[bot] 38d27b262a Update dependency diffs 3 months ago
Alex Baker b61842646b Update ical4android
Fixes alarm synchronization
3 months ago
Alex Baker 29cbb33a42 Move VtodoCache to kmp 3 months ago
renovate[bot] 2418b664e9
Update agp to v8.13.0 (#3850)
* Update agp to v8.13.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
vale-decem 0e3803c28d Translated using Weblate (Serbian)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
3 months ago
glemco c0bb7b306a Translated using Weblate (Italian)
Currently translated at 99.6% (654 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
3 months ago
Alex Baker 903412fdea Update version and changelog 3 months ago
Alex Baker 12b1127e6b Fix clear completed crash when grouping by list 3 months ago
Alex Baker c3ce7a43fb Fix all day calendar events 3 months ago
bittin1ddc447d824349b2 a391dff9bc Translated using Weblate (Swedish)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sv/
3 months ago
renovate[bot] 28e12110fa
Update lifecycle to v2.9.3 (#3839)
* Update lifecycle to v2.9.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 0bba2c4a63
Update wearCompose to v1.5.0 (#3840)
* Update wearCompose to v1.5.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 700421c5ce
Update dependency com.google.android.gms:oss-licenses-plugin to v0.10.8 (#3838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] d4a742b136
Update agp to v8.12.2 (#3837)
* Update agp to v8.12.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 585967c601
Update dependency androidx.wear:wear-input to v1.2.0-rc01 (#3836)
* Update dependency androidx.wear:wear-input to v1.2.0-rc01

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] d279f7c42e
Update dependency androidx.compose:compose-bom to v2025.08.01 (#3835)
* Update dependency androidx.compose:compose-bom to v2025.08.01

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
Milo Ivir 5cfbe9c8cb Translated using Weblate (Croatian)
Currently translated at 100.0% (656 of 656 strings)

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

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/nl/
3 months ago
MisterTechnik 3401a59716 Translated using Weblate (German)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
3 months ago
renovate[bot] d0328b378a
Update dependency com.google.apis:google-api-services-drive to v3-rev20250819-2.0.0 (#3823)
* Update dependency com.google.apis:google-api-services-drive to v3-rev20250819-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
Alex Baker f9d859a33e Update version and changelog 3 months ago
Alex Baker 5a1560e513 System bar scrim improvements 3 months ago
renovate[bot] 655cdc1a9d
Update dependency com.google.auth:google-auth-library-oauth2-http to v1.38.0 (#3820)
* Update dependency com.google.auth:google-auth-library-oauth2-http to v1.38.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] f2235e6aa6
Update dependency com.microsoft.identity.client:msal to v7.0.3 (#3818)
* Update dependency com.microsoft.identity.client:msal to v7.0.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 09baafb47f
Update actions/setup-java action to v5 (#3819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3 months ago
renovate[bot] 2d0cfaa04d
Update dependency co.touchlab:kermit to v2.0.8 (#3817)
* Update dependency co.touchlab:kermit to v2.0.8

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
renovate[bot] e60d516fcf
Update dagger.hilt to v2.57.1 (#3816)
* Update dagger.hilt to v2.57.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
3 months ago
Jiho Min 9ba82c3a01 Translated using Weblate (Korean)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/ko/
3 months ago
Jiho Min b7abdfe2ea Translated using Weblate (Korean)
Currently translated at 99.6% (654 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ko/
3 months ago
renovate[bot] 3da6f67ace
Update dependency com.microsoft.identity.client:msal to v7.0.2 (#3810)
* Update dependency com.microsoft.identity.client:msal to v7.0.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] eec3ae447a
Update actions/checkout action to v5 (#3806)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 7a9a27eae0
Update agp to v8.12.1 (#3809)
* Update agp to v8.12.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
Hugoren Martinako 05b5f1470a Translated using Weblate (Galician)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/gl/
4 months ago
Don Zouras 2a94af70fd Translated using Weblate (Esperanto)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
4 months ago
Hugoren Martinako 464903bf4d Translated using Weblate (Portuguese)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt/
4 months ago
Iago 0542f24c29 Translated using Weblate (Galician)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/gl/
4 months ago
Hugoren Martinako 0fe834b46c Translated using Weblate (Galician)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/gl/
4 months ago
Iago 997810af4c Translated using Weblate (Galician)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/gl/
4 months ago
Hugoren Martinako 6829f3f690 Translated using Weblate (Galician)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/gl/
4 months ago
ferranpujolcamins cec5c1e4b8 Translated using Weblate (Catalan)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ca/
4 months ago
Hugoren Martinako d3a12b039a Translated using Weblate (Catalan)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ca/
4 months ago
renovate[bot] 67dcc1db38
Update mockito monorepo to v5.19.0 (#3804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 83ae176288
Update protobuf monorepo to v4.32.0 (#3805)
* Update protobuf monorepo to v4.32.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 5e535b6d46
Update dependency androidx.compose:compose-bom to v2025.08.00 (#3802)
* Update dependency androidx.compose:compose-bom to v2025.08.00

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 5c124047e8
Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.9.0 (#3803)
* Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.9.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 8a332c8b2a
Update dependency com.microsoft.identity.client:msal to v7.0.1 (#3801)
* Update dependency com.microsoft.identity.client:msal to v7.0.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 9d6a925fca
Update dependency androidx.fragment:fragment-compose to v1.8.9 (#3799)
* Update dependency androidx.fragment:fragment-compose to v1.8.9

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 8cc28aa88b
Update dependency com.google.android.gms:play-services-oss-licenses to v17.2.2 (#3800)
* Update dependency com.google.android.gms:play-services-oss-licenses to v17.2.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
Hugoren Martinako 99ea6cb0eb Translated using Weblate (Spanish)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/es/
4 months ago
Hugoren Martinako a698236f4d Translated using Weblate (Spanish)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
4 months ago
Alex Baker ad616472b3 Attempt to recover from HTTP 400 errors 4 months ago
Alex Baker d5cda9e84b Always fetch and apply remote order and parents
Subtasks are sorted manually by default, so the app should try to remain
consistent with remote order
4 months ago
Алексей Ежков 9808ca1745 Translated using Weblate (Russian)
Currently translated at 99.8% (655 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ru/
4 months ago
Alex Baker 0b76caa9a8 Don't log missing wear api stacktrace 4 months ago
Alex Baker 07ac9f9ead Fix crash when looking up ALWAYS_FINISH_ACTIVITIES 4 months ago
Alex Baker 977edf4d8d Add proguard rule for microsoft 4 months ago
Alex Baker 071d670c6d Attempt to fix crash in menu search bar 4 months ago
Alex Baker eb89cc689a Two panes on medium width, like z folds 4 months ago
renovate[bot] a5c73ccc24
Update dependency com.google.android.gms:oss-licenses-plugin to v0.10.7 (#3784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 51884d46f2
Update dependency com.google.firebase:firebase-crashlytics-gradle to v3.0.6 (#3785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months ago
Antoni Jurczyk 101d7f2357 Translated using Weblate (Polish)
Currently translated at 84.8% (28 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/pl/
4 months ago
renovate[bot] 75563b6a61
Update dependency com.microsoft.identity.client:msal to v7 (#3787)
* Update dependency com.microsoft.identity.client:msal to v7

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
Tamas Gervai 3028d492b2 Translated using Weblate (Hungarian)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hu/
4 months ago
Antoni Jurczyk ab2fc34e98 Translated using Weblate (Polish)
Currently translated at 95.1% (624 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pl/
4 months ago
renovate[bot] 852ac708b5
Update actions/download-artifact action to v5 (#3779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months ago
Pierfrancesco Passerini 092f357719 Translated using Weblate (Italian)
Currently translated at 99.5% (653 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
4 months ago
ERYpTION ad1ace8fbf Translated using Weblate (Danish)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/da/
4 months ago
odnankenobi 204f49fc25 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (655 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
4 months ago
Alex Baker 9ef95291c8 Update version and changelog 4 months ago
Alex Baker 3e034ab91f Target Android 15
Need to update some back handler stuff for Android 16
4 months ago
Alex Baker 6f89ac3b93 Fix some window inset issues 4 months ago
Alex Baker 7d2ebf9cdf Use ExistingPeriodicWorkPolicy.UPDATE 4 months ago
Alex Baker 16011b1963 Target Android 16 4 months ago
Alex Baker 2f6348c53d Return to last viewed filter after search 4 months ago
renovate[bot] 566c22c17e
Update agp to v8.12.0 (#3764)
* Update agp to v8.12.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
Ihor Hordiichuk 2c33be700a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/uk/
4 months ago
renovate[bot] 7a24f43387
Update horologist to v0.7.15 (#3763)
* Update horologist to v0.7.15

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 370ac149d3
Update dependency androidx.test.ext:junit to v1.3.0 (#3762)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 4c851ce7f3
Update dependency androidx.test:core to v1.7.0 (#3759)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 7c78854663
Update dependency androidx.test:runner to v1.7.0 (#3760)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months ago
Ihor Hordiichuk d05730399d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/uk/
4 months ago
Xo c8f564d2d5 Translated using Weblate (Hebrew)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/he/
4 months ago
Alex Baker 65db4ab926 Remove shadow from date picker sheet 4 months ago
renovate[bot] 1476e7fb27
Update dependency androidx.navigation:navigation-compose to v2.9.3 (#3756)
* Update dependency androidx.navigation:navigation-compose to v2.9.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 2ee0939564
Update dependency androidx.wear:wear-input to v1.2.0-beta01 (#3757)
* Update dependency androidx.wear:wear-input to v1.2.0-beta01

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 4c530a5de3
Update dependency androidx.work:work-runtime-ktx to v2.10.3 (#3758)
* Update dependency androidx.work:work-runtime-ktx to v2.10.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
Alex Baker fcd62c6801 Update version and changelog 4 months ago
Alex Baker aedd29982a Fix test compilation 4 months ago
Alex Baker 3a37d6481e Synchronize list icons with CalDAV 4 months ago
Alex Baker c5f8583146 Simplify fetching calendars 4 months ago
Alex Baker 9d96bed5b3 Add helper method to check if list is read only 4 months ago
renovate[bot] 2f268c8c70
Update dependency com.google.apis:google-api-services-drive to v3-rev20250723-2.0.0 (#3749)
* Update dependency com.google.apis:google-api-services-drive to v3-rev20250723-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
renovate[bot] 130a29d7e3
Update dependency com.google.apis:google-api-services-drive to v3-rev20250717-2.0.0 (#3746)
* Update dependency com.google.apis:google-api-services-drive to v3-rev20250717-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
4 months ago
Emin Tufan Çetin dcb69394be Translated using Weblate (Turkish)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/tr/
4 months ago
renovate[bot] 2cf3438e07
Update dependency com.google.firebase:firebase-crashlytics-gradle to v3.0.5 (#3742)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
4 months ago
Frits van Bommel 06e9da41d6 Translated using Weblate (Dutch)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/nl/
4 months ago
Pierfrancesco Passerini 7b34e33c0e Translated using Weblate (Italian)
Currently translated at 99.6% (654 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
4 months ago
Florian Trayon be51651779 Translated using Weblate (French)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fr/
4 months ago
ColorfulRhino 627b05a575 Translated using Weblate (German)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
4 months ago
Sketch6580 8207f30c5f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
5 months ago
Priit Jõerüüt 6811677d21 Translated using Weblate (Estonian)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/et/
5 months ago
Fjuro 9d88c5b3a0 Translated using Weblate (Czech)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/cs/
5 months ago
109247019824 1f24a371fb Translated using Weblate (Bulgarian)
Currently translated at 100.0% (656 of 656 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/bg/
5 months ago
Pierfrancesco Passerini 877a2cd6a5 Translated using Weblate (Italian)
Currently translated at 99.6% (653 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
5 months ago
109247019824 299b5b4d21 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/
5 months ago
renovate[bot] e6320d42a7
Update dependency androidx.compose:compose-bom to v2025.07.00 (#3732)
* Update dependency androidx.compose:compose-bom to v2025.07.00

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 84c36a1a90
Update lifecycle to v2.9.2 (#3730)
* Update lifecycle to v2.9.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 73c0e38991
Update dagger.hilt to v2.57 (#3731)
* Update dagger.hilt to v2.57

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 773e822f14
Update dependency androidx.navigation:navigation-compose to v2.9.2 (#3727)
* Update dependency androidx.navigation:navigation-compose to v2.9.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 6f167b5ae0
Update dependency androidx.wear:wear-input to v1.2.0-alpha04 (#3728)
* Update dependency androidx.wear:wear-input to v1.2.0-alpha04

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] ba0cd26abc
Update dependency com.google.android.gms:play-services-oss-licenses to v17.2.1 (#3724)
* Update dependency com.google.android.gms:play-services-oss-licenses to v17.2.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 3450db4006
Update dependency ruby to v3.4.5 (#3725)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5 months ago
Alex Baker 7c2cf38788 Update version and changelog 5 months ago
Alex Baker e576a48eba Fix widget 'View more tasks' text color 5 months ago
renovate[bot] 803593a3a7
Update dependency com.microsoft.identity.client:msal to v6.2.0 (#3721)
* Update dependency com.microsoft.identity.client:msal to v6.2.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] c4fc7fbadb
Update dependency com.google.android.gms:play-services-oss-licenses to v17.2.0 (#3720)
* Update dependency com.google.android.gms:play-services-oss-licenses to v17.2.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
github-actions[bot] 75d53fb8ac Update dependency diffs 5 months ago
renovate[bot] 769802c10a
Update agp to v8.11.1 (#3718)
* Update agp to v8.11.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 14ff0086fa
Update dependency gradle to v8.14.3 (#3719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5 months ago
Alex Baker 761d4afeef Widget sort groups ascending by default 5 months ago
Alex Baker c7336589cd Limit widget task list size on Android 16+
Android 16 really slows down on large binder transactions
5 months ago
Alex Baker e30c583d5a Fix widget update race condition 5 months ago
Alex Baker 38527aef0a Update compileSdk to Android 16 5 months ago
Alex Baker c9cdc4d50f Log if "Don't keep activities" is enabled 5 months ago
renovate[bot] 80753f607c
Update dependency com.google.apis:google-api-services-drive to v3-rev20250701-2.0.0 (#3712)
* Update dependency com.google.apis:google-api-services-drive to v3-rev20250701-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
Milo Ivir 32cb067ffd 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/
5 months ago
தமிழ்நேரம் e93d0735d4 Translated using Weblate (Tamil)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ta/
5 months ago
renovate[bot] 103e7eaa60
Update dependency androidx.navigation:navigation-compose to v2.9.1 (#3709)
* Update dependency androidx.navigation:navigation-compose to v2.9.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
Nick Wick a490307251 Translated using Weblate (Swedish)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/sv/
5 months ago
Nick Wick 976df68671 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/
5 months ago
vale-decem 384f6e4604 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem b68439b0e7 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem 3611593307 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem c7a7384cf5 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem f89789dd10 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem 91da4bc661 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem 30abeba683 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem 8f567a153a Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem 2fbffa20cc Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem a071b05a71 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem 63dbb48d96 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem e2c65c06a1 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem 03b1d78feb Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem fe72301c55 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem ee40b72b02 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem 6181351db7 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
vale-decem 8263ab2935 Translated using Weblate (Serbian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sr/
5 months ago
Kachelkaiser 38dbbe379b Translated using Weblate (German)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
5 months ago
vale-decem 58d5eea978 Translated using Weblate (Serbian)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/sr/
5 months ago
vale-decem f902ff38b0 Translated using Weblate (Serbian)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/sr/
5 months ago
Alex Baker 7c970eec95 Update proguard 5 months ago
renovate[bot] 9ec448aedd
Update dependency fastlane to v2.228.0 (#3700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 2fb1bb4873
Update dependency com.google.firebase:firebase-bom to v33.16.0 (#3699)
* Update dependency com.google.firebase:firebase-bom to v33.16.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
Alex Baker c64e581fd4 Update proguard 5 months ago
renovate[bot] 36ec47e9bd
Update dependency gradle to v8.14.2 (#3695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 2942554ec8
Update room to v2.7.2 (#3697)
* Update room to v2.7.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] b6b624ce5b
Update plugin jetbrains-compose to v1.8.2 (#3696)
* Update plugin jetbrains-compose to v1.8.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 1267c803c6
Update agp to v8.11.0 (#3698)
* Update agp to v8.11.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 7a17943142
Update dependency com.google.gms:google-services to v4.4.3 (#3694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5 months ago
renovate[bot] d73b8496cb
Update dependency com.google.auth:google-auth-library-oauth2-http to v1.37.1 (#3693)
* Update dependency com.google.auth:google-auth-library-oauth2-http to v1.37.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] eda6eeaf62
Update dependency androidx.work:work-runtime-ktx to v2.10.2 (#3692)
* Update dependency androidx.work:work-runtime-ktx to v2.10.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 6d89f2cb02
Update dependency androidx.sqlite:sqlite-bundled to v2.5.2 (#3691)
* Update dependency androidx.sqlite:sqlite-bundled to v2.5.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 273fbf9153
Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.8.3 (#3689)
* Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.8.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] 2b1ad31f76
Update dependency androidx.compose:compose-bom to v2025.06.01 (#3688)
* Update dependency androidx.compose:compose-bom to v2025.06.01

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
5 months ago
renovate[bot] fdd1fc4989 Migrate config renovate.json 5 months ago
github-actions[bot] ff6a8ae0f1 Update dependency diffs 5 months ago
devn1x d1da6dc970 Update libs.versions.toml 5 months ago
devn1x a3dac4a397 Update dependency ical4android
Fixes #2870
5 months ago
devn1x 30060d8faf Update dependency ical4android
Fixes #2870
5 months ago
Alex Baker a299363fe8 New Compose color picker 5 months ago
大王叫我来巡山 36b20f47fd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
6 months ago
大王叫我来巡山 5b1aff00df Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/zh_Hans/
6 months ago
Anonymous f53aec3e8c Translated using Weblate (Slovak)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/sk/
6 months ago
renovate[bot] 079d7867f1
Update dependency co.touchlab:kermit to v2.0.6 (#3676)
* Update dependency co.touchlab:kermit to v2.0.6

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
pitroig 8b99e8feb2 Translated using Weblate (Catalan)
Currently translated at 64.2% (422 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ca/
6 months ago
Alex Baker 1a9b371bda Update version and changelog 6 months ago
Alex Baker dfc311ff31 Remove redundant string 6 months ago
Alex Baker cbcb812150 Create a local list if there are no lists 6 months ago
Alex Baker 70793f2433 Remove libera.chat links 6 months ago
Xo a198846902 Translated using Weblate (Hebrew)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/he/
6 months ago
Alex Baker 86a3b2b426 Don't post summaries when canNotify is false 6 months ago
Alex Baker bbac4da7d0 Additional debug info in application logs 6 months ago
Maria Eduarda Weiland Machado Bratti f4e0d519d7 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/
6 months ago
Alex Baker 9a584c851b Fix dynamic color setting 6 months ago
Alex Baker 704edaa0ab Fix Microsoft To Do serialization crash 6 months ago
Sina 71833adf21 Translated using Weblate (Persian)
Currently translated at 60.6% (20 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/fa/
6 months ago
renovate[bot] 4aad9bf00e
Update dependency com.google.firebase:firebase-crashlytics-gradle to v3.0.4 (#3660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6 months ago
Frits van Bommel d4b1a0dd09 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/
6 months ago
renovate[bot] 0f1508f59a
Update dependency com.google.auth:google-auth-library-oauth2-http to v1.37.0 (#3656)
* Update dependency com.google.auth:google-auth-library-oauth2-http to v1.37.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 266fe1281e
Update dependency androidx.wear.tiles:tiles-proto to v1.5.0 (#3655)
* Update dependency androidx.wear.tiles:tiles-proto to v1.5.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 7e99762814
Update lifecycle to v2.9.1 (#3653)
* Update lifecycle to v2.9.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 56ec24d2f9
Update dependency androidx.compose:compose-bom to v2025.06.00 (#3654)
* Update dependency androidx.compose:compose-bom to v2025.06.00

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] fad3ab6ce3
Update dependency androidx.fragment:fragment-compose to v1.8.8 (#3652)
* Update dependency androidx.fragment:fragment-compose to v1.8.8

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 522bd6e304
Update dependency androidx.appcompat:appcompat to v1.7.1 (#3651)
* Update dependency androidx.appcompat:appcompat to v1.7.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
109247019824 119572971d 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/
6 months ago
Nicklas 48270d6f2c 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/
6 months ago
M_Haruki 44d15556c6 Translated using Weblate (Japanese)
Currently translated at 99.8% (656 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ja/
6 months ago
Anael G. P 33611d12bd Translated using Weblate (Spanish)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/es/
6 months ago
Don Zouras caa5916ad7 Translated using Weblate (Esperanto)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
6 months ago
Emin Tufan Çetin 821e8e0ee4 Translated using Weblate (Turkish)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/tr/
6 months ago
Florian Trayon fe4bd73d62 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/
6 months ago
Anael G. P b0493fdd7d Translated using Weblate (Spanish)
Currently translated at 99.8% (656 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
6 months ago
Anonymous f330daa764 Translated using Weblate (Vietnamese)
Currently translated at 92.3% (607 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/vi/
6 months ago
Anonymous 017dd17021 Translated using Weblate (Croatian)
Currently translated at 98.7% (649 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hr/
6 months ago
Anonymous aa535981c3 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 95.4% (627 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hant/
6 months ago
Anonymous 6f39614b5b Translated using Weblate (Polish)
Currently translated at 91.6% (602 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pl/
6 months ago
Anonymous d33d87b6cb Translated using Weblate (Norwegian Bokmål)
Currently translated at 95.4% (627 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/nb_NO/
6 months ago
Anonymous a828d510e3 Translated using Weblate (Lithuanian)
Currently translated at 89.1% (586 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/lt/
6 months ago
Anonymous 898f84e3c8 Translated using Weblate (Finnish)
Currently translated at 96.0% (631 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/fi/
6 months ago
Yurt Page a1711fa0ea Translated using Weblate (Russian)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ru/
6 months ago
Yurt Page 542ba69870 Translated using Weblate (Russian)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/ru/
6 months ago
Alex Baker 01d07cccbd Fix case on "Default reminder" settings 6 months ago
renovate[bot] 3e8838bdb6
Update dependency com.google.auth:google-auth-library-oauth2-http to v1.36.0 (#3636)
* Update dependency com.google.auth:google-auth-library-oauth2-http to v1.36.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] ac4c610841
Update dependency com.google.protobuf:protoc to v4.31.1 (#3635)
* Update dependency com.google.protobuf:protoc to v4.31.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 6bea199a75
Update agp to v8.10.1 (#3634)
* Update agp to v8.10.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 1efb8c8ee0
Update grpc-java monorepo to v1.73.0 (#3631)
* Update grpc-java monorepo to v1.73.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
Igor Sorocean d2c23a79de Translated using Weblate (Romanian)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/ro/
6 months ago
Igor Sorocean 0091f80945 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/
6 months ago
renovate[bot] 5cb770e722
Update dependency ruby to v3.4.4 (#3206)
* Update dependency ruby to v3.4.4

* Fix fastlane

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex Baker <alex@tasks.org>
6 months ago
renovate[bot] 81759305c5
Update protobuf monorepo to v4.31.0 (#3616)
* Update protobuf monorepo to v4.31.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 1692a98d3d
Update mockito monorepo to v5.18.0 (#3615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 9174855f2f
Update dependency com.google.firebase:firebase-bom to v33.14.0 (#3613)
* Update dependency com.google.firebase:firebase-bom to v33.14.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 9632ea61d2
Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0 (#3614)
* Update dependency org.jetbrains.kotlinx:kotlinx-collections-immutable to v0.4.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] e2ca2a2251
Update dependency com.google.apis:google-api-services-tasks to v1-rev20250518-2.0.0 (#3611)
* Update dependency com.google.apis:google-api-services-tasks to v1-rev20250518-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] dad35aafd3
Update plugin jetbrains-compose to v1.8.1 (#3612)
* Update plugin jetbrains-compose to v1.8.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 0735cb5e1d
Update dependency gradle to v8.14.1 (#3550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 55c8ab6e3a
Update dependency androidx.fragment:fragment-compose to v1.8.7 (#3610)
* Update dependency androidx.fragment:fragment-compose to v1.8.7

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 5c4b345695
Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.8.2 (#3607)
* Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.8.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] 2c6b1644dc
Update dependency androidx.datastore:datastore-preferences to v1.1.7 (#3609)
* Update dependency androidx.datastore:datastore-preferences to v1.1.7

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
renovate[bot] efaa2cf472
Update dependency androidx.compose:compose-bom to v2025.05.01 (#3606)
* Update dependency androidx.compose:compose-bom to v2025.05.01

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
6 months ago
Alex Baker c4e160a21a Update version and changelog 6 months ago
Alex Baker 7d6c20aec0 Fix null pointer exception 6 months ago
renovate[bot] 3960b57242
Update dependency com.google.apis:google-api-services-drive to v3-rev20250511-2.0.0 (#3599)
* Update dependency com.google.apis:google-api-services-drive to v3-rev20250511-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Alex Baker 0e5e6e9b05 Update version and changelog 7 months ago
Alex Baker 147c9f44d2 Remove erroneously added contacts permission 7 months ago
Alex Baker ad7141a1c9 Fix wallpaper theme 7 months ago
abdelbasset jabrane 6a99e75115 Translated using Weblate (Arabic)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/ar/
7 months ago
Hugoren Martinako b4f9770344 Translated using Weblate (Spanish)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
7 months ago
Hugoren Martinako f6ffcf8397 Translated using Weblate (Catalan)
Currently translated at 61.3% (403 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ca/
7 months ago
109247019824 a23c541195 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/
7 months ago
renovate[bot] dea9783ce5
Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.8.1 (#3544)
* Update dependency androidx.compose.ui:ui-tooling-preview-android to v1.8.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] 93e7a16850
Update kotlin (#3594)
* Update kotlin

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] 760bd1d58b
Update plugin jetbrains-compose to v1.8.0 (#3586)
* Update plugin jetbrains-compose to v1.8.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Alex Baker e580ced066 Fix handling multiple shared attachments 7 months ago
Alex Baker 169140cc0b Allow video attachments 7 months ago
Alex Baker 8f5c41051b compose-bom 2025.05.00 7 months ago
renovate[bot] a2098c1876
Update dependency androidx.datastore:datastore-preferences to v1.1.6 (#3539)
* Update dependency androidx.datastore:datastore-preferences to v1.1.6

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Alex Baker d446e009b4 Fix lint 7 months ago
Alex Baker 35161972c1 Revert "Update kotlin to v2.1.21 (#3593)"
This reverts commit c996462e32.
7 months ago
Alex Baker 6a8a9dec80 AGP 8.10 7 months ago
Alex Baker 627ada6679 Replace deprecated method call 7 months ago
renovate[bot] c996462e32
Update kotlin to v2.1.21 (#3593)
* Update kotlin to v2.1.21

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] cd5a89960d
Update dependency com.google.auth:google-auth-library-oauth2-http to v1.35.0 (#3592)
* Update dependency com.google.auth:google-auth-library-oauth2-http to v1.35.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
cat 805b7d23df Translated using Weblate (Danish)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/da/
7 months ago
Alex Baker a4ca8b28aa msal for Google Play, AppAuth for F-Droid 7 months ago
renovate[bot] a519a06c3b
Update dependency com.google.apis:google-api-services-drive to v3-rev20250506-2.0.0 (#3587)
* Update dependency com.google.apis:google-api-services-drive to v3-rev20250506-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] c22384d789
Update lifecycle to v2.9.0 (#3585)
* Update lifecycle to v2.9.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] 247c286e61
Update dependency androidx.navigation:navigation-compose to v2.9.0 (#3583)
* Update dependency androidx.navigation:navigation-compose to v2.9.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] 15c7bc2fc0
Update dependency androidx.sqlite:sqlite-bundled to v2.5.1 (#3582)
* Update dependency androidx.sqlite:sqlite-bundled to v2.5.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Kaci 08fa635ce9 Translated using Weblate (Hungarian)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/hu/
7 months ago
Priit Jõerüüt 2946133eb2 Translated using Weblate (Estonian)
Currently translated at 100.0% (657 of 657 strings)

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

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/eo/
7 months ago
renovate[bot] 83e8044bc5
Update dependency com.microsoft.identity.client:msal to v6.0.1 (#3574)
* Update dependency com.microsoft.identity.client:msal to v6.0.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Don Zouras 892beb83f7 Translated using Weblate (Esperanto)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/eo/
7 months ago
Ihor Hordiichuk a623a7bd97 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/uk/
7 months ago
Emin Tufan Çetin 98ae90a6ad Translated using Weblate (Turkish)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/tr/
7 months ago
renovate[bot] 9b1078dc16
Update ktor monorepo to v3.1.3 (#3570)
* Update ktor monorepo to v3.1.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] cb220654cf
Update dependency fastlane to v2.227.2 (#3569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
7 months ago
Frits van Bommel 65533d3675 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
Pierfrancesco Passerini cdb11e631f Translated using Weblate (Italian)
Currently translated at 99.8% (656 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/it/
7 months ago
Priit Jõerüüt 1fd0fe232a Translated using Weblate (Estonian)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/et/
7 months ago
Fjuro ec05fed425 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/
7 months ago
renovate[bot] 1d7cc3794a
Update dependency com.google.auth:google-auth-library-oauth2-http to v1.34.0 (#3566)
* Update dependency com.google.auth:google-auth-library-oauth2-http to v1.34.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Alex Baker 3d3c16b297 Update version and changelog 7 months ago
Alex Baker deefb20481 Fix activity finishing itself 7 months ago
Alex Baker 83bc9798d6 Automatically set default Microsoft To Do list 7 months ago
Alex Baker 3d04f93aae Use lazy initializers for firebase 7 months ago
Alex Baker 15aada7f7f Limit drawer headers to 1 line 7 months ago
Alex Baker 20e1b802cd Reduce db query log level 7 months ago
Alex Baker bb4db1e63d Fix link in changelog 7 months ago
Sketch6580 6a5e8896b8 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/zh_Hans/
7 months ago
Jose Delvani e469a2ea97 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/pt_BR/
7 months ago
Florian Trayon bfb5529b73 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
Kachelkaiser 89101a22d4 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
abdelbasset jabrane 221efb9bef Translated using Weblate (Arabic)
Currently translated at 100.0% (657 of 657 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ar/
7 months ago
Abdullhakim Sami Alshanqiti 2757821f4e Translated using Weblate (Arabic)
Currently translated at 97.8% (641 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ar/
7 months ago
abdelbasset jabrane ceb7da1e2c Translated using Weblate (Arabic)
Currently translated at 97.7% (640 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ar/
7 months ago
abdelbasset jabrane d5f737b6ac Translated using Weblate (Arabic)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/ar/
7 months ago
Alex Baker ff33989020 Update version and changelog 7 months ago
Alex Baker 4c01ab2e66 Import backup file in two passes
Import non-task data before loading tasks
7 months ago
Alex Baker 93674075cb Fix crash on search back handler 7 months ago
Alex Baker 6fe8175012 Prevent renaming or deleting the default list 7 months ago
Alex Baker d72b0f352b Fix crash when creating new list 7 months ago
Maksim_220 Кабанов 6b92cbee44 Translated using Weblate (Russian)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/ru/
7 months ago
renovate[bot] de2aca2877
chore(deps): update plugin ksp to v2.1.20-2.0.1 (#3559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
7 months ago
renovate[bot] 1616d9903d
fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20250427-2.0.0 (#3561)
* fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20250427-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Alex Baker 5f1ebd528d Don't create microsoft service on main 7 months ago
Alex Baker 6091410bcd Update version and changelog 7 months ago
Alex Baker 44eac87c9c Fix tests 7 months ago
Alex Baker ac9cdb7120 Initiate sync after signing in to Microsoft To Do 7 months ago
Alex Baker 0dea530c50 Prompt for sync at install time 7 months ago
renovate[bot] 267ebfe86e
fix(deps): update dependency com.google.accompanist:accompanist-permissions to v0.37.3 (#3552)
* fix(deps): update dependency com.google.accompanist:accompanist-permissions to v0.37.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Alex Baker be3ca88f46 Add microsoft_config for f-droid, fix proguard 7 months ago
Alex Baker eaab2196e7 Fix generic build and remove some non-free deps 7 months ago
Alex Baker c3e10bde94 Add support for microsoft organization accounts 7 months ago
Alex Baker 68d7a02db8 Remove ManageSpaceActivity 7 months ago
தமிழ்நேரம் e48839b314 Translated using Weblate (Tamil)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ta/
7 months ago
renovate[bot] 0296008f8a
fix(deps): update dependency com.google.firebase:firebase-bom to v33.13.0 (#3548)
* fix(deps): update dependency com.google.firebase:firebase-bom to v33.13.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Jose Riha 7c918bbb84 Translated using Weblate (Slovak)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/sk/
7 months ago
Alex Baker 202af2d451 Launch new list activities from HomeScreen 7 months ago
Alex Baker a779dc331a Don't check for xml extension for backups 7 months ago
renovate[bot] 8480f03a96
fix(deps): update dependency com.google.apis:google-api-services-tasks to v1-rev20250415-2.0.0 (#3545)
* fix(deps): update dependency com.google.apis:google-api-services-tasks to v1-rev20250415-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] c18296e7d0
fix(deps): update dependency androidx.work:work-runtime-ktx to v2.10.1 (#3541)
* fix(deps): update dependency androidx.work:work-runtime-ktx to v2.10.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] 520fd0cebe
fix(deps): update dependency io.grpc:grpc-kotlin-stub to v1.4.3 (#3543)
* fix(deps): update dependency io.grpc:grpc-kotlin-stub to v1.4.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] bd555f4003
fix(deps): update room to v2.7.1 (#3542)
* fix(deps): update room to v2.7.1

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
renovate[bot] b4b6abac19
fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20250329-2.0.0 (#3537)
* fix(deps): update dependency com.google.apis:google-api-services-drive to v3-rev20250329-2.0.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
7 months ago
Alex Baker e578263149 Remove drop shadows to match icons 8 months ago
renovate[bot] b6989ba9a0
fix(deps): update agp to v8.9.2 (#3535)
* fix(deps): update agp to v8.9.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
Alex Baker 93d4eab92f Fix viewmodel 8 months ago
Alex Baker 8abf1f0342 Add splash screen 8 months ago
Alex Baker d3fed98e64 Add navigation and HomeScreenDestination 8 months ago
தமிழ்நேரம் e8b8fc0c87 Translated using Weblate (Tamil)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/ta/
8 months ago
Fjuro 939bcef641 Translated using Weblate (Czech)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/cs/
8 months ago
renovate[bot] d49ee12271
fix(deps): update dagger.hilt to v2.56.2 (#3527)
* fix(deps): update dagger.hilt to v2.56.2

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
DiamondtipDR 3a05d410b0 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/
8 months ago
renovate[bot] 591998b9fd
chore(deps): update plugin jetbrains-compose to v1.7.3 (#3196)
* chore(deps): update plugin jetbrains-compose to v1.7.3

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
renovate[bot] 5d8b39d8f1
chore(deps): update plugin redacted to v1.13.0 (#3140)
* chore(deps): update plugin redacted to v1.13.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
renovate[bot] c445f0dff0
fix(deps): update mockito monorepo to v5.17.0 (#3485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 months ago
renovate[bot] 20e696d65d
fix(deps): update kotlin (#3430)
* fix(deps): update kotlin

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
codokie bce545944f
Fix text input alignment/direction (#3489)
* Revert text alignment change

* Set text direction of input fields based on content
8 months ago
leo 72aaf43db5 fix: Back Button closes app after using search #3426 8 months ago
renovate[bot] 966a529a51
Update grpc-java monorepo to v1.72.0 (#3521)
* Update grpc-java monorepo to v1.72.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
Alex Baker 08bf839e95 Fix crash in Intent.flagsToString 8 months ago
Alex Baker 286d031fca Hide start date = due date for OpenTasks sync 8 months ago
renovate[bot] 428d04eb46
Update dependency com.google.android.gms:play-services-maps to v19.2.0 (#3518)
* Update dependency com.google.android.gms:play-services-maps to v19.2.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
Alex Baker fc6f09f097 Use system language picker on Android 33+ 8 months ago
Maria Eduarda Weiland Machado Bratti dafc677374 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/
8 months ago
min7-i 00bf17c9ed Translated using Weblate (German)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/de/
8 months ago
Maria Eduarda Weiland Machado Bratti f622209e97 Translated using Weblate (Portuguese)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/pt/
8 months ago
codokie 6d03cf6a6a
Automirrored icons fix (#3499)
* Fix arrow direction in RTL

* Fix help icon when locale is Hebrew

* Fix help icon in other RTL locales
8 months ago
Jay Tromp cd729bb04c Translated using Weblate (Dutch)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/nl/
8 months ago
Nucl3arSnake 7d32ada1e4 Translated using Weblate (Spanish)
Currently translated at 99.8% (654 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/es/
8 months ago
Alex Baker 526107ce1d Fix start date chip when grouping by start date 8 months ago
Hady 858a5475ad Translated using Weblate (Russian)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/ru/
8 months ago
dedakir923 0c9d2d724a 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/
8 months ago
renovate[bot] 446fdd817f
Update dependency androidx.compose:compose-bom to v2025.04.00 (#3507)
* Update dependency androidx.compose:compose-bom to v2025.04.00

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
renovate[bot] b833c0cc5d
Update room to v2.7.0 (#3506)
* Update room to v2.7.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
renovate[bot] 5499a74dcc
Update dependency androidx.sqlite:sqlite-bundled to v2.5.0 (#3505)
* Update dependency androidx.sqlite:sqlite-bundled to v2.5.0

* Update dependency diffs

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
8 months ago
Alex Baker 6c3185da50 Add missing locales 8 months ago
sobeitnow0 6f0a6787fd 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/
8 months ago
sobeitnow0 848aad76d6 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/
8 months ago
renovate[bot] 1ace1bc618
Update dependency fastlane to v2.227.1 (#3502)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 months ago
renovate[bot] cda3e91933
Update dependency com.android.tools:desugar_jdk_libs to v2.1.5 (#3198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
8 months ago
Xo 2c1246ea5c Translated using Weblate (Hebrew)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/he/
8 months ago
Xo 5731ca2579 Translated using Weblate (Hebrew)
Currently translated at 100.0% (655 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/he/
8 months ago
Xo b04b16e24e Translated using Weblate (Hebrew)
Currently translated at 100.0% (33 of 33 strings)

Translation: Tasks.org/Desktop
Translate-URL: https://hosted.weblate.org/projects/tasks/multiplatform/he/
8 months ago
Xo 53e43b58cd Translated using Weblate (Hebrew)
Currently translated at 99.6% (653 of 655 strings)

Translation: Tasks.org/Android
Translate-URL: https://hosted.weblate.org/projects/tasks/android/he/
8 months ago
Alex Baker 0c7bc22eb9 Merge tag '14.6.2' 8 months ago
Alex Baker 35e340c936 Reapply "Set minSdk to 26 (Android 8)"
This reverts commit 660b6c8df5.
8 months ago

@ -20,16 +20,16 @@ jobs:
- name: Decode Keystore
run: |
echo ${{ secrets.KEY_STORE }} | base64 -di > "${RUNNER_TEMP}"/keystore.jks
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Set up JDK 17
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
@ -44,7 +44,7 @@ jobs:
GOOGLE_KEY: ${{ secrets.GOOGLE_KEY }}
run: bundle exec fastlane bundle
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: release
path: |

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

@ -22,15 +22,15 @@ jobs:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: ${{ github.head_ref }}
- name: Set up JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '17'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
needs: [ bundle ]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Fastlane key
run: |
echo "$FASTLANE" > ./fastlane.json
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v6
with:
name: release
path: .

@ -1 +1 @@
3.3.6
3.4.7

@ -1,3 +1,179 @@
### 14.8.4 (2025-11-09)
* Fix flashing widgets [#3902](https://github.com/tasks/tasks/issues/3902)
* Fix random reminder scheduling
* Fix random reminders firing immediately on recurring tasks [#3904](https://github.com/tasks/tasks/issues/3904)
* Fix deadlock when adding new task
* Fix crash in settings when backup location unavailable [#3989](https://github.com/tasks/tasks/issues/3989)
* Fix Hebrew and Indonesian support [#3928](https://github.com/tasks/tasks/issues/3928)
* Update translations
* Bosnian - @hasak
* Finnish - @pHamala
* Indonesian - @erigmac
* Japanese - @array
* Romanian - @ygorigor
### 14.8.3 (2025-09-16)
* Fix crash on Android 10 and below
### 14.8.2 (2025-09-14)
* Fix blank widgets on Android 16 QPR1 [#3847](https://github.com/tasks/tasks/issues/3847)
* Fix all-day calendar events [#1534](https://github.com/tasks/tasks/issues/1534)
* Fix alarm synchronization [#3859](https://github.com/tasks/tasks/issues/3859)
* Fix sync failure when migrating data from EteSync to CalDAV [#3869](https://github.com/tasks/tasks/issues/3869)
* Fix removing values from Microsoft To Do [#3862](https://github.com/tasks/tasks/issues/3862)
* Fix share invites for Nextcloud [#2386](https://github.com/tasks/tasks/issues/2386)
* Fix failure to delete source data when moving to Google Tasks [#3867](https://github.com/tasks/tasks/issues/3867)
* Fix crash when clearing completed while grouping by lists
* Update translations
* Croatian - @milotype
* Dutch - @fvbommel
* German - @MisterTechnik
* Italian - @glemco
* Serbian - @vale-decem
### 14.8.1 (2025-08-24)
* System bar scrim improvements
* Recover from Google Task 'Bad request' errors
* Improve layout on Z Folds
* Crash fixes
* Update translations
* Brazilian Portuguese - odnankenobi
* Catalan - @Crashillo, @ferranpujolcamins
* Danish - ERYpTION
* Esperanto - Don Zouras
* Galician - @Crashillo, @delthia
* Hungarian - @Antmajgra, @gthrepwood
* Italian - @ppasserini
* Korean - Jiho Min
* Polish - @Antmajgra
* Portuguese - @Crashillo
* Russian - Алексей Ежков
* Spanish - @Crashillo
### 14.8 (2025-08-02)
* Synchronize **list** icons for Tasks.org and CalDAV accounts
* Does not apply to Microsoft To Do, Google Tasks, DAVx5, EteSync, or DecSync
CC accounts
* Does not apply to tags or filters
* CalDAV server must support extensible properties, e.g. Nextcloud or sabre/dav
* Target Android 15
* Return to previous view after searching
* Remove shadow from date picker sheet
* Fix updating list names and colors for Tasks.org and CalDAV accounts
* Update translations
* Bulgarian - 109247019824
* Chinese (Simplified) - Sketch6580
* Czech - @Fjuro
* Dutch - @fvbommel
* Estonian - Priit Jõerüüt
* French - @FlorianLeChat
* German - @Colorful Rhino
* Hebrew - Xo
* Italian - @ppasserini
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 14.7.4 (2025-07-12)
* @devn1x: Fix escaping quotes in iCalendar [#3645](https://github.com/tasks/tasks/pull/3645)
* Limit widget to 25 items on Android 16+
* Android 16 nerfed widget performance 😢
* Fix bug when reconfiguring widget
* Fix default widget group sort order
* Update translations
* Catalan - pitroig
* Chinese (Simplified) - 大王叫我来巡山
* Croatian - @milotype
* German - @Kachelkaiser
* Serbian - @vale-decem
* Swedish - Nick Wick
* Tamil - @TamilNeram
### 14.7.3 (2025-06-13)
* Fix dynamic color
* Fix Microsoft To Do sync failure
* Fix crash after deleting last list
* Fix notifications when 'Alarms & reminders' not allowed
* Update translations
* Bulgarian - 109247019824
* Dutch - @fvbommel
* Esperanto - Don Zouras
* French - @FlorianLeChat
* Hebrew - Xo
* Japanese - M_Haruki
* Persian - @theuser17
* Portuguese - @nero-bratti
* Romanian - @ygorigor
* Russian - @yurtpage
* Spanish - @orionn333
* Swedish - @Nicklasfox
* Turkish - @emintufan
### 14.7.2 (2025-05-23)
* Remove Microsoft Authentication Library from F-Droid builds [#3581](https://github.com/tasks/tasks/issues/3581)
* Remove contacts permission added by Microsoft Authentication Library
* Enable video attachments
* Fix wallpaper theme
* Fix handling multiple attachments
* Update translations
* Arabic - abdelbasset jabrane
* Bulgarian - 109247019824
* Catalan - @Crashillo
* Czech - @Fjuro
* Danish - @catsnote
* Dutch - @fvbommel
* Esperanto - Don Zouras
* Estonian - Priit Jõerüüt
* Hungarian - Kaci
* Italian - @ppasserini
* Turkish - @emintufan
* Ukrainian - @IhorHordiichuk
### 14.7.1 (2025-05-04)
* Fix app closing itself automatically [#3366](https://github.com/tasks/tasks/issues/3366)
* Automatically set default list when connecting Microsoft To Do account
* Update translations
* Arabic - abdelbasset jabrane, @kemo-1
* Brazilian Portuguese - Jose Delvani
* Chinese (Simplified) - Sketch6580
* French - @FlorianLeChat
* German - @Kachelkaiser
### 14.7 (2025-05-03)
* Add support for Microsoft To Do work & school accounts [#3267](https://github.com/tasks/tasks/issues/3267)
* Add ability to rename or delete local account
* Prompt to sign in or import backup on first launch
* @BeaterGhalio: Fix back button closing app after search [#3426](https://github.com/tasks/tasks/issues/3426)
* @codokie: Automirrored icons fix [#3499](https://github.com/tasks/tasks/pull/3499)
* @codokie: Fix ltr-rtl alignment for text input [#3489](https://github.com/tasks/tasks/pull/3489)
* Use system language picker on Android 33+
* Don't show 'due date' as a start date option for DAVx5, EteSync, DecSync CC [#1558](https://github.com/tasks/tasks/issues/1558)
* Prevent attempts to delete or rename Microsoft To Do default list
* Don't handle system 'Clear storage' button
* Update minimum Android version to 8
* Fix backup import dropping tags [#3556](https://github.com/tasks/tasks/issues/3556)
* Fix start date chip when grouping by start date [#3509](https://github.com/tasks/tasks/issues/3509)
* Update translations
* Brazilian Portuguese - @sobeitnow0, dedakir923
* Czech - @Fjuro
* Dutch - Jay Tromp
* German - min7-i
* Hebrew - Xo
* Portuguese - @wm-pucrs
* Russian - @hady-exc, Maksim_220 Кабанов
* Slovak - @jose1711
* Spanish - Nucl3arSnake, @diamondtipdr
* Tamil - @TamilNeram
### 14.6.2 (2025-04-06)
* Show error indicators if 'When started' or 'When due' reminders are used

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

@ -5,29 +5,31 @@ GEM
base64
nkf
rexml
abbrev (0.1.2)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.2)
aws-partitions (1.1067.0)
aws-sdk-core (3.220.1)
aws-eventstream (1.4.0)
aws-partitions (1.1122.0)
aws-sdk-core (3.226.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.99.0)
aws-sdk-core (~> 3, >= 3.216.0)
logger
aws-sdk-kms (1.106.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.182.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-s3 (1.191.0)
aws-sdk-core (~> 3, >= 3.225.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
base64 (0.3.0)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
@ -56,10 +58,10 @@ GEM
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.1.0)
faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
@ -69,7 +71,7 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
fastlane (2.227.0)
fastlane (2.228.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@ -109,7 +111,7 @@ GEM
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.4.0)
xcpretty (~> 0.4.1)
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
fastlane-sirp (1.0.0)
sysrandom (~> 1.0)
@ -156,22 +158,23 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
json (2.10.2)
jwt (2.10.1)
json (2.12.2)
jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
naturally (2.2.1)
naturally (2.3.0)
nkf (0.2.0)
optparse (0.6.0)
os (1.1.4)
plist (3.7.2)
public_suffix (5.1.1)
rake (13.2.1)
public_suffix (6.0.2)
rake (13.3.0)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@ -182,7 +185,7 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
signet (0.19.0)
signet (0.20.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
jwt (>= 1.5, < 3.0)
@ -209,7 +212,7 @@ GEM
colored2 (~> 3.1)
nanaimo (~> 0.4.0)
rexml (>= 3.3.6, < 4.0)
xcpretty (0.4.0)
xcpretty (0.4.1)
rouge (~> 3.28.0)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
@ -218,7 +221,8 @@ PLATFORMS
ruby
DEPENDENCIES
abbrev
fastlane
BUNDLED WITH
2.2.32
2.6.9

@ -15,7 +15,7 @@ Please visit [tasks.org](https://tasks.org) for end user documentation and suppo
[![PayPal donate button](https://img.shields.io/badge/paypal-donate-yellow.svg?logo=paypal)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=alex@tasks.org)
[![Liberapay donate button](https://img.shields.io/liberapay/receives/tasks.svg?logo=liberapay)](https://liberapay.com/tasks/donate)
[![build](https://github.com/tasks/tasks/actions/workflows/bundle.yml/badge.svg)](https://github.com/tasks/tasks/actions/workflows/bundle.yml) [![weblate](https://hosted.weblate.org/widgets/tasks/-/android/svg-badge.svg)](https://hosted.weblate.org/engage/tasks/?utm_source=widget) [![codebeat badge](https://codebeat.co/badges/07924fca-2f18-4eff-99a3-120ec5ac2d5f)](https://codebeat.co/projects/github-com-tasks-tasks-main)
[![build](https://github.com/tasks/tasks/actions/workflows/bundle.yml/badge.svg)](https://github.com/tasks/tasks/actions/workflows/bundle.yml) [![weblate](https://hosted.weblate.org/widgets/tasks/-/android/svg-badge.svg)](https://hosted.weblate.org/engage/tasks/?utm_source=widget)
### Contributing
@ -23,8 +23,6 @@ Contributions are always welcome! Whether translations, code changes, bug report
### Communication
Join the #tasks channel on Libera Chat to chat with the Tasks team and other people. [Link to webchat](https://web.libera.chat/#tasks)
You can also use [GitHub Discussions](https://github.com/tasks/tasks/discussions).
You can submit questions to [GitHub Discussions](https://github.com/tasks/tasks/discussions).
If you have a suggestion or want to report a bug, please see [CONTRIBUTING.md](CONTRIBUTING.md).

@ -154,6 +154,7 @@ dependencies {
implementation(projects.data)
implementation(projects.kmp)
implementation(projects.icons)
implementation(libs.androidx.navigation)
implementation(libs.androidx.adaptive.navigation.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bitfire.dav4jvm) {
@ -180,6 +181,7 @@ dependencies {
implementation(libs.androidx.hilt.navigation)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.datastore)
implementation(libs.androidx.fragment.compose)
implementation(libs.androidx.lifecycle.runtime)
@ -266,6 +268,9 @@ dependencies {
googleplayImplementation(libs.horologist.datalayer.grpc)
googleplayImplementation(libs.horologist.datalayer.core)
googleplayImplementation(libs.play.services.wearable)
googleplayImplementation(libs.microsoft.authentication) {
exclude("com.microsoft.device.display", "display-mask")
}
googleplayImplementation(projects.wearDatalayer)
androidTestImplementation(libs.dagger.hilt.testing)

10
app/proguard.pro vendored

@ -26,6 +26,8 @@
-dontwarn net.fortuna.ical4j.model.**
-dontwarn org.codehaus.groovy.**
-dontwarn org.apache.log4j.** # ignore warnings from log4j dependency
-dontwarn com.github.erosb.jsonsKema.** # ical4android
-dontwarn org.jparsec.** # ical4android
-keep class net.fortuna.ical4j.** { *; } # keep all model classes (properties/factories, created at runtime)
-keep class at.bitfire.** { *; } # all DAVdroid code is required
@ -51,4 +53,12 @@
# material icons
-keep class androidx.compose.material.icons.outlined.** { *; }
# microsoft authentication
-dontwarn com.microsoft.device.display.DisplayMask
-dontwarn com.google.android.libraries.identity.**
-dontwarn edu.umd.cs.findbugs.annotations.**
-dontwarn com.google.crypto.tink.subtle.**
-dontwarn net.jcip.annotations.**
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { <fields>; }

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

@ -56,7 +56,7 @@ class TaskMoverTest : InjectingTestCase() {
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(1)
val deleted = googleTaskDao.getDeletedByTaskId(1, "account1")
assertEquals(1, deleted.size.toLong())
assertEquals(1, deleted[0].task)
assertTrue(deleted[0].deleted > 0)
@ -71,7 +71,7 @@ class TaskMoverTest : InjectingTestCase() {
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
googleTaskDao.insert(newCaldavTask(with(TASK, 2), with(CALENDAR, "1")))
moveToGoogleTasks("2", 1)
val deleted = googleTaskDao.getDeletedByTaskId(2)
val deleted = googleTaskDao.getDeletedByTaskId(2, "account1")
assertEquals(1, deleted.size.toLong())
assertEquals(2, deleted[0].task)
assertTrue(deleted[0].deleted > 0)
@ -249,7 +249,7 @@ class TaskMoverTest : InjectingTestCase() {
createTasks(1)
googleTaskDao.insert(newCaldavTask(with(TASK, 1), with(CALENDAR, "1")))
moveToGoogleTasks("1", 1)
assertTrue(googleTaskDao.getDeletedByTaskId(1).isEmpty())
assertTrue(googleTaskDao.getDeletedByTaskId(1, "account1").isEmpty())
assertEquals(1, googleTaskDao.getAllByTaskId(1).size.toLong())
}

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

@ -1,13 +1,13 @@
package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.data.UUIDHelper
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_NEXTCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN
@ -36,7 +36,7 @@ class ServerDetectionTest : CaldavTest() {
sync()
assertEquals(SERVER_OWNCLOUD, loadAccount().serverType)
assertEquals(SERVER_NEXTCLOUD, loadAccount().serverType)
}
@Test

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

@ -9,8 +9,8 @@ import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.CaldavDao.Companion.LOCAL
import org.tasks.data.dao.DeletionDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavTask
import org.tasks.date.DateTimeUtils.newDateTime
@ -62,7 +62,8 @@ class DeletionDaoTests : InjectingTestCase() {
fun purgeDeletedLocalTask() = runBlocking {
val task = newTask(with(DELETION_TIME, newDateTime()))
taskDao.createNew(task)
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = LOCAL))
caldavDao.insert(CaldavAccount(uuid = "abcd", accountType = CaldavAccount.TYPE_LOCAL))
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = "abcd"))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
deletionDao.purgeDeleted()
@ -74,7 +75,8 @@ class DeletionDaoTests : InjectingTestCase() {
fun dontPurgeActiveTasks() = runBlocking {
val task = newTask()
taskDao.createNew(task)
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = LOCAL))
caldavDao.insert(CaldavAccount(uuid = "abcd", accountType = CaldavAccount.TYPE_LOCAL))
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = "abcd"))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
deletionDao.purgeDeleted()

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

@ -12,13 +12,16 @@ import com.todoroo.astrid.timers.TimerPlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.tasks.calendars.CalendarEventProvider
import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.LocationDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.dao.UserActivityDao
import org.tasks.data.db.Database
import org.tasks.data.entity.Task
import org.tasks.data.newLocalAccount
import org.tasks.injection.InjectingTestCase
import org.tasks.location.GeofenceApi
import org.tasks.preferences.DefaultFilterProvider
@ -44,9 +47,18 @@ open class BaseTaskEditViewModelTest : InjectingTestCase() {
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var userActivityDao: UserActivityDao
@Inject lateinit var caldavDao: CaldavDao
protected lateinit var viewModel: TaskEditViewModel
@Before
override fun setUp() {
runBlocking {
super.setUp()
caldavDao.newLocalAccount()
}
}
protected fun setup(task: Task) = runBlocking {
viewModel = TaskEditViewModel(
context,

@ -3,7 +3,6 @@ package org.tasks
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo
import com.todoroo.andlib.utility.AndroidUtilities.atLeastQ
import leakcanary.AppWatcher
import org.tasks.logging.FileLogger
@ -37,9 +36,7 @@ class BuildSetup @Inject constructor(
.detectLeakedClosableObjects()
.detectFileUriExposure()
.penaltyLog()
if (atLeastOreo()) {
builder.detectContentUriWithoutPermission()
}
.detectContentUriWithoutPermission()
if (atLeastQ()) {
builder
.detectCredentialProtectedWhileLocked()

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

@ -15,4 +15,5 @@
<string name="debug_force_restart">Restart app</string>
<string name="debug_clear_hints">Clear hints</string>
<string name="google_oauth_scheme">com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf</string>
<string name="microsoft_oauth_path">/8wnYBRqh5nnQgFzbIXfxXSs41xE=</string>
</resources>

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

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

@ -39,8 +39,10 @@ import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_MICROSOFT
import org.tasks.http.HttpClientFactory
import org.tasks.jobs.WorkManager
import org.tasks.preferences.fragments.TasksAccountViewModel.Companion.getStringOrNull
import org.tasks.security.KeyStoreEncryption
import org.tasks.sync.SyncAdapters
import org.tasks.sync.microsoft.requestTokenExchange
import javax.inject.Inject
@ -51,6 +53,8 @@ class MicrosoftAuthenticationActivity : ComponentActivity() {
@Inject lateinit var encryption: KeyStoreEncryption
@Inject lateinit var httpClientFactory: HttpClientFactory
@Inject lateinit var firebase: Firebase
@Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var workManager: WorkManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -93,6 +97,8 @@ class MicrosoftAuthenticationActivity : ComponentActivity() {
R.string.param_type to Constants.SYNC_TYPE_MICROSOFT
)
}
syncAdapters.sync(true)
workManager.updateBackgroundSync()
finish()
} else {
error(ex?.message ?: "Token exchange failed")
@ -149,4 +155,3 @@ class MicrosoftAuthenticationActivity : ComponentActivity() {
const val EXTRA_SERVICE_DISCOVERY = "extra_service_discovery"
}
}

@ -1,24 +1,14 @@
package org.tasks.sync.microsoft
import android.content.Context
import net.openid.appauth.*
import org.tasks.auth.IdentityProvider
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService
import net.openid.appauth.TokenRequest
import net.openid.appauth.TokenResponse
import kotlin.coroutines.suspendCoroutine
suspend fun IdentityProvider.retrieveConfig(): AuthorizationServiceConfiguration {
return suspendCoroutine { cont ->
AuthorizationServiceConfiguration.fetchFromUrl(discoveryEndpoint) { serviceConfiguration, ex ->
cont.resumeWith(
when {
ex != null -> Result.failure(ex)
serviceConfiguration != null -> Result.success(serviceConfiguration)
else -> Result.failure(IllegalStateException())
}
)
}
}
}
suspend fun Context.requestTokenRefresh(state: AuthState) =
requestToken(state.createTokenRefreshRequest())

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

@ -53,6 +53,20 @@
</intent-filter>
</service>
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${applicationId}"
android:path="@string/microsoft_oauth_path"
android:scheme="msauth" />
</intent-filter>
</activity>
</application>
</manifest>

@ -19,12 +19,42 @@ import javax.inject.Singleton
@Singleton
class Firebase @Inject constructor(
@param:ApplicationContext val context: Context,
@ApplicationContext private val context: Context,
private val preferences: Preferences
) {
private var crashlytics: FirebaseCrashlytics? = null
private var analytics: FirebaseAnalytics? = null
private var remoteConfig: FirebaseRemoteConfig? = null
private val crashlytics by lazy {
if (preferences.isTrackingEnabled) {
FirebaseCrashlytics.getInstance().apply {
setCrashlyticsCollectionEnabled(true)
}
} else {
null
}
}
private val analytics by lazy {
if (preferences.isTrackingEnabled) {
FirebaseAnalytics.getInstance(context).apply {
setAnalyticsCollectionEnabled(true)
}
} else {
null
}
}
private val remoteConfig by lazy {
if (preferences.isTrackingEnabled) {
FirebaseRemoteConfig.getInstance().apply {
setConfigSettingsAsync(remoteConfigSettings {
minimumFetchIntervalInSeconds =
TimeUnit.HOURS.toSeconds(WorkManager.REMOTE_CONFIG_INTERVAL_HOURS)
})
setDefaultsAsync(R.xml.remote_config_defaults)
}
} else {
null
}
}
fun reportException(t: Throwable) {
Timber.e(t)
@ -79,22 +109,4 @@ class Firebase @Inject constructor(
private fun days(key: String, default: Long): Long =
TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default)
init {
if (preferences.isTrackingEnabled) {
analytics = FirebaseAnalytics.getInstance(context).apply {
setAnalyticsCollectionEnabled(true)
}
crashlytics = FirebaseCrashlytics.getInstance().apply {
setCrashlyticsCollectionEnabled(true)
}
remoteConfig = FirebaseRemoteConfig.getInstance().apply {
setConfigSettingsAsync(remoteConfigSettings {
minimumFetchIntervalInSeconds =
TimeUnit.HOURS.toSeconds(WorkManager.REMOTE_CONFIG_INTERVAL_HOURS)
})
setDefaultsAsync(R.xml.remote_config_defaults)
}
}
}
}

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

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

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

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

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

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

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

@ -66,13 +66,24 @@
<!-- **************************************** -->
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" android:maxSdkVersion="25"/>
<!-- ****************************** -->
<!-- Check DAVx5/EteSync sync state -->
<!-- ****************************** -->
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<!-- ******************************** -->
<!-- Microsoft Authentication Library -->
<!-- ******************************** -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" tools:node="remove" />
<uses-feature
android:name="android.hardware.nfc"
android:required="false" />
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />
<!-- ****************************************** -->
<!-- Exclude OpenTasks and jtxBoard permissions -->
<!-- ****************************************** -->
@ -148,13 +159,13 @@
</queries>
<application
android:pageSizeCompat="enabled"
android:allowBackup="true"
android:backupAgent="org.tasks.backup.TasksBackupAgent"
android:backupInForeground="true"
android:fullBackupOnly="false"
android:icon="@mipmap/ic_launcher_blue"
android:label="@string/app_name"
android:manageSpaceActivity="org.tasks.preferences.ManageSpaceActivity"
android:name=".TasksApplication"
android:roundIcon="@mipmap/ic_launcher_blue"
android:supportsRtl="true"
@ -180,14 +191,6 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="org.tasks.github.a50fdbf3e289a7fb2fc6" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:host="${applicationId}"
android:scheme="msauth" />
</intent-filter>
</activity>
<activity
@ -279,6 +282,7 @@
<data android:mimeType="text/plain" />
<data android:mimeType="image/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="video/*"/>
<data android:mimeType="application/*" />
</intent-filter>
<intent-filter>
@ -383,6 +387,13 @@
android:resource="@xml/file_provider_paths"/>
</provider>
<provider
android:name=".widget.WidgetIconProvider"
android:authorities="${applicationId}.widgeticons"
android:exported="true"
android:grantUriPermissions="true"
tools:ignore="ExportedContentProvider" />
<receiver
android:name="org.dmfs.provider.tasks.TaskProviderBroadcastReceiver"
tools:node="remove"/>
@ -428,6 +439,10 @@
android:name=".caldav.CaldavAccountSettingsActivity"
android:theme="@style/Tasks"/>
<activity
android:name=".caldav.LocalAccountSettingsActivity"
android:theme="@style/Tasks" />
<activity
android:name=".etebase.EtebaseAccountSettingsActivity"
android:theme="@style/Tasks" />
@ -620,14 +635,11 @@
<receiver android:name="org.tasks.jobs.NotificationReceiver" />
<activity
android:name=".auth.MicrosoftAuthenticationActivity"
android:theme="@style/TranslucentDialog"/>
<activity
android:launchMode="singleTask"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:name="com.todoroo.astrid.activity.MainActivity">
android:name="com.todoroo.astrid.activity.MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/Tasks"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
</intent-filter>
@ -664,9 +676,6 @@
</intent-filter>
</activity>
<activity
android:name=".preferences.ManageSpaceActivity"
android:theme="@style/Tasks" />
<activity android:name="org.tasks.sync.microsoft.MicrosoftListSettingsActivity" />
<activity

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

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

@ -11,103 +11,75 @@ import android.graphics.Color
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.content.IntentCompat.getParcelableExtra
import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.rememberFragmentState
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.activity.TaskEditFragment.Companion.EXTRA_TASK
import com.todoroo.astrid.activity.TaskListFragment.Companion.EXTRA_FILTER
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.todoroo.astrid.adapter.SubheaderClickHandler
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity
import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.TasksApplication
import org.tasks.activities.TagSettingsActivity
import org.tasks.analytics.Firebase
import org.tasks.auth.SignInActivity
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity.Companion.EXTRA_CALDAV_ACCOUNT
import org.tasks.compose.drawer.DrawerAction
import org.tasks.compose.drawer.DrawerItem
import org.tasks.compose.drawer.MenuSearchBar
import org.tasks.compose.drawer.TaskListDrawer
import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.compose.AddAccountDestination
import org.tasks.compose.HomeDestination
import org.tasks.compose.accounts.AddAccountScreen
import org.tasks.compose.accounts.AddAccountViewModel
import org.tasks.compose.home.HomeScreen
import org.tasks.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.listSettingsClass
import org.tasks.dialogs.ImportTasksDialog
import org.tasks.dialogs.NewFilterDialog
import org.tasks.etebase.EtebaseAccountSettingsActivity
import org.tasks.extensions.Context.nightMode
import org.tasks.extensions.Context.openUri
import org.tasks.extensions.Context.toast
import org.tasks.extensions.broughtToFront
import org.tasks.extensions.flagsToString
import org.tasks.extensions.isFromHistory
import org.tasks.files.FileHelper
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_LIST
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_PLACE
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_TAGS
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.PlaceFilter
import org.tasks.kmp.org.tasks.compose.TouchSlopMultiplier
import org.tasks.kmp.org.tasks.compose.rememberImeState
import org.tasks.location.LocationPickerActivity
import org.tasks.location.LocationPickerActivity.Companion.EXTRA_PLACE
import org.tasks.jobs.WorkManager
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.HelpAndFeedback
import org.tasks.preferences.MainPreferences
import org.tasks.preferences.Preferences
import org.tasks.preferences.fragments.FRAG_TAG_IMPORT_TASKS
import org.tasks.sync.AddAccountDialog
import org.tasks.sync.SyncAdapters
import org.tasks.sync.microsoft.MicrosoftSignInViewModel
import org.tasks.themes.ColorProvider
import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme
@ -129,17 +101,20 @@ class MainActivity : AppCompatActivity() {
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var firebase: Firebase
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var workManager: WorkManager
private val viewModel: MainActivityViewModel by viewModels()
private var currentNightMode = 0
private var currentPro = false
private var actionMode: ActionMode? = null
private var isReady = false
/** @see android.app.Activity.onCreate
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
theme.themeBase.set(this)
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !isReady }
currentNightMode = nightMode
currentPro = inventory.hasPro
@ -148,10 +123,11 @@ class MainActivity : AppCompatActivity() {
lightScrim = Color.TRANSPARENT,
darkScrim = Color.TRANSPARENT
),
navigationBarStyle = SystemBarStyle.auto(
lightScrim = Color.TRANSPARENT,
darkScrim = Color.TRANSPARENT
)
navigationBarStyle = if (theme.themeBase.isDarkTheme(this)) {
SystemBarStyle.dark(Color.TRANSPARENT)
} else {
SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT)
}
)
setContent {
@ -159,184 +135,136 @@ class MainActivity : AppCompatActivity() {
theme = theme.themeBase.index,
primary = theme.themeColor.primaryColor,
) {
val drawerState = rememberDrawerState(
initialValue = DrawerValue.Closed,
confirmStateChange = {
viewModel.setDrawerState(it == DrawerValue.Open)
true
}
)
val state = viewModel.state.collectAsStateWithLifecycle().value
val currentWindowInsets = WindowInsets.systemBars.asPaddingValues()
val windowInsets = remember { mutableStateOf(currentWindowInsets) }
val keyboard = LocalSoftwareKeyboardController.current
LaunchedEffect(currentWindowInsets) {
Timber.d("insets: $currentWindowInsets")
if (currentWindowInsets.calculateTopPadding() != 0.dp || currentWindowInsets.calculateBottomPadding() != 0.dp) {
windowInsets.value = currentWindowInsets
val navController = rememberNavController()
val hasAccount = viewModel
.accountExists
.collectAsStateWithLifecycle(null)
.value
LaunchedEffect(hasAccount) {
Timber.d("hasAccount=$hasAccount")
if (hasAccount == false) {
navController.navigate(AddAccountDestination(showImport = true))
}
isReady = hasAccount != null
}
val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirective(
windowAdaptiveInfo = currentWindowAdaptiveInfo(),
).copy(
horizontalPartitionSpacerSize = 0.dp,
verticalPartitionSpacerSize = 0.dp,
)
)
val isListVisible =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
val isDetailVisible =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
TouchSlopMultiplier {
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = isListVisible,
drawerContent = {
ModalDrawerSheet(
drawerState = drawerState,
windowInsets = WindowInsets(0, 0, 0, 0),
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
TaskListDrawer(
arrangement = if (state.menuQuery.isBlank()) Arrangement.Top else Arrangement.Bottom,
filters = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems,
onClick = {
when (it) {
is DrawerItem.Filter -> {
viewModel.setFilter(it.filter)
scope.launch {
drawerState.close()
keyboard?.hide()
}
}
is DrawerItem.Header -> {
viewModel.toggleCollapsed(it.header)
}
}
},
onAddClick = {
scope.launch {
drawerState.close()
when (it.header.addIntentRc) {
FilterProvider.REQUEST_NEW_FILTER ->
NewFilterDialog.newFilterDialog().show(
supportFragmentManager,
SubheaderClickHandler.FRAG_TAG_NEW_FILTER
)
NavHost(
navController = navController,
startDestination = HomeDestination,
) {
composable<AddAccountDestination> {
val route = it.toRoute<AddAccountDestination>()
LaunchedEffect(hasAccount) {
if (route.showImport && hasAccount == true) {
navController.popBackStack()
}
}
val addAccountViewModel: AddAccountViewModel = hiltViewModel()
val microsoftVM: MicrosoftSignInViewModel = hiltViewModel()
val syncLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
syncAdapters.sync(true)
workManager.updateBackgroundSync()
} else {
result.data
?.getStringExtra(GtasksLoginActivity.EXTRA_ERROR)
?.let { toast(it) }
}
}
val importBackupLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
ImportTasksDialog.newImportTasksDialog(uri)
.show(supportFragmentManager, FRAG_TAG_IMPORT_TASKS)
}
}
AddAccountScreen(
gettingStarted = route.showImport,
hasTasksAccount = inventory.hasTasksAccount,
hasPro = inventory.hasPro,
onBack = { navController.popBackStack() },
signIn = { platform ->
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to platform)
when (platform) {
AddAccountDialog.Platform.TASKS_ORG ->
syncLauncher.launch(
Intent(this@MainActivity, SignInActivity::class.java)
)
REQUEST_NEW_PLACE ->
startActivityForResult(
Intent(
this@MainActivity,
LocationPickerActivity::class.java
),
REQUEST_NEW_PLACE
)
AddAccountDialog.Platform.GOOGLE_TASKS ->
syncLauncher.launch(
Intent(this@MainActivity, GtasksLoginActivity::class.java)
)
REQUEST_NEW_TAGS ->
startActivityForResult(
Intent(
this@MainActivity,
TagSettingsActivity::class.java
),
REQUEST_NEW_LIST
)
AddAccountDialog.Platform.MICROSOFT ->
microsoftVM.signIn(this@MainActivity)
REQUEST_NEW_LIST -> {
val account =
caldavDao.getAccount(it.header.id.toLong())
?: return@launch
when (it.header.subheaderType) {
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS,
->
startActivityForResult(
Intent(
this@MainActivity,
account.listSettingsClass()
)
.putExtra(
EXTRA_CALDAV_ACCOUNT,
account
),
REQUEST_NEW_LIST
)
AddAccountDialog.Platform.CALDAV ->
syncLauncher.launch(
Intent(this@MainActivity, CaldavAccountSettingsActivity::class.java)
)
else -> {}
}
}
AddAccountDialog.Platform.ETESYNC ->
syncLauncher.launch(
Intent(this@MainActivity, EtebaseAccountSettingsActivity::class.java)
)
else -> Timber.e("Unhandled request code: $it")
}
}
},
onErrorClick = {
context.startActivity(Intent(context, MainPreferences::class.java))
},
searchBar = {
MenuSearchBar(
begForMoney = state.begForMoney,
onDrawerAction = {
scope.launch {
drawerState.close()
when (it) {
DrawerAction.PURCHASE ->
if (TasksApplication.IS_GENERIC)
context.openUri(R.string.url_donate)
else
context.startActivity(
Intent(
context,
PurchaseActivity::class.java
)
)
AddAccountDialog.Platform.LOCAL ->
addAccountViewModel.createLocalAccount()
DrawerAction.HELP_AND_FEEDBACK ->
context.startActivity(
Intent(
context,
HelpAndFeedback::class.java
)
)
}
}
},
query = state.menuQuery,
onQueryChange = { viewModel.queryMenu(it) },
)
},
else -> throw IllegalArgumentException()
}
},
openUrl = { platform ->
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to platform.name)
addAccountViewModel.openUrl(this@MainActivity, platform)
},
onImportBackup = {
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to "import_backup")
importBackupLauncher.launch(
FileHelper.newFilePickerIntent(this@MainActivity, preferences.backupDirectory),
)
}
)
}
composable<HomeDestination> {
if (hasAccount != true) {
return@composable
}
) {
val scope = rememberCoroutineScope()
val state = viewModel.state.collectAsStateWithLifecycle().value
val drawerState = rememberDrawerState(
initialValue = DrawerValue.Closed,
confirmStateChange = {
viewModel.setDrawerState(it == DrawerValue.Open)
true
}
)
val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirectiveWithTwoPanesOnMediumWidth(
windowAdaptiveInfo = currentWindowAdaptiveInfo(),
).copy(
horizontalPartitionSpacerSize = 0.dp,
verticalPartitionSpacerSize = 0.dp,
)
)
val keyboard = LocalSoftwareKeyboardController.current
LaunchedEffect(state.task) {
if (state.task == null) {
if (intent.finishAffinity) {
finishAffinity()
} else {
if (intent.removeTask && intent.broughtToFront) {
moveTaskToBack(true)
}
keyboard?.hide()
navigator.navigateTo(pane = ThreePaneScaffoldRole.Secondary)
}
val pane = if (state.task == null) {
ThreePaneScaffoldRole.Secondary
} else {
navigator.navigateTo(pane = ThreePaneScaffoldRole.Primary)
ThreePaneScaffoldRole.Primary
}
Timber.d("Navigating to $pane")
navigator.navigateTo(pane = pane)
}
val isDetailVisible =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
BackHandler(enabled = state.task == null) {
Timber.d("onBackPressed")
if (intent.finishAffinity) {
finishAffinity()
} else if (isDetailVisible && navigator.canNavigateBack()) {
if (isDetailVisible && navigator.canNavigateBack()) {
scope.launch {
navigator.navigateBack()
}
@ -357,80 +285,15 @@ class MainActivity : AppCompatActivity() {
}
drawerState.close()
}
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
key (state.filter) {
val fragment = remember { mutableStateOf<TaskListFragment?>(null) }
val keyboardOpen = rememberImeState()
AndroidFragment<TaskListFragment>(
fragmentState = rememberFragmentState(),
arguments = remember(state.filter) {
Bundle()
.apply { putParcelable(EXTRA_FILTER, state.filter) }
},
modifier = Modifier
.fillMaxSize()
.imePadding(),
) { tlf ->
fragment.value = tlf
tlf.applyInsets(windowInsets.value)
tlf.setNavigationClickListener {
scope.launch { drawerState.open() }
}
}
LaunchedEffect(fragment, windowInsets, keyboardOpen.value) {
fragment.value?.applyInsets(
if (keyboardOpen.value) {
PaddingValues(
top = windowInsets.value.calculateTopPadding(),
)
} else {
windowInsets.value
}
)
}
}
},
detailPane = {
val direction = LocalLayoutDirection.current
Box(
modifier = Modifier
.fillMaxSize()
.padding(
top = windowInsets.value.calculateTopPadding(),
start = windowInsets.value.calculateStartPadding(direction),
end = windowInsets.value.calculateEndPadding(direction),
bottom = if (rememberImeState().value)
WindowInsets.ime.asPaddingValues().calculateBottomPadding()
else
windowInsets.value.calculateBottomPadding()
),
contentAlignment = Alignment.Center,
) {
if (state.task == null) {
if (isListVisible && isDetailVisible) {
Icon(
painter = painterResource(org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground),
contentDescription = null,
modifier = Modifier.size(192.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} else {
key(state.task) {
AndroidFragment<TaskEditFragment>(
fragmentState = rememberFragmentState(),
arguments = remember(state.task) {
Bundle()
.apply { putParcelable(EXTRA_TASK, state.task) }
},
modifier = Modifier.fillMaxSize(),
)
}
}
}
HomeScreen(
state = state,
drawerState = drawerState,
navigator = navigator,
showNewFilterDialog = {
NewFilterDialog.newFilterDialog().show(
supportFragmentManager,
SubheaderClickHandler.FRAG_TAG_NEW_FILTER
)
},
)
}
@ -441,27 +304,6 @@ class MainActivity : AppCompatActivity() {
handleIntent()
}
@Deprecated("Deprecated in Java")
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_NEW_LIST ->
if (resultCode == RESULT_OK && data != null) {
getParcelableExtra(data, OPEN_FILTER, Filter::class.java)?.let {
viewModel.setFilter(it)
}
}
REQUEST_NEW_PLACE ->
if (resultCode == RESULT_OK && data != null) {
getParcelableExtra(data, EXTRA_PLACE, Place::class.java)?.let {
viewModel.setFilter(PlaceFilter(it))
}
}
else ->
super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
@ -573,23 +415,17 @@ class MainActivity : AppCompatActivity() {
}
val Intent.removeTask: Boolean
get() = if (isFromHistory) {
false
} else {
getBooleanExtra(REMOVE_TASK, false).let {
removeExtra(REMOVE_TASK)
it
}
get() = try {
getBooleanExtra(REMOVE_TASK, false) && !isFromHistory && !broughtToFront
} finally {
removeExtra(REMOVE_TASK)
}
val Intent.finishAffinity: Boolean
get() = if (isFromHistory) {
false
} else {
getBooleanExtra(FINISH_AFFINITY, false).let {
removeExtra(FINISH_AFFINITY)
it
}
get() = try {
getBooleanExtra(FINISH_AFFINITY, false) && !isFromHistory && !broughtToFront
} finally {
removeExtra(FINISH_AFFINITY)
}
}
}

@ -14,6 +14,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
@ -38,6 +39,7 @@ import org.tasks.filters.CaldavFilter
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.SearchFilter
import org.tasks.filters.getIcon
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.TasksPreferences
@ -83,11 +85,13 @@ class MainActivityViewModel @Inject constructor(
)
val state = _state.asStateFlow()
val accountExists: Flow<Boolean>
get() = caldavDao.watchAccountExists()
private val refreshReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
LocalBroadcastManager.REFRESH,
LocalBroadcastManager.REFRESH_LIST -> _updateFilters.update { currentTimeMillis() }
LocalBroadcastManager.REFRESH -> _updateFilters.update { currentTimeMillis() }
}
}
}
@ -110,12 +114,9 @@ class MainActivityViewModel @Inject constructor(
)
}
updateFilters()
defaultFilterProvider.setLastViewedFilter(filter)
}
fun closeDrawer() {
_drawerOpen.update { false }
_state.update { it.copy(menuQuery = "") }
if (filter !is SearchFilter) {
defaultFilterProvider.setLastViewedFilter(filter)
}
}
fun setDrawerState(opened: Boolean) {
@ -214,12 +215,12 @@ class MainActivityViewModel @Inject constructor(
when (subheader.subheaderType) {
NavigationDrawerSubheader.SubheaderType.PREFERENCE -> {
tasksPreferences.set(booleanPreferencesKey(subheader.id), collapsed)
localBroadcastManager.broadcastRefreshList()
localBroadcastManager.broadcastRefresh()
}
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS -> {
caldavDao.setCollapsed(subheader.id, collapsed)
localBroadcastManager.broadcastRefreshList()
localBroadcastManager.broadcastRefresh()
}
}
}
@ -232,4 +233,10 @@ class MainActivityViewModel @Inject constructor(
_state.update { it.copy(menuQuery = query) }
updateFilters()
}
suspend fun getAccount(id: Long) = caldavDao.getAccount(id)
fun openLastViewedFilter() = viewModelScope.launch {
setFilter(defaultFilterProvider.getLastViewedFilter())
}
}

@ -110,7 +110,7 @@ class ShareLinkActivity : AppCompatActivity() {
intent.type?.let { type -> ATTACHMENT_TYPES.any { type.startsWith(it) } } ?: false
companion object {
private val ATTACHMENT_TYPES = listOf("image/", "application/", "audio/")
private val ATTACHMENT_TYPES = listOf("image/", "application/", "audio/", "video/", "text/plain")
private suspend fun TaskCreator.create(intent: Intent): Task {
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)

@ -15,6 +15,8 @@ import androidx.fragment.app.viewModels
import androidx.fragment.compose.content
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.activity.MainActivity.Companion.finishAffinity
import com.todoroo.astrid.activity.MainActivity.Companion.removeTask
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
@ -24,6 +26,7 @@ import org.tasks.data.dao.UserActivityDao
import org.tasks.dialogs.DateTimePicker
import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.Linkify
import org.tasks.extensions.hideKeyboard
import org.tasks.markdown.MarkdownProvider
import org.tasks.notifications.NotificationManager
import org.tasks.play.PlayServices
@ -32,6 +35,7 @@ import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme
import org.tasks.ui.ChipProvider
import org.tasks.ui.TaskEditViewModel
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
@ -98,7 +102,7 @@ class TaskEditFragment : Fragment() {
.setPositiveButton(R.string.ok) { _, _ ->
lifecycleScope.launch {
editViewModel.delete()
mainViewModel.setTask(null)
clearTask()
}
}
.setNegativeButton(R.string.cancel, null)
@ -132,17 +136,36 @@ class TaskEditFragment : Fragment() {
}
}
private fun clearTask() {
Timber.d("clearTask()")
mainViewModel.setTask(null)
activity?.let { activity ->
activity.hideKeyboard()
when {
activity.intent.finishAffinity -> {
Timber.d("finishAffinity")
activity.finishAffinity()
}
activity.intent.removeTask -> {
Timber.d("removeTask")
activity.moveTaskToBack(true)
activity.finish()
}
}
}
}
suspend fun save(remove: Boolean = true) {
editViewModel.save()
if (remove) {
mainViewModel.setTask(null)
clearTask()
}
activity?.let { playServices.requestReview(it) }
}
private fun discard() = lifecycleScope.launch {
editViewModel.discard()
mainViewModel.setTask(null)
clearTask()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

@ -22,7 +22,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LAYOUT_DIRECTION_LTR
import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.BackHandler
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
@ -128,6 +128,7 @@ import org.tasks.filters.Filter
import org.tasks.filters.FilterImpl
import org.tasks.filters.MyTasksFilter
import org.tasks.filters.PlaceFilter
import org.tasks.filters.SearchFilter
import org.tasks.filters.TagFilter
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
@ -278,6 +279,23 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
taskListEventBus
.onEach(this::process)
.launchIn(viewLifecycleOwner.lifecycleScope)
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if ((mainViewModel.state.value.filter as? SearchFilter)?.query?.isNotBlank() == true) {
lifecycleScope.launch {
mainViewModel.openLastViewedFilter()
}
if (search.isActionViewExpanded) {
search.collapseActionView()
}
Timber.d("Filtro resettato")
} else {
isEnabled = false // Disabilita il callback per consentire il comportamento predefinito
requireActivity().onBackPressedDispatcher.onBackPressed()
}
}
})
}
fun setNavigationClickListener(onClick: () -> Unit) {
@ -319,7 +337,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
right = endInset,
)
binding.bottomAppBar.updatePadding(bottom = bottomInset)
(binding.fab.layoutParams as MarginLayoutParams).bottomMargin = bottomInset / 2
val scrimLayoutParams = binding.systemBarScrim.layoutParams
scrimLayoutParams.height = bottomInset
binding.systemBarScrim.layoutParams = scrimLayoutParams
}
@OptIn(ExperimentalPermissionsApi::class)
@ -346,6 +366,14 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
listViewModel.setFilter(filter)
(recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView.layoutManager = LinearLayoutManager(context)
val baseFooterHeight = resources.getDimensionPixelSize(R.dimen.task_list_footer_height)
val additionalFabSpace = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
56f,
resources.displayMetrics
).toInt()
recyclerView.updatePadding(bottom = baseFooterHeight + additionalFabSpace)
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
listViewModel.updateBannerState()
@ -375,6 +403,11 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
binding.bottomAppBar.performShow()
}
}
val typedValue = TypedValue()
requireContext().theme.resolveAttribute(android.R.attr.colorBackground, typedValue, true)
val scrimColor = typedValue.data
binding.systemBarScrim.setBackgroundColor((scrimColor and 0x00FFFFFF) or 0xCC000000.toInt()) // 80% opacity
with (binding.fab) {
backgroundTintList = ColorStateList.valueOf(themeColor.primaryColor)
imageTintList = ColorStateList.valueOf(themeColor.colorOnPrimary)
@ -653,7 +686,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} else {
dialogBuilder
.newDialog(R.string.clear_completed_tasks_confirmation)
.setMessage(R.string.clear_completed_tasks_count, countString)
.setMessage(R.string.delete_tasks_warning, countString)
.setPositiveButton(R.string.ok) { _, _ ->
lifecycleScope.launch {
listViewModel.markDeleted(tasks)
@ -749,10 +782,12 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
colorProvider.getPriorityColor(3))
}
@SuppressLint("NotifyDataSetChanged")
override fun onResume() {
super.onResume()
listViewModel.invalidate()
localBroadcastManager.registerTaskCompletedReceiver(repeatConfirmationReceiver)
recyclerAdapter?.notifyDataSetChanged() // force rebind to update timestamps (hidden/overdue)
}
private fun makeSnackbar(@StringRes res: Int, vararg args: Any?): Snackbar? {

@ -3,8 +3,8 @@ package com.todoroo.astrid.adapter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.subtasks.SubtasksFilterUpdater
import org.tasks.LocalBroadcastManager
import org.tasks.Strings.isNullOrEmpty
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.data.TaskContainer
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
@ -24,9 +24,9 @@ class AstridTaskAdapter internal constructor(
googleTaskDao: GoogleTaskDao,
caldavDao: CaldavDao,
private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
taskMover: TaskMover,
) : TaskAdapter(false, googleTaskDao, caldavDao, taskDao, localBroadcastManager, taskMover) {
) : TaskAdapter(false, googleTaskDao, caldavDao, taskDao, refreshBroadcaster, taskMover) {
private val chainedCompletions = Collections.synchronizedMap(HashMap<String, ArrayList<String>>())
@ -56,7 +56,7 @@ class AstridTaskAdapter internal constructor(
for (i in 0 until abs(delta)) {
updater.indent(list, filter, targetTaskId, delta)
}
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
} catch (e: Exception) {
Timber.e(e)
}

@ -6,7 +6,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.activities.TagSettingsActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity.Companion.EXTRA_CALDAV_ACCOUNT
import org.tasks.data.dao.CaldavDao
@ -30,7 +30,7 @@ class SubheaderClickHandler @Inject constructor(
private val activity: Activity,
private val tasksPreferences: TasksPreferences,
private val caldavDao: CaldavDao,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
): SubheaderViewHolder.ClickHandler {
override fun onClick(subheader: NavigationDrawerSubheader) {
(activity as AppCompatActivity).lifecycleScope.launch {
@ -40,7 +40,7 @@ class SubheaderClickHandler @Inject constructor(
CALDAV,
TASKS -> caldavDao.setCollapsed(subheader.id, collapsed)
}
localBroadcastManager.broadcastRefreshList()
refreshBroadcaster.broadcastRefresh()
}
}

@ -13,7 +13,7 @@ import com.todoroo.astrid.core.SortHelper.SORT_START
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskMover
import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.data.TaskContainer
import org.tasks.data.createDueDate
import org.tasks.data.createHideUntil
@ -31,7 +31,7 @@ open class TaskAdapter(
private val googleTaskDao: GoogleTaskDao,
private val caldavDao: CaldavDao,
private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
private val taskMover: TaskMover,
) {
private val selected = HashSet<Long>()
@ -296,7 +296,7 @@ open class TaskAdapter(
taskDao.setOrder(task.id, task.task.order)
taskDao.setParent(newParentId, listOf(task.id))
taskDao.touch(task.id)
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
}
protected suspend fun moveGoogleTask(from: Int, to: Int, indent: Int) {
@ -375,7 +375,7 @@ open class TaskAdapter(
}
}
taskDao.touch(task.id)
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
if (BuildConfig.DEBUG) {
googleTaskDao.validateSorting(task.caldav!!)
}
@ -407,7 +407,7 @@ open class TaskAdapter(
newPosition = newPosition,
)
taskDao.touch(task.id)
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
}
private suspend fun changeCaldavParent(task: TaskContainer, indent: Int, to: Int): Long {

@ -75,10 +75,16 @@ class AlarmCalculator(
*/
private fun calculateNextRandomReminder(random: Random, task: Task, reminderPeriod: Long) =
if (reminderPeriod > 0) {
val baseline = when {
task.reminderLast > 0 -> task.reminderLast
task.isRecurring -> task.modificationDate
else -> task.creationDate
}
val multiplier = 0.85f + 0.3f * random.nextFloat(task.id + baseline)
maxOf(
task.reminderLast
.coerceAtLeast(task.creationDate)
.plus((reminderPeriod * (0.85f + 0.3f * random.nextFloat())).toLong()),
baseline.plus((reminderPeriod * multiplier).toLong()),
task.hideUntil
)
} else {

@ -5,7 +5,7 @@
*/
package com.todoroo.astrid.alarms
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.TaskDao
import org.tasks.data.db.DbUtils
@ -28,7 +28,7 @@ import javax.inject.Inject
class AlarmService @Inject constructor(
private val alarmDao: AlarmDao,
private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
private val notificationManager: NotificationManager,
private val workManager: WorkManager,
private val alarmCalculator: AlarmCalculator,
@ -54,7 +54,7 @@ class AlarmService @Inject constructor(
changed = true
}
if (changed) {
localBroadcastManager.broadcastRefreshList()
refreshBroadcaster.broadcastRefresh()
}
return changed
}

@ -6,7 +6,7 @@
package com.todoroo.astrid.dao
import com.todoroo.astrid.timers.TimerPlugin
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.data.TaskContainer
import org.tasks.data.count
import org.tasks.data.dao.TaskDao
@ -28,7 +28,7 @@ import javax.inject.Inject
class TaskDao @Inject constructor(
private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
private val notificationManager: NotificationManager,
private val geofenceApi: GeofenceApi,
private val timerPlugin: TimerPlugin,
@ -82,7 +82,7 @@ class TaskDao @Inject constructor(
suspend fun setCollapsed(id: Long, collapsed: Boolean) {
taskDao.setCollapsed(listOf(id), collapsed)
syncAdapters.sync()
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
}
suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) {
@ -103,7 +103,7 @@ class TaskDao @Inject constructor(
Timber.d("Saved $task")
afterUpdate(task, original)
if (!task.isSuppressRefresh()) {
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
}
workManager.triggerNotifications()
workManager.scheduleRefresh()

@ -14,7 +14,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.R
import org.tasks.Strings
import org.tasks.compose.edit.AttachmentRow
@ -108,16 +110,16 @@ class FilesControlSet : TaskEditControlFragment() {
)
}
private fun newAttachment(output: Uri) {
private suspend fun newAttachment(output: Uri) {
val attachment = TaskAttachment(
uri = output.toString(),
name = FileHelper.getFilename(requireContext(), output)!!,
)
lifecycleScope.launch {
withContext(Dispatchers.IO) {
taskAttachmentDao.insert(attachment)
viewModel.setAttachments(
viewModel.viewState.value.attachments +
(taskAttachmentDao.getAttachment(attachment.remoteId) ?: return@launch))
(taskAttachmentDao.getAttachment(attachment.remoteId) ?: return@withContext))
}
}

@ -11,6 +11,7 @@ import android.content.Context
import android.net.Uri
import android.provider.CalendarContract
import android.text.format.Time
import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
@ -19,7 +20,7 @@ import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.Task
import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.DateTime
import org.tasks.time.ONE_HOUR
import timber.log.Timber
import java.util.TimeZone
@ -30,8 +31,8 @@ class GCalHelper @Inject constructor(
private val taskDao: TaskDao,
private val preferences: Preferences,
private val permissionChecker: PermissionChecker,
private val calendarEventProvider: CalendarEventProvider) {
private val calendarEventProvider: CalendarEventProvider,
) {
private val cr: ContentResolver = context.contentResolver
private suspend fun getTaskEventUri(task: Task) =
@ -109,7 +110,7 @@ class GCalHelper @Inject constructor(
})
updateValues.put(CalendarContract.Events.DESCRIPTION, task.notes)
createStartAndEndDate(task, updateValues)
cr.update(Uri.parse(uri), updateValues, null, null)
cr.update(uri.toUri(), updateValues, null, null)
} catch (e: Exception) {
Timber.e(e, "Failed to update calendar: %s [%s]", uri, task)
}
@ -117,10 +118,10 @@ class GCalHelper @Inject constructor(
suspend fun rescheduleRepeatingTask(task: Task) {
val taskUri = getTaskEventUri(task)
if (isNullOrEmpty(taskUri)) {
if (taskUri.isNullOrBlank()) {
return
}
val eventUri = Uri.parse(taskUri)
val eventUri = taskUri.toUri()
val event = calendarEventProvider.getEvent(eventUri)
if (event == null) {
task.calendarURI = ""
@ -134,11 +135,6 @@ class GCalHelper @Inject constructor(
private fun createStartAndEndDate(task: Task, values: ContentValues) {
val dueDate = task.dueDate
val tzCorrectedDueDate = dueDate + TimeZone.getDefault().getOffset(dueDate)
val tzCorrectedDueDateNow = currentTimeMillis() + TimeZone.getDefault().getOffset(
currentTimeMillis()
)
// FIXME: doesn't respect timezones, see story 17443653
if (task.hasDueDate()) {
if (task.hasDueTime()) {
var estimatedTime = task.estimatedSeconds * 1000.toLong()
@ -152,24 +148,19 @@ class GCalHelper @Inject constructor(
values.put(CalendarContract.Events.DTSTART, dueDate - estimatedTime)
values.put(CalendarContract.Events.DTEND, dueDate)
}
// setting a duetime to a previously timeless event requires explicitly setting allDay=0
values.put(CalendarContract.Events.ALL_DAY, "0")
values.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
} else {
values.put(CalendarContract.Events.DTSTART, tzCorrectedDueDate)
values.put(CalendarContract.Events.DTEND, tzCorrectedDueDate)
val utcMidnight = DateTime(dueDate).toUTC().startOfDay()
values.put(CalendarContract.Events.DTSTART, utcMidnight.millis)
values.put(CalendarContract.Events.DTEND, utcMidnight.plusDays(1).millis)
values.put(CalendarContract.Events.ALL_DAY, "1")
values.put(CalendarContract.Events.EVENT_TIMEZONE, Time.TIMEZONE_UTC)
}
} else {
values.put(CalendarContract.Events.DTSTART, tzCorrectedDueDateNow)
values.put(CalendarContract.Events.DTEND, tzCorrectedDueDateNow)
values.put(CalendarContract.Events.ALL_DAY, "1")
}
if ("1" == values[CalendarContract.Events.ALL_DAY]) {
values.put(CalendarContract.Events.EVENT_TIMEZONE, Time.TIMEZONE_UTC)
} else {
values.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
Timber.w("Not creating calendar event, task has no due date: %s", task)
}
}
companion object {

@ -7,7 +7,7 @@ package com.todoroo.astrid.gtasks
import com.google.api.services.tasks.model.TaskList
import com.todoroo.astrid.service.TaskDeleter
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
@ -16,7 +16,7 @@ import javax.inject.Inject
class GtasksListService @Inject constructor(
private val caldavDao: CaldavDao,
private val taskDeleter: TaskDeleter,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
) {
/**
@ -55,6 +55,6 @@ class GtasksListService @Inject constructor(
for (listId in previousLists) {
taskDeleter.delete(caldavDao.getCalendarById(listId)!!)
}
localBroadcastManager.broadcastRefreshList()
refreshBroadcaster.broadcastRefresh()
}
}

@ -7,6 +7,7 @@ import com.google.api.services.tasks.model.Task
import com.google.api.services.tasks.model.TaskList
import com.google.api.services.tasks.model.TaskLists
import org.tasks.googleapis.BaseInvoker
import timber.log.Timber
import java.io.IOException
/**
@ -43,21 +44,30 @@ class GtasksInvoker(
@Throws(IOException::class)
suspend fun getAllPositions(
listId: String?, pageToken: String?): com.google.api.services.tasks.model.Tasks? =
execute(
service!!
.tasks()
.list(listId)
.setMaxResults(100)
.setShowDeleted(false)
.setShowHidden(false)
.setPageToken(pageToken)
.setFields("items(id,parent,position),nextPageToken"))
listId: String?,
pageToken: String?,
): com.google.api.services.tasks.model.Tasks? =
execute(
service!!
.tasks()
.list(listId)
.setMaxResults(100)
.setShowDeleted(false)
.setShowHidden(false)
.setPageToken(pageToken)
.setFields("items(id,parent,position),nextPageToken")
)
@Throws(IOException::class)
suspend fun createGtask(
listId: String?, task: Task?, parent: String?, previous: String?): Task? =
execute(service!!.tasks().insert(listId, task).setParent(parent).setPrevious(previous))
listId: String?,
task: Task?,
parent: String?,
previous: String?,
): Task? {
Timber.d("createGtask(listId=$listId, task=<redacted>, parent=$parent, previous=$previous)")
return execute(service!!.tasks().insert(listId, task).setParent(parent).setPrevious(previous))
}
@Throws(IOException::class)
suspend fun updateGtask(listId: String?, task: Task) =
@ -65,19 +75,26 @@ class GtasksInvoker(
@Throws(IOException::class)
suspend fun moveGtask(
listId: String?, taskId: String?, parentId: String?, previousId: String?): Task? =
execute(
service!!
.tasks()
.move(listId, taskId)
.setParent(parentId)
.setPrevious(previousId))
listId: String?,
taskId: String?,
parentId: String?,
previousId: String?,
): Task? {
Timber.d("moveGtask(listId=$listId, taskId=$taskId, parentId=$parentId, previousId=$previousId)")
return execute(
service!!
.tasks()
.move(listId, taskId)
.setParent(parentId)
.setPrevious(previousId)
)
}
@Throws(IOException::class)
suspend fun deleteGtaskList(listId: String?) {
try {
execute(service!!.tasklists().delete(listId))
} catch (ignored: HttpNotFoundException) {
} catch (_: HttpNotFoundException) {
}
}
@ -91,9 +108,10 @@ class GtasksInvoker(
@Throws(IOException::class)
suspend fun deleteGtask(listId: String?, taskId: String?) {
Timber.d("deleteGtask(listId=$listId, taskId=$taskId)")
try {
execute(service!!.tasks().delete(listId, taskId))
} catch (ignored: HttpNotFoundException) {
} catch (_: HttpNotFoundException) {
}
}
}

@ -26,7 +26,6 @@ import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.dialogs.DialogBuilder
import org.tasks.gtasks.GoogleAccountManager
import org.tasks.preferences.ActivityPermissionRequestor
import org.tasks.preferences.PermissionRequestor
import javax.inject.Inject
@ -41,14 +40,11 @@ class GtasksLoginActivity : AppCompatActivity() {
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var googleAccountManager: GoogleAccountManager
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var permissionRequestor: ActivityPermissionRequestor
@Inject lateinit var firebase: Firebase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (permissionRequestor.requestAccountPermissions()) {
chooseAccount()
}
chooseAccount()
}
private fun chooseAccount() {

@ -7,7 +7,7 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.caldav.VtodoCache
import org.tasks.data.dao.DeletionDao
import org.tasks.data.dao.LocationDao
@ -28,7 +28,7 @@ class TaskDeleter @Inject constructor(
@ApplicationContext private val context: Context,
private val deletionDao: DeletionDao,
private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
private val syncAdapters: SyncAdapters,
private val vtodoCache: VtodoCache,
private val notificationManager: NotificationManager,
@ -50,7 +50,7 @@ class TaskDeleter @Inject constructor(
cleanup = { cleanup(it) }
)
syncAdapters.sync()
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
taskDao.fetch(ids)
}
@ -63,7 +63,7 @@ class TaskDeleter @Inject constructor(
ids = tasks,
cleanup = { cleanup(it) }
)
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
}
suspend fun delete(list: CaldavCalendar) {
@ -72,7 +72,7 @@ class TaskDeleter @Inject constructor(
caldavCalendar = list,
cleanup = { cleanup(it) }
)
localBroadcastManager.broadcastRefreshList()
refreshBroadcaster.broadcastRefresh()
}
suspend fun delete(account: CaldavAccount) {
@ -81,7 +81,7 @@ class TaskDeleter @Inject constructor(
caldavAccount = account,
cleanup = { cleanup(it) }
)
localBroadcastManager.broadcastRefreshList()
refreshBroadcaster.broadcastRefresh()
}
private suspend fun cleanup(tasks: List<Long>) {

@ -2,7 +2,7 @@ package com.todoroo.astrid.service
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.gcal.GCalHelper
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
@ -24,7 +24,7 @@ import javax.inject.Inject
class TaskDuplicator @Inject constructor(
private val gcalHelper: GCalHelper,
private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
private val tagDao: TagDao,
private val tagDataDao: TagDataDao,
private val googleTaskDao: GoogleTaskDao,
@ -44,7 +44,7 @@ class TaskDuplicator @Inject constructor(
.let { taskDao.fetch(it) }
.filterNot { it.readOnly }
.map { clone(it, it.parent) }
.also { localBroadcastManager.broadcastRefresh() }
.also { refreshBroadcaster.broadcastRefresh() }
}
private suspend fun clone(task: Task, parentId: Long): Task {

@ -1,6 +1,6 @@
package com.todoroo.astrid.service
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.caldav.VtodoCache
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
@ -22,7 +22,7 @@ class TaskMover @Inject constructor(
private val caldavDao: CaldavDao,
private val googleTaskDao: GoogleTaskDao,
private val preferences: Preferences,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
private val syncAdapters: SyncAdapters,
private val vtodoCache: VtodoCache,
) {
@ -63,7 +63,7 @@ class TaskMover @Inject constructor(
taskIds.dbchunk().forEach {
taskDao.touch(it)
}
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
syncAdapters.sync()
}

@ -3,6 +3,9 @@ package com.todoroo.astrid.service
import android.content.Context
import android.net.Uri
import androidx.annotation.ColorRes
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.google.common.collect.ImmutableListMultimap
import com.google.common.collect.ListMultimap
import com.google.common.collect.Multimaps
@ -34,6 +37,8 @@ import org.tasks.data.entity.Filter
import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData
import org.tasks.filters.CaldavFilter
import org.tasks.jobs.UpgradeIconSyncWork
import org.tasks.jobs.networkConstraints
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
@ -145,6 +150,15 @@ class Upgrader @Inject constructor(
}
}
}
run(from, V14_8) {
WorkManager.getInstance(context).enqueueUniqueWork(
uniqueWorkName = "upload_icons",
existingWorkPolicy = ExistingWorkPolicy.KEEP,
request = OneTimeWorkRequestBuilder<UpgradeIconSyncWork>()
.setConstraints(networkConstraints)
.build()
)
}
preferences.setBoolean(R.string.p_just_updated, true)
} else {
setInstallDetails(to)
@ -407,6 +421,7 @@ class Upgrader @Inject constructor(
const val V12_8 = 120800
const val V14_5_4 = 140516
const val V14_6_1 = 140602
const val V14_8 = 140800
@JvmStatic
fun getAndroidColor(context: Context, index: Int): Int {

@ -73,10 +73,11 @@ class StartDateControlSet : TaskEditControlFragment() {
REQUEST_START_DATE,
vm.selectedDay.value,
vm.selectedTime.value,
preferences.getBoolean(
autoClose = preferences.getBoolean(
R.string.p_auto_dismiss_datetime_edit_screen,
false
)
),
showDueDate = !viewModel.viewState.value.list.account.isOpenTasks,
)
.show(fragmentManager, FRAG_TAG_DATE_PICKER)
}

@ -7,13 +7,14 @@ import android.content.IntentFilter
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.todoroo.astrid.api.AstridApiConstants
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.widget.AppWidgetManager
import javax.inject.Inject
class LocalBroadcastManager @Inject constructor(
@ApplicationContext context: Context,
private val appWidgetManager: AppWidgetManager,
) {
): RefreshBroadcaster {
private val localBroadcastManager = LocalBroadcastManager.getInstance(context)
fun registerRefreshReceiver(broadcastReceiver: BroadcastReceiver?) {
@ -23,7 +24,6 @@ class LocalBroadcastManager @Inject constructor(
fun registerRefreshListReceiver(broadcastReceiver: BroadcastReceiver?) {
val intentFilter = IntentFilter()
intentFilter.addAction(REFRESH)
intentFilter.addAction(REFRESH_LIST)
localBroadcastManager.registerReceiver(broadcastReceiver!!, intentFilter)
}
@ -42,15 +42,11 @@ class LocalBroadcastManager @Inject constructor(
)
}
fun broadcastRefresh() {
override fun broadcastRefresh() {
localBroadcastManager.sendBroadcast(Intent(REFRESH))
appWidgetManager.updateWidgets()
}
fun broadcastRefreshList() {
localBroadcastManager.sendBroadcast(Intent(REFRESH_LIST))
}
fun broadcastPreferenceRefresh() {
localBroadcastManager.sendBroadcast(Intent(REFRESH_PREFERENCES))
}
@ -80,7 +76,6 @@ class LocalBroadcastManager @Inject constructor(
companion object {
const val REFRESH = "${BuildConfig.APPLICATION_ID}.REFRESH"
const val REFRESH_LIST = "${BuildConfig.APPLICATION_ID}.REFRESH_LIST"
private const val TASK_COMPLETED = "${BuildConfig.APPLICATION_ID}.REPEAT"
private const val REFRESH_PURCHASES = "${BuildConfig.APPLICATION_ID}.REFRESH_PURCHASES"
private const val REFRESH_PREFERENCES = "${BuildConfig.APPLICATION_ID}.REFRESH_PREFERENCES"

@ -2,23 +2,16 @@ package org.tasks
import android.content.Context
import android.content.pm.ShortcutManager
import com.todoroo.andlib.utility.AndroidUtilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ShortcutManager @Inject constructor(@ApplicationContext context: Context) {
private val shortcutManager: ShortcutManager? = if (AndroidUtilities.atLeastNougatMR1()) {
context.getSystemService(ShortcutManager::class.java)
} else {
null
}
private val shortcutManager = context.getSystemService(ShortcutManager::class.java)
fun reportShortcutUsed(shortcutId: String) {
if (AndroidUtilities.atLeastNougatMR1()) {
shortcutManager?.reportShortcutUsed(shortcutId)
}
shortcutManager?.reportShortcutUsed(shortcutId)
}
companion object {

@ -6,6 +6,7 @@ import android.app.ApplicationExitInfo
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
@ -17,6 +18,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.coroutineScope
import androidx.work.Configuration
import com.mikepenz.iconics.Iconics
import com.todoroo.andlib.utility.AndroidUtilities.atLeastAndroid15
import com.todoroo.andlib.utility.AndroidUtilities.atLeastR
import com.todoroo.astrid.service.Upgrader
import dagger.Lazy
@ -102,11 +104,17 @@ class TasksApplication : Application(), Configuration.Provider {
Timber.i("Astrid Startup. %s => %s", lastVersion, currentVersion)
if (atLeastR()) {
scope.launch {
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val exitReasons = activityManager.getHistoricalProcessExitReasons(null, 0, 1)
logExitReasons(exitReasons)
}
}
if (atLeastAndroid15()) {
val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
activityManager.addApplicationStartInfoCompletionListener(mainExecutor) { startInfo ->
Timber.d("Application was force stopped: ${startInfo.wasForceStopped()}")
}
}
// invoke upgrade service
if (lastVersion != currentVersion) {

@ -8,13 +8,13 @@ import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Build
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.content.pm.ShortcutInfoCompat
@ -26,25 +26,24 @@ import androidx.lifecycle.lifecycleScope
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.andlib.utility.AndroidUtilities.atLeastS
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseActivity
import org.tasks.compose.DeleteButton
import org.tasks.compose.IconPickerActivity.Companion.launchIconPicker
import org.tasks.compose.IconPickerActivity.Companion.registerForIconPickerResult
import org.tasks.compose.settings.ListSettingsContent
import org.tasks.compose.settings.ListSettingsScaffold
import org.tasks.data.UUIDHelper
import org.tasks.dialogs.ColorPalettePicker
import org.tasks.dialogs.ColorPalettePicker.Companion.newColorPalette
import org.tasks.dialogs.ColorPickerAdapter.Palette
import org.tasks.dialogs.ColorWheelPicker
import org.tasks.extensions.addBackPressedCallback
import org.tasks.filters.Filter
import org.tasks.icons.OutlinedGoogleMaterial
import org.tasks.intents.TaskIntents
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.themes.ColorProvider
import org.tasks.themes.Theme
import org.tasks.themes.contentColorFor
import org.tasks.widget.RequestPinWidgetReceiver
@ -54,10 +53,12 @@ import org.tasks.widget.TasksWidget
import javax.inject.Inject
abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicker.ColorPickedCallback, ColorWheelPicker.ColorPickedCallback {
abstract class BaseListSettingsActivity : AppCompatActivity() {
@Inject lateinit var tasksTheme: Theme
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var firebase: Firebase
@Inject lateinit var inventory: Inventory
@Inject lateinit var colorProvider: ColorProvider
protected val baseViewModel: BaseListSettingsViewModel by viewModels()
@ -89,11 +90,6 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
}
}
private fun showThemePicker() {
newColorPalette(null, 0, baseViewModel.color, Palette.COLORS)
.show(supportFragmentManager, FRAG_TAG_COLOR_PICKER)
}
private val launcher = registerForIconPickerResult { selected ->
baseViewModel.setIcon(selected)
}
@ -102,10 +98,6 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
launcher.launchIconPicker(this, baseViewModel.icon)
}
override fun onColorPicked(color: Int) {
baseViewModel.setColor(color)
}
protected open fun promptDelete() { baseViewModel.promptDelete(true) }
/** Standard @Compose view content for descendants. Caller must wrap it to TasksTheme{} */
@ -133,7 +125,9 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
fab = fab,
) {
ListSettingsContent(
hasPro = remember { inventory.purchasedThemes() },
color = viewState.color,
colors = remember { colorProvider.getThemeColors() },
icon = viewState.icon ?: defaultIcon,
text = viewState.title,
error = viewState.error,
@ -143,12 +137,16 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
baseViewModel.setTitle(it)
baseViewModel.setError("")
},
pickColor = { showThemePicker() },
clearColor = { onColorPicked(0) },
setColor = { baseViewModel.setColor(it) },
pickIcon = { showIconPicker() },
addShortcutToHome = { createShortcut(color) },
addWidgetToHome = { createWidget() },
extensionContent = extensionContent,
purchase = {
startActivity(
Intent(this@BaseListSettingsActivity, PurchaseActivity::class.java)
)
},
)
}
}
@ -201,7 +199,7 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
protected fun createWidget() {
val filter = filter ?: return
val appWidgetManager = getSystemService(AppWidgetManager::class.java)
if (AndroidUtilities.atLeastOreo() && appWidgetManager.isRequestPinAppWidgetSupported) {
if (appWidgetManager.isRequestPinAppWidgetSupported) {
val provider = ComponentName(this, TasksWidget::class.java)
val configIntent = Intent(this, RequestPinWidgetReceiver::class.java).apply {
action = RequestPinWidgetReceiver.ACTION_CONFIGURE_WIDGET
@ -212,7 +210,7 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
this,
filter.hashCode(),
configIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else PendingIntent.FLAG_UPDATE_CURRENT
if (atLeastS()) PendingIntent.FLAG_MUTABLE else PendingIntent.FLAG_UPDATE_CURRENT
)
appWidgetManager.requestPinAppWidget(provider, null, successCallback)
firebase.logEvent(R.string.event_create_widget, R.string.param_type to "settings_activity")
@ -220,8 +218,6 @@ abstract class BaseListSettingsActivity : AppCompatActivity(), ColorPalettePicke
}
companion object {
private const val FRAG_TAG_COLOR_PICKER = "frag_tag_color_picker"
fun createShortcutIcon(context: Context, backgroundColor: Color): IconCompat {
val size = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)

@ -6,7 +6,7 @@ import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.automirrored.outlined.Help
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
@ -14,6 +14,8 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
@ -26,7 +28,7 @@ import com.todoroo.astrid.api.TextInputCriterion
import com.todoroo.astrid.core.CriterionInstance
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.R
import org.tasks.Strings
import org.tasks.compose.DeleteButton
@ -47,13 +49,14 @@ import org.tasks.filters.FilterCriteriaProvider
import org.tasks.filters.mapToSerializedString
import org.tasks.themes.TasksIcons
import org.tasks.themes.TasksTheme
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
class FilterSettingsActivity : BaseListSettingsActivity() {
@Inject lateinit var filterDao: FilterDao
@Inject lateinit var filterCriteriaProvider: FilterCriteriaProvider
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var refreshBroadcaster: RefreshBroadcaster
private val viewModel: FilterSettingsViewModel by viewModels()
@ -125,7 +128,7 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
} else {
filterDao.update(f)
}
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
setResult(
Activity.RESULT_OK,
Intent(TaskListFragment.ACTION_RELOAD)
@ -170,7 +173,18 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
optionButton = {
if (isNew) {
IconButton(onClick = { help() }) {
Icon(imageVector = Icons.Outlined.Help, contentDescription = "")
// Cancel the mirroring of the help icon when the locale is Hebrew.
val modifier =
if (Locale.getDefault().language == Locale.forLanguageTag("he").language) {
Modifier.scale(scaleX = -1f, scaleY = 1f)
} else {
Modifier
}
Icon(
imageVector = Icons.AutoMirrored.Outlined.Help,
contentDescription = "",
modifier = modifier,
)
}
} else DeleteButton(filter?.title ?: ""){ delete() }
},

@ -15,7 +15,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity.Companion.EXTRA_CALDAV_ACCOUNT
@ -35,7 +35,7 @@ import javax.inject.Inject
class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var refreshBroadcaster: RefreshBroadcaster
private val account: CaldavAccount
get() = intent.getParcelableExtra(EXTRA_CALDAV_ACCOUNT)!!
@ -122,7 +122,7 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
icon = baseViewModel.icon
)
)
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
setResult(
Activity.RESULT_OK,
Intent(TaskListFragment.ACTION_RELOAD)

@ -140,7 +140,7 @@ class NavigationDrawerCustomization : ThemedInjectingAppCompatActivity(), Toolba
private inner class RefreshReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val action = intent?.action
if (LocalBroadcastManager.REFRESH == action || LocalBroadcastManager.REFRESH_LIST == action) {
if (LocalBroadcastManager.REFRESH == action) {
updateFilters()
}
}

@ -27,7 +27,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.activity.TaskListFragment
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.compose.Constants
@ -60,7 +60,7 @@ class PlaceSettingsActivity : BaseListSettingsActivity(),
@Inject lateinit var map: MapFragment
@Inject lateinit var preferences: Preferences
@Inject lateinit var locale: Locale
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var refreshBroadcaster: RefreshBroadcaster
private lateinit var place: Place
override val defaultIcon = TasksIcons.PLACE
@ -172,7 +172,7 @@ class PlaceSettingsActivity : BaseListSettingsActivity(),
radius = sliderPos.floatValue.roundToInt(),
)
locationDao.update(place)
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
setResult(
Activity.RESULT_OK,
Intent(TaskListFragment.ACTION_RELOAD)
@ -190,7 +190,7 @@ class PlaceSettingsActivity : BaseListSettingsActivity(),
locationDao.deleteGeofencesByPlace(place.uid!!)
locationDao.delete(place)
setResult(Activity.RESULT_OK, Intent(TaskListFragment.ACTION_DELETED))
localBroadcastManager.broadcastRefreshList()
refreshBroadcaster.broadcastRefresh()
finish()
}

@ -12,7 +12,7 @@ import androidx.activity.compose.setContent
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.activity.TaskListFragment
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.dao.TagDao
@ -28,7 +28,7 @@ import javax.inject.Inject
class TagSettingsActivity : BaseListSettingsActivity() {
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var tagDao: TagDao
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var refreshBroadcaster: RefreshBroadcaster
private lateinit var tagData: TagData
private val isNewTag: Boolean
@ -88,7 +88,7 @@ class TagSettingsActivity : BaseListSettingsActivity() {
)
.let { it.copy(id = tagDataDao.insert(it)) }
.let {
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
setResult(
Activity.RESULT_OK,
Intent().putExtra(MainActivity.OPEN_FILTER, TagFilter(it))
@ -104,7 +104,7 @@ class TagSettingsActivity : BaseListSettingsActivity() {
.let {
tagDataDao.update(it)
tagDao.rename(it.remoteId!!, newName)
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
setResult(
Activity.RESULT_OK,
Intent(TaskListFragment.ACTION_RELOAD)

@ -9,4 +9,5 @@ object Constants {
const val SYNC_TYPE_ETEBASE = "etebase"
const val SYNC_TYPE_DECSYNC = "decsync"
const val SYNC_TYPE_MICROSOFT = "microsoft"
const val SYNC_TYPE_LOCAL = "local"
}

@ -1,22 +0,0 @@
package org.tasks.auth
import android.net.Uri
import androidx.core.net.toUri
data class IdentityProvider(
val name: String,
val discoveryEndpoint: Uri,
val clientId: String,
val redirectUri: Uri,
val scope: String
) {
companion object {
val MICROSOFT = IdentityProvider(
"Microsoft",
"https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration".toUri(),
"9d4babd5-e7ba-4286-ba4b-17274495a901",
"msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D".toUri(),
"user.read Tasks.ReadWrite openid offline_access email"
)
}
}

@ -112,6 +112,20 @@ class TasksJsonExporter @Inject constructor(
}
}
suspend fun doSettingsExport(os: OutputStream?) = withContext(Dispatchers.IO) {
val writer = os!!.bufferedWriter()
with (JsonWriter(writer)) {
write("{")
write("version", BuildConfig.VERSION_CODE)
write("timestamp", currentTimeMillis())
write("\"data\":{")
writePreferences()
write("}")
write("}")
}
writer.flush()
}
@Throws(IOException::class)
private suspend fun doTasksExport(os: OutputStream?, taskIds: List<Long>) = withContext(Dispatchers.IO) {
val writer = os!!.bufferedWriter()
@ -146,11 +160,7 @@ class TasksJsonExporter @Inject constructor(
write("caldavCalendars", caldavDao.getCalendars())
write("taskListMetadata", taskListMetadataDao.getAll())
write("taskAttachments", taskAttachmentDao.getAttachments())
write("intPrefs", preferences.getPrefs(Integer::class.java))
write("longPrefs", preferences.getPrefs(java.lang.Long::class.java))
write("stringPrefs", preferences.getPrefs(String::class.java))
write("boolPrefs", preferences.getPrefs(java.lang.Boolean::class.java))
write("setPrefs", preferences.getPrefs(Set::class.java) as Map<String, Set<String>>, lastItem = true)
writePreferences()
write("}")
write("}")
}
@ -159,6 +169,14 @@ class TasksJsonExporter @Inject constructor(
exportCount = taskIds.size
}
private fun JsonWriter.writePreferences() {
write("intPrefs", preferences.getPrefs(Integer::class.java))
write("longPrefs", preferences.getPrefs(java.lang.Long::class.java))
write("stringPrefs", preferences.getPrefs(String::class.java))
write("boolPrefs", preferences.getPrefs(java.lang.Boolean::class.java))
write("setPrefs", preferences.getPrefs(Set::class.java) as Map<String, Set<String>>, lastItem = true)
}
private fun onFinishExport(outputFile: String) = post {
context?.toast(
R.string.export_toast,

@ -15,9 +15,11 @@ import com.todoroo.astrid.service.Upgrader.Companion.V12_4
import com.todoroo.astrid.service.Upgrader.Companion.V12_8
import com.todoroo.astrid.service.Upgrader.Companion.V6_4
import com.todoroo.astrid.service.Upgrader.Companion.getAndroidColor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.tasks.LocalBroadcastManager
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.R
import org.tasks.caldav.VtodoCache
import org.tasks.data.GoogleTaskAccount
@ -63,7 +65,7 @@ class TasksJsonImporter @Inject constructor(
private val userActivityDao: UserActivityDao,
private val taskDao: TaskDao,
private val locationDao: LocationDao,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
private val alarmDao: AlarmDao,
private val tagDao: TagDao,
private val filterDao: FilterDao,
@ -85,9 +87,37 @@ class TasksJsonImporter @Inject constructor(
handler.post { progressDialog.setMessage(message) }
}
suspend fun importTasks(context: Context, backupFile: Uri?, progressDialog: ProgressDialog?): ImportResult {
suspend fun importTasks(
context: Context,
backupFile: Uri?,
progressDialog: ProgressDialog?
): ImportResult = withContext(Dispatchers.IO) {
Timber.d("Importing backup file $backupFile")
val handler = Handler(context.mainLooper)
try {
val version = importMetadata(context, backupFile)
importTasks(context, backupFile, progressDialog, version)
if (version < Upgrader.V8_2) {
val themeIndex = preferences.getInt(R.string.p_theme_color, 7)
preferences.setInt(
R.string.p_theme_color,
getAndroidColor(context, themeIndex))
}
if (version < Upgrader.V9_6) {
taskMover.migrateLocalTasks()
}
Timber.d("Updating parents")
caldavDao.updateParents()
} catch (e: IOException) {
Timber.e(e)
}
refreshBroadcaster.broadcastRefresh()
result
}
private suspend fun importMetadata(
context: Context,
backupFile: Uri?,
): Int {
val `is`: InputStream? = try {
context.contentResolver.openInputStream(backupFile!!)
} catch (e: FileNotFoundException) {
@ -97,167 +127,192 @@ class TasksJsonImporter @Inject constructor(
val reader = JsonReader(bufferedReader)
reader.isLenient = true
val ignoreKeys = ignorePrefs.map { context.getString(it) }
try {
reader.beginObject()
var version = 0
while (reader.hasNext()) {
when (val name = reader.nextName()) {
"version" -> version = reader.nextInt().also { Timber.d("Backup version: $it") }
"timestamp" -> reader.nextLong().let { Timber.d("Backup timestamp: $it") }
"data" -> {
reader.beginObject()
while (reader.hasNext()) {
when (val element = reader.nextName()) {
"tasks" -> {
reader.forEach<TaskBackup> { backup ->
result.taskCount++
setProgressMessage(
handler,
progressDialog,
context.getString(R.string.import_progress_read, result.taskCount))
importTask(backup, version)
}
reader.beginObject()
var version = 0
while (reader.hasNext()) {
when (val name = reader.nextName()) {
"version" -> version = reader.nextInt().also { Timber.d("Backup version: $it") }
"timestamp" -> reader.nextLong().let { Timber.d("Backup timestamp: $it") }
"data" -> {
reader.beginObject()
while (reader.hasNext()) {
when (val element = reader.nextName()) {
"places" -> reader.forEach<Place> { place ->
if (locationDao.getByUid(place.uid!!) == null) {
locationDao.insert(
place.copy(icon = place.icon.migrateLegacyIcon())
)
}
"places" -> reader.forEach<Place> { place ->
if (locationDao.getByUid(place.uid!!) == null) {
locationDao.insert(
place.copy(icon = place.icon.migrateLegacyIcon())
)
}
}
"tags" -> reader.forEach<TagData> { tagData ->
findTagData(tagData)?.let {
return@forEach
}
"tags" -> reader.forEach<TagData> { tagData ->
findTagData(tagData)?.let {
return@forEach
}
tagDataDao.insert(
tagData.copy(
color = themeToColor(context, version, tagData.color ?: 0),
icon = tagData.icon.migrateLegacyIcon(),
)
tagDataDao.insert(
tagData.copy(
color = themeToColor(context, version, tagData.color ?: 0),
icon = tagData.icon.migrateLegacyIcon(),
)
}
"filters" -> reader.forEach<Filter> {
it
.let {
if (version < Upgrade_13_2.VERSION)
filterCriteriaProvider.rebuildFilter(it)
else
it
}
.let { filter ->
if (filterDao.getByName(filter.title!!) == null) {
filterDao.insert(
filter.copy(
color = themeToColor(context, version, filter.color ?: 0),
icon = filter.icon.migrateLegacyIcon(),
)
)
}
"filters" -> reader.forEach<Filter> {
it
.let {
if (version < Upgrade_13_2.VERSION)
filterCriteriaProvider.rebuildFilter(it)
else
it
}
.let { filter ->
if (filterDao.getByName(filter.title!!) == null) {
filterDao.insert(
filter.copy(
color = themeToColor(context, version, filter.color ?: 0),
icon = filter.icon.migrateLegacyIcon(),
)
}
)
}
}
"caldavAccounts" -> reader.forEach<CaldavAccount> { account ->
if (caldavDao.getAccountByUuid(account.uuid!!) == null) {
caldavDao.insert(account)
}
}
"caldavAccounts" -> reader.forEach<CaldavAccount> { account ->
if (caldavDao.getAccountByUuid(account.uuid!!) == null) {
caldavDao.insert(account)
}
"caldavCalendars" -> reader.forEach<CaldavCalendar> { calendar ->
if (caldavDao.getCalendarByUuid(calendar.uuid!!) == null) {
caldavDao.insert(
calendar.copy(
color = themeToColor(context, version, calendar.color),
icon = calendar.icon.migrateLegacyIcon(),
)
}
"caldavCalendars" -> reader.forEach<CaldavCalendar> { calendar ->
if (caldavDao.getCalendarByUuid(calendar.uuid!!) == null) {
caldavDao.insert(
calendar.copy(
color = themeToColor(context, version, calendar.color),
icon = calendar.icon.migrateLegacyIcon(),
)
}
)
}
"taskListMetadata" -> reader.forEach<TaskListMetadata> { tlm ->
val id = tlm.filter.takeIf { it?.isNotBlank() == true } ?: tlm.tagUuid!!
if (taskListMetadataDao.fetchByTagOrFilter(id) == null) {
taskListMetadataDao.insert(tlm)
}
}
"taskListMetadata" -> reader.forEach<TaskListMetadata> { tlm ->
val id = tlm.filter.takeIf { it?.isNotBlank() == true } ?: tlm.tagUuid!!
if (taskListMetadataDao.fetchByTagOrFilter(id) == null) {
taskListMetadataDao.insert(tlm)
}
"taskAttachments" -> reader.forEach<TaskAttachment> { attachment ->
if (taskAttachmentDao.getAttachment(attachment.remoteId) == null) {
taskAttachmentDao.insert(attachment)
}
}
"taskAttachments" -> reader.forEach<TaskAttachment> { attachment ->
if (taskAttachmentDao.getAttachment(attachment.remoteId) == null) {
taskAttachmentDao.insert(attachment)
}
"intPrefs" ->
Json.decodeFromString<Map<String, Integer>>(reader.jsonString())
.filterNot { (key, _) -> ignoreKeys.contains(key) }
.forEach { (k, v) -> preferences.setInt(k, v as Int) }
"longPrefs" ->
Json.decodeFromString<Map<String, java.lang.Long>>(reader.jsonString())
.filterNot { (key, _) -> ignoreKeys.contains(key) }
.forEach { (k, v) -> preferences.setLong(k, v as Long)}
"stringPrefs" ->
Json.decodeFromString<Map<String, String>>(reader.jsonString())
.filterNot { (k, _) -> ignoreKeys.contains(k) }
.forEach { (k, v) -> preferences.setString(k, v)}
"boolPrefs" ->
Json.decodeFromString<Map<String, java.lang.Boolean>>(reader.jsonString())
.filterNot { (k, _) -> ignoreKeys.contains(k) }
.forEach { (k, v) -> preferences.setBoolean(k, v as Boolean) }
"setPrefs" ->
Json.decodeFromString<Map<String, Set<String>>>(reader.jsonString())
.filterNot { (k, _) -> ignoreKeys.contains(k) }
.forEach { (k, v) -> preferences.setStringSet(k, v as HashSet<String>)}
"googleTaskAccounts" -> reader.forEach<GoogleTaskAccount> { googleTaskAccount ->
if (caldavDao.getAccount(TYPE_GOOGLE_TASKS, googleTaskAccount.account!!) == null) {
caldavDao.insert(
CaldavAccount(
accountType = TYPE_GOOGLE_TASKS,
uuid = googleTaskAccount.account,
name = googleTaskAccount.account,
username = googleTaskAccount.account,
)
}
"intPrefs" ->
Json.decodeFromString<Map<String, Integer>>(reader.jsonString())
.filterNot { (key, _) -> ignoreKeys.contains(key) }
.forEach { (k, v) -> preferences.setInt(k, v as Int) }
"longPrefs" ->
Json.decodeFromString<Map<String, java.lang.Long>>(reader.jsonString())
.filterNot { (key, _) -> ignoreKeys.contains(key) }
.forEach { (k, v) -> preferences.setLong(k, v as Long)}
"stringPrefs" ->
Json.decodeFromString<Map<String, String>>(reader.jsonString())
.filterNot { (k, _) -> ignoreKeys.contains(k) }
.forEach { (k, v) -> preferences.setString(k, v)}
"boolPrefs" ->
Json.decodeFromString<Map<String, java.lang.Boolean>>(reader.jsonString())
.filterNot { (k, _) -> ignoreKeys.contains(k) }
.forEach { (k, v) -> preferences.setBoolean(k, v as Boolean) }
"setPrefs" ->
Json.decodeFromString<Map<String, Set<String>>>(reader.jsonString())
.filterNot { (k, _) -> ignoreKeys.contains(k) }
.forEach { (k, v) -> preferences.setStringSet(k, v as HashSet<String>)}
"googleTaskAccounts" -> reader.forEach<GoogleTaskAccount> { googleTaskAccount ->
if (caldavDao.getAccount(TYPE_GOOGLE_TASKS, googleTaskAccount.account!!) == null) {
caldavDao.insert(
CaldavAccount(
accountType = TYPE_GOOGLE_TASKS,
uuid = googleTaskAccount.account,
name = googleTaskAccount.account,
username = googleTaskAccount.account,
)
}
)
}
"googleTaskLists" -> reader.forEach<GoogleTaskList> { googleTaskList ->
if (caldavDao.getCalendar(googleTaskList.remoteId!!) == null) {
caldavDao.insert(
CaldavCalendar(
account = googleTaskList.account,
uuid = googleTaskList.remoteId,
color = themeToColor(context, version, googleTaskList.color ?: 0),
icon = googleTaskList.icon?.toString().migrateLegacyIcon(),
)
}
"googleTaskLists" -> reader.forEach<GoogleTaskList> { googleTaskList ->
if (caldavDao.getCalendar(googleTaskList.remoteId!!) == null) {
caldavDao.insert(
CaldavCalendar(
account = googleTaskList.account,
uuid = googleTaskList.remoteId,
color = themeToColor(context, version, googleTaskList.color ?: 0),
icon = googleTaskList.icon?.toString().migrateLegacyIcon(),
)
}
}
else -> {
Timber.w("Skipping $element")
reader.skipValue()
)
}
}
else -> {
Timber.w("Skipping $element")
reader.skipValue()
}
}
reader.endObject()
}
else -> {
Timber.w("Skipping $name")
reader.skipValue()
}
reader.endObject()
}
else -> {
Timber.w("Skipping $name")
reader.skipValue()
}
}
if (version < Upgrader.V8_2) {
val themeIndex = preferences.getInt(R.string.p_theme_color, 7)
preferences.setInt(
R.string.p_theme_color,
getAndroidColor(context, themeIndex))
}
if (version < Upgrader.V9_6) {
taskMover.migrateLocalTasks()
}
reader.close()
bufferedReader.close()
`is`.close()
return version
}
private suspend fun importTasks(
context: Context,
backupFile: Uri?,
progressDialog: ProgressDialog?,
version: Int,
) {
val handler = Handler(context.mainLooper)
val `is`: InputStream? = try {
context.contentResolver.openInputStream(backupFile!!)
} catch (e: FileNotFoundException) {
throw IllegalStateException(e)
}
val bufferedReader = `is`!!.bufferedReader()
val reader = JsonReader(bufferedReader)
reader.isLenient = true
reader.beginObject()
while (reader.hasNext()) {
when (val name = reader.nextName()) {
"data" -> {
reader.beginObject()
while (reader.hasNext()) {
when (val element = reader.nextName()) {
"tasks" -> {
reader.forEach<TaskBackup> { backup ->
result.taskCount++
setProgressMessage(
handler,
progressDialog,
context.getString(R.string.import_progress_read, result.taskCount))
importTask(backup, version)
}
}
else -> {
Timber.w("Skipping $element")
reader.skipValue()
}
}
}
reader.endObject()
}
else -> {
Timber.w("Skipping $name")
reader.skipValue()
}
}
Timber.d("Updating parents")
caldavDao.updateParents()
reader.close()
bufferedReader.close()
`is`!!.close()
} catch (e: IOException) {
Timber.e(e)
}
localBroadcastManager.broadcastRefresh()
return result
reader.close()
bufferedReader.close()
`is`.close()
}
private suspend fun importTask(backup: TaskBackup, version: Int) {

@ -8,10 +8,16 @@ import android.os.Bundle
import android.text.TextUtils
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.activity.enableEdgeToEdge
import androidx.annotation.StringRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -36,6 +42,7 @@ import org.tasks.compose.ServerSelector
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL
import org.tasks.data.entity.Task
import org.tasks.databinding.ActivityCaldavAccountSettingsBinding
import org.tasks.dialogs.DialogBuilder
@ -69,8 +76,18 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityCaldavAccountSettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
binding.toolbar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = systemBars.top
}
binding.rootLayout.updatePadding(bottom = systemBars.bottom)
insets
}
caldavAccount = if (savedInstanceState == null) intent.getParcelableExtra(EXTRA_CALDAV_DATA) else savedInstanceState.getParcelable(EXTRA_CALDAV_DATA)
serverType = mutableStateOf(
savedInstanceState?.getInt(EXTRA_SERVER_TYPE, SERVER_UNKNOWN)
@ -116,7 +133,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.name, InputMethodManager.SHOW_IMPLICIT)
}
if (!inventory.hasPro) {
if (!inventory.hasPro && caldavAccount?.accountType != TYPE_LOCAL) {
newSnackbar(getString(R.string.this_feature_requires_a_subscription))
.setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE)
.setAction(R.string.button_subscribe) {
@ -308,7 +325,8 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
private fun newSnackbar(message: String?): Snackbar {
val snackbar = Snackbar.make(binding.rootLayout, message!!, 8000)
.setTextColor(getColor(R.color.snackbar_text_color))
.setBackgroundTint(getColor(R.color.dialog_background))
.setTextColor(getColor(R.color.text_primary))
.setActionTextColor(getColor(R.color.snackbar_action_color))
snackbar
.view
@ -341,7 +359,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
}
}
private fun removeAccountPrompt() {
protected open suspend fun removeAccountPrompt() {
if (requestInProgress()) {
return
}
@ -378,7 +396,9 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_help -> openUri(helpUrl)
R.id.remove -> removeAccountPrompt()
R.id.remove -> lifecycleScope.launch {
removeAccountPrompt()
}
}
return onOptionsItemSelected(item)
}

@ -78,11 +78,10 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
showProgressIndicator()
createCalendar(caldavAccount, name, baseViewModel.color)
}
nameChanged() || colorChanged() -> {
nameChanged() || colorChanged() || iconChanged() -> {
showProgressIndicator()
updateNameAndColor(caldavAccount, caldavCalendar!!, name, baseViewModel.color)
}
iconChanged() -> updateCalendar()
else -> finish()
}
}
@ -150,7 +149,7 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
)
caldavDao.update(result)
setResult(
Activity.RESULT_OK,
RESULT_OK,
Intent(TaskListFragment.ACTION_RELOAD)
.putExtra(
MainActivity.OPEN_FILTER,

@ -15,7 +15,6 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
@ -35,7 +34,6 @@ import org.tasks.data.entity.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.themes.TasksTheme
import org.tasks.themes.colorOn
import javax.inject.Inject
@AndroidEntryPoint
@ -72,7 +70,7 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
val openDialog = rememberSaveable { mutableStateOf(false) }
ShareInviteDialog(
openDialog,
email = caldavAccount.serverType != SERVER_OWNCLOUD
email = caldavAccount.serverType !in listOf(SERVER_OWNCLOUD, SERVER_NEXTCLOUD),
) { input ->
lifecycleScope.launch {
share(input)

@ -12,6 +12,8 @@ import org.tasks.data.UUIDHelper
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.PrincipalDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_NEXTCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.entity.CaldavCalendar.Companion.INVITE_UNKNOWN
@ -37,7 +39,7 @@ class CaldavCalendarViewModel @Inject constructor(
): CaldavCalendar? =
doRequest {
val url = withContext(Dispatchers.IO) {
provider.forAccount(caldavAccount).makeCollection(name, color)
provider.forAccount(caldavAccount).makeCollection(name, color, icon)
}
val calendar = CaldavCalendar(
uuid = UUIDHelper.newUUID(),
@ -67,7 +69,7 @@ class CaldavCalendarViewModel @Inject constructor(
) =
doRequest {
withContext(Dispatchers.IO) {
provider.forAccount(account, calendar.url!!).updateCollection(name, color)
provider.forAccount(account, calendar.url!!).updateCollection(name, color, icon)
}
val result = calendar.copy(
name = name,
@ -96,10 +98,10 @@ class CaldavCalendarViewModel @Inject constructor(
list: CaldavCalendar,
input: String
) = doRequest {
val href = if (account.serverType == CaldavAccount.SERVER_OWNCLOUD)
"principal:principals/users/$input"
else
"mailto:$input"
val href = when (account.serverType) {
SERVER_OWNCLOUD, SERVER_NEXTCLOUD -> "principal:principals/users/$input"
else -> "mailto:$input"
}
withContext(Dispatchers.IO) {
provider.forAccount(account, list.url!!).share(account, href)
}

@ -11,24 +11,32 @@ import at.bitfire.dav4jvm.XmlUtils.NS_CALDAV
import at.bitfire.dav4jvm.XmlUtils.NS_WEBDAV
import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.property.*
import at.bitfire.dav4jvm.property.CalendarColor
import at.bitfire.dav4jvm.property.CalendarHomeSet
import at.bitfire.dav4jvm.property.CurrentUserPrincipal
import at.bitfire.dav4jvm.property.CurrentUserPrivilegeSet
import at.bitfire.dav4jvm.property.DisplayName
import at.bitfire.dav4jvm.property.GetCTag
import at.bitfire.dav4jvm.property.ResourceType
import at.bitfire.dav4jvm.property.ResourceType.Companion.CALENDAR
import org.tasks.data.UUIDHelper
import at.bitfire.dav4jvm.property.SupportedCalendarComponentSet
import at.bitfire.dav4jvm.property.SyncToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.property.CalendarIcon
import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCInvite
import org.tasks.caldav.property.OCOwnerPrincipal
import org.tasks.caldav.property.PropertyUtils.NS_OWNCLOUD
import org.tasks.caldav.property.ShareAccess
import org.tasks.data.UUIDHelper
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_NEXTCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD
@ -101,9 +109,12 @@ open class CaldavClient(
.findHomeset()
}
suspend fun calendars(interceptor: (Interceptor.Chain) -> okhttp3.Response): List<Response> =
suspend fun calendars(interceptor: (okhttp3.Response) -> okhttp3.Response = { it }): List<Response> =
DavResource(
httpClient.newBuilder().addNetworkInterceptor(interceptor).build(),
httpClient
.newBuilder()
.addNetworkInterceptor { interceptor(it.proceed(it.request())) }
.build(),
httpUrl!!
)
.propfind(1, *calendarProperties)
@ -120,33 +131,44 @@ open class CaldavClient(
}
@Throws(IOException::class, XmlPullParserException::class, HttpException::class)
suspend fun makeCollection(displayName: String, color: Int): String = withContext(Dispatchers.IO) {
suspend fun makeCollection(displayName: String, color: Int, icon: String?): String = withContext(Dispatchers.IO) {
val davResource = DavResource(httpClient, httpUrl!!.resolve(UUIDHelper.newUUID() + "/")!!)
val mkcolString = getMkcolString(displayName, color)
davResource.mkCol(mkcolString) {}
if (icon?.isNotBlank() == true) {
davResource.proppatch(CalendarIcon.NAME, icon)
}
davResource.location.toString()
}
@Throws(IOException::class, XmlPullParserException::class, HttpException::class)
suspend fun updateCollection(displayName: String, color: Int): String =
suspend fun updateCollection(displayName: String, color: Int, icon: String?): String =
withContext(Dispatchers.IO) {
with(DavResource(httpClient, httpUrl!!)) {
proppatch(
setProperties = mutableMapOf(DisplayName.NAME to displayName).apply {
if (color != 0) {
put(
CalendarColor.NAME,
String.format("#%06X%02X", color and 0xFFFFFF, color ushr 24)
)
}
},
removeProperties = if (color == 0) listOf(CalendarColor.NAME) else emptyList(),
callback = { _, _ -> },
)
proppatch(DisplayName.NAME, displayName)
if (color != 0) {
proppatch(
CalendarColor.NAME,
String.format("#%06X%02X", color and 0xFFFFFF, color ushr 24)
)
}
if (icon?.isNotBlank() == true) {
proppatch(CalendarIcon.NAME, icon)
}
location.toString()
}
}
@Throws(IOException::class, XmlPullParserException::class, HttpException::class)
suspend fun updateIcon(url: HttpUrl, icon: String?, onFailure: () -> Unit) =
withContext(Dispatchers.IO) {
with(DavResource(httpClient, url)) {
if (icon?.isNotBlank() == true) {
proppatch(CalendarIcon.NAME, icon, onFailure)
}
}
}
@Throws(IOException::class, XmlPullParserException::class)
private fun getMkcolString(displayName: String, color: Int): String {
val xmlPullParserFactory = XmlPullParserFactory.newInstance()
@ -203,8 +225,8 @@ open class CaldavClient(
href: String,
) {
when (account.serverType) {
SERVER_TASKS, SERVER_SABREDAV, SERVER_NEXTCLOUD -> shareSabredav(href)
SERVER_OWNCLOUD -> shareOwncloud(href)
SERVER_TASKS, SERVER_SABREDAV -> shareSabredav(href)
SERVER_OWNCLOUD, SERVER_NEXTCLOUD -> shareOwncloud(href)
else -> throw IllegalArgumentException()
}
}
@ -243,8 +265,8 @@ open class CaldavClient(
href: String,
) {
when (account.serverType) {
SERVER_TASKS, SERVER_SABREDAV, SERVER_NEXTCLOUD -> removeSabrePrincipal(calendar, href)
SERVER_OWNCLOUD -> removeOwncloudPrincipal(calendar, href)
SERVER_TASKS, SERVER_SABREDAV -> removeSabrePrincipal(calendar, href)
SERVER_OWNCLOUD, SERVER_NEXTCLOUD -> removeOwncloudPrincipal(calendar, href)
else -> throw IllegalArgumentException()
}
}
@ -284,18 +306,19 @@ open class CaldavClient(
private val MEDIATYPE_SHARING = "application/davsharing+xml".toMediaType()
private val calendarProperties = arrayOf(
ResourceType.NAME,
DisplayName.NAME,
SupportedCalendarComponentSet.NAME,
GetCTag.NAME,
CalendarColor.NAME,
SyncToken.NAME,
ShareAccess.NAME,
Invite.NAME,
OCOwnerPrincipal.NAME,
OCInvite.NAME,
CurrentUserPrivilegeSet.NAME,
CurrentUserPrincipal.NAME,
ResourceType.NAME,
DisplayName.NAME,
SupportedCalendarComponentSet.NAME,
GetCTag.NAME,
CalendarColor.NAME,
SyncToken.NAME,
ShareAccess.NAME,
Invite.NAME,
OCOwnerPrincipal.NAME,
OCInvite.NAME,
CurrentUserPrivilegeSet.NAME,
CurrentUserPrincipal.NAME,
CalendarIcon.NAME,
)
private suspend fun DavResource.propfind(
@ -311,5 +334,22 @@ open class CaldavClient(
cont.resumeWith(Result.success(responses))
}
}
fun DavResource.proppatch(
property: Property.Name,
value: String,
onFailure: () -> Unit = {},
) {
proppatch(
setProperties = mapOf(property to value),
removeProperties = emptyList(),
callback = { response, _ ->
if (!response.isSuccess()) {
Timber.e("${response.status} when updating $property: ${response.error}")
onFailure()
}
},
)
}
}
}
}

@ -32,18 +32,18 @@ import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.broadcast.RefreshBroadcaster
import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.caldav.property.CalendarIcon
import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCAccess
import org.tasks.caldav.property.OCInvite
import org.tasks.caldav.property.OCOwnerPrincipal
import org.tasks.caldav.property.OCUser
import org.tasks.caldav.property.PropertyUtils.register
import org.tasks.caldav.property.ShareAccess
import org.tasks.caldav.property.ShareAccess.Companion.NOT_SHARED
import org.tasks.caldav.property.ShareAccess.Companion.NO_ACCESS
@ -56,6 +56,7 @@ import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.PrincipalDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.ERROR_UNAUTHORIZED
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_NEXTCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_SABREDAV
@ -84,7 +85,7 @@ class CaldavSynchronizer @Inject constructor(
@param:ApplicationContext private val context: Context,
private val caldavDao: CaldavDao,
private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
private val refreshBroadcaster: RefreshBroadcaster,
private val taskDeleter: TaskDeleter,
private val inventory: Inventory,
private val firebase: Firebase,
@ -136,8 +137,7 @@ class CaldavSynchronizer @Inject constructor(
private suspend fun synchronize(account: CaldavAccount) {
val caldavClient = provider.forAccount(account)
var serverType = account.serverType
val resources = caldavClient.calendars { chain ->
val response = chain.proceed(chain.request())
val resources = caldavClient.calendars { response ->
if (serverType == SERVER_UNKNOWN) {
serverType = getServerType(account, response.headers)
}
@ -155,8 +155,10 @@ class CaldavSynchronizer @Inject constructor(
val url = resource.href.toString()
var calendar = caldavDao.getCalendarByUrl(account.uuid!!, url)
val remoteName = resource[DisplayName::class.java]!!.displayName
val calendarColor = resource[CalendarColor::class.java]
val color = resource[CalendarColor::class.java]?.color ?: 0
val access = resource.accessLevel
val icon = resource[CalendarIcon::class.java]?.icon?.takeIf { it.isNotBlank() }
if (access == ACCESS_UNKNOWN) {
firebase.logEvent(
R.string.event_sync_unknown_access,
@ -164,7 +166,6 @@ class CaldavSynchronizer @Inject constructor(
(resource[ShareAccess::class.java]?.access?.toString() ?: "???")
)
}
val color = calendarColor?.color ?: 0
if (calendar == null) {
calendar = CaldavCalendar(
name = remoteName,
@ -173,17 +174,22 @@ class CaldavSynchronizer @Inject constructor(
uuid = UUIDHelper.newUUID(),
color = color,
access = access,
icon = icon,
)
caldavDao.insert(calendar)
} else if (calendar.name != remoteName
|| calendar.color != color
|| calendar.access != access
|| calendar.color != color
|| calendar.access != access
|| (icon != null && calendar.icon != icon)
) {
calendar.color = color
calendar.name = remoteName
calendar.access = access
calendar = calendar.copy(
color = color,
name = remoteName,
access = access,
icon = icon ?: calendar.icon,
)
caldavDao.update(calendar)
localBroadcastManager.broadcastRefreshList()
refreshBroadcaster.broadcastRefresh()
}
resource
.principals(account, calendar)
@ -198,7 +204,11 @@ class CaldavSynchronizer @Inject constructor(
private fun getServerType(account: CaldavAccount, headers: Headers) = when {
account.isTasksOrg -> SERVER_TASKS
headers["DAV"]?.contains("oc-resource-sharing") == true -> SERVER_OWNCLOUD
headers["DAV"]?.contains("oc-resource-sharing") == true ->
if (headers["DAV"]?.let { it.contains("nextcloud-") || it.contains("nc-") } == true)
SERVER_NEXTCLOUD
else
SERVER_OWNCLOUD
headers["x-sabre-version"]?.isNotBlank() == true -> SERVER_SABREDAV
headers["server"] == "Openexchange WebDAV" -> SERVER_OPEN_XCHANGE
else -> SERVER_UNKNOWN
@ -215,7 +225,7 @@ class CaldavSynchronizer @Inject constructor(
}
account.error = message
caldavDao.update(account)
localBroadcastManager.broadcastRefreshList()
refreshBroadcaster.broadcastRefresh()
if (!isNullOrEmpty(message)) {
Timber.e(message)
}
@ -292,7 +302,7 @@ class CaldavSynchronizer @Inject constructor(
caldavDao.update(caldavCalendar)
Timber.d("Updating parents for ${caldavCalendar.uuid}")
caldavDao.updateParents(caldavCalendar.uuid!!)
localBroadcastManager.broadcastRefresh()
refreshBroadcaster.broadcastRefresh()
}
private suspend fun pushLocalChanges(
@ -320,9 +330,17 @@ class CaldavSynchronizer @Inject constructor(
caldavTask: CaldavTask
): Boolean {
try {
if (!isNullOrEmpty(caldavTask.obj)) {
val objectId = caldavTask.obj
?: run {
Timber.e("null obj for caldavTask.id=${caldavTask.id} task.id=${caldavTask.task}")
caldavTask.obj = caldavTask.remoteId?.let { "$it.ics" }
caldavTask.obj
}
if (objectId?.isNotBlank() == true) {
val remote = DavResource(
httpClient, httpUrl.newBuilder().addPathSegment(caldavTask.obj!!).build())
httpClient = httpClient,
location = httpUrl.newBuilder().addPathSegment(objectId).build(),
)
remote.delete(null) {}
}
} catch (e: HttpException) {
@ -346,8 +364,8 @@ class CaldavSynchronizer @Inject constructor(
httpClient: OkHttpClient,
httpUrl: HttpUrl
) {
Timber.d("pushing %s", task)
val caldavTask = caldavDao.getTask(task.id) ?: return
Timber.d("pushing caldavTask=$caldavTask task=$task")
if (task.isDeleted) {
if (deleteRemoteResource(httpClient, httpUrl, calendar, caldavTask)) {
taskDeleter.delete(task)
@ -356,9 +374,19 @@ class CaldavSynchronizer @Inject constructor(
}
val data = iCal.toVtodo(account, calendar, caldavTask, task)
val requestBody = data.toRequestBody(contentType = MIME_ICALENDAR)
val objPath = caldavTask.obj
?: run {
Timber.e("null obj for caldavTask.id=${caldavTask.id} task.id=${task.id}")
caldavTask.obj = caldavTask.remoteId?.let { "$it.ics" }
caldavTask.obj
}
?: throw IllegalStateException("Push failed - missing UUID")
try {
val remote = DavResource(
httpClient, httpUrl.newBuilder().addPathSegment(caldavTask.obj!!).build())
httpClient = httpClient,
location = httpUrl.newBuilder().addPathSegment(objPath).build(),
)
remote.put(requestBody) {
if (it.isSuccessful) {
fromResponse(it)?.eTag?.takeIf(String::isNotBlank)?.let { etag ->
@ -436,10 +464,13 @@ class CaldavSynchronizer @Inject constructor(
fun registerFactories() {
PropertyRegistry.register(
ShareAccess.Factory(),
Invite.Factory(),
OCOwnerPrincipal.Factory(),
OCInvite.Factory(),
listOf(
ShareAccess.Factory(),
Invite.Factory(),
OCOwnerPrincipal.Factory(),
OCInvite.Factory(),
CalendarIcon.Factory,
)
)
}
@ -488,4 +519,4 @@ class CaldavSynchronizer @Inject constructor(
else -> INVITE_UNKNOWN
}
}
}
}

@ -0,0 +1,90 @@
package org.tasks.caldav
import android.app.Activity
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.analytics.Constants
import org.tasks.data.UUIDHelper
import org.tasks.data.entity.CaldavAccount
@AndroidEntryPoint
class LocalAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.userLayout.visibility = View.GONE
binding.passwordLayout.visibility = View.GONE
binding.urlLayout.visibility = View.GONE
binding.serverSelector.visibility = View.GONE
}
override fun hasChanges() = newName != caldavAccount!!.name
override fun save() = lifecycleScope.launch {
if (newName.isBlank()) {
binding.nameLayout.error = getString(R.string.name_cannot_be_empty)
return@launch
}
updateAccount()
}
private suspend fun addAccount() {
caldavDao.insert(
CaldavAccount(
name = newName,
uuid = UUIDHelper.newUUID(),
)
)
firebase.logEvent(
R.string.event_sync_add_account,
R.string.param_type to Constants.SYNC_TYPE_LOCAL
)
setResult(Activity.RESULT_OK)
finish()
}
override suspend fun updateAccount() {
caldavAccount!!.name = newName
caldavDao.update(caldavAccount!!)
setResult(Activity.RESULT_OK)
finish()
}
override suspend fun addAccount(url: String, username: String, password: String) {
addAccount()
}
override suspend fun updateAccount(url: String, username: String, password: String) {
updateAccount()
}
override suspend fun removeAccountPrompt() {
val countTasks = caldavAccount?.uuid?.let { caldavDao.countTasks(it) } ?: 0
val countString = resources.getQuantityString(R.plurals.task_count, countTasks, countTasks)
dialogBuilder
.newDialog()
.setTitle(
R.string.delete_tag_confirmation,
caldavAccount?.name?.takeIf { it.isNotBlank() } ?: getString(R.string.local_lists)
)
.apply {
if (countTasks > 0) {
setMessage(R.string.delete_tasks_warning, countString)
} else {
setMessage(R.string.logout_warning)
}
}
.setPositiveButton(R.string.delete) { _, _ -> lifecycleScope.launch { removeAccount() } }
.setNegativeButton(R.string.cancel, null)
.show()
}
override val newPassword: String? = null
override val helpUrl = R.string.url_caldav
}

@ -3,26 +3,20 @@ package org.tasks.caldav
import android.os.Bundle
import androidx.activity.compose.setContent
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import org.tasks.compose.DeleteButton
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.themes.TasksTheme
@AndroidEntryPoint
class LocalListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val canDelete = runBlocking { caldavDao.getCalendarsByAccount(CaldavDao.LOCAL).size > 1 }
setContent {
TasksTheme {
BaseCaldavSettingsContent (
optionButton = { if (!isNew && canDelete) DeleteButton(caldavCalendar?.name ?: "") { delete() } }
optionButton = { if (!isNew) DeleteButton(caldavCalendar?.name ?: "") { delete() } }
)
}
}
@ -35,6 +29,7 @@ class LocalListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) =
updateCalendar()
// TODO: prevent deleting the last list
override suspend fun deleteCalendar(caldavAccount: CaldavAccount, caldavCalendar: CaldavCalendar) =
onDeleted(true)
}
}

@ -41,7 +41,6 @@ import org.tasks.data.entity.Alarm.Companion.TYPE_RANDOM
import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Place
import org.tasks.data.entity.TagData
@ -208,7 +207,7 @@ class iCalendar @Inject constructor(
val task = existing?.task
?.let { taskDao.fetch(it) }
?: taskCreator.createWithValues("").apply {
readOnly = calendar.access == ACCESS_READ_ONLY
readOnly = calendar.readOnly()
taskDao.createNew(this)
}
val caldavTask =

@ -0,0 +1,32 @@
package org.tasks.caldav.property
import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.PropertyFactory
import at.bitfire.dav4jvm.XmlUtils
import org.xmlpull.v1.XmlPullParser
import timber.log.Timber
data class CalendarIcon(
val icon: String,
): Property {
companion object Companion {
@JvmField
val NAME = Property.Name(PropertyUtils.NS_TASKS, "x-calendar-icon")
}
object Factory: PropertyFactory {
override fun getName() = NAME
override fun create(parser: XmlPullParser): CalendarIcon? {
XmlUtils.readText(parser)?.takeIf { it.isNotBlank() }?.let {
try {
return CalendarIcon(it)
} catch (e: IllegalArgumentException) {
Timber.e(e, "Couldn't parse icon: $it")
}
}
return null
}
}
}

@ -1,10 +1,6 @@
package org.tasks.caldav.property
import at.bitfire.dav4jvm.PropertyFactory
import at.bitfire.dav4jvm.PropertyRegistry
object PropertyUtils {
const val NS_TASKS = "http://org.tasks/ns/"
const val NS_OWNCLOUD = "http://owncloud.org/ns"
fun PropertyRegistry.register(vararg factories: PropertyFactory) = register(factories.toList())
}
}

@ -16,7 +16,6 @@ import org.tasks.themes.TasksTheme
fun AddAccountDialog(
hasTasksAccount: Boolean,
hasPro: Boolean,
enableMicrosoftSync: Boolean = true,
selected: (Platform) -> Unit,
) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
@ -36,15 +35,13 @@ fun AddAccountDialog(
icon = R.drawable.ic_google,
onClick = { selected(Platform.GOOGLE_TASKS) }
)
if (enableMicrosoftSync) {
SyncAccount(
title = R.string.microsoft,
cost = if (hasPro) null else R.string.cost_free,
description = R.string.microsoft_selection_description,
icon = R.drawable.ic_microsoft_tasks,
onClick = { selected(Platform.MICROSOFT) }
)
}
SyncAccount(
title = R.string.microsoft,
cost = if (hasPro) null else R.string.cost_free,
description = R.string.microsoft_selection_description,
icon = R.drawable.ic_microsoft_tasks,
onClick = { selected(Platform.MICROSOFT) }
)
SyncAccount(
title = R.string.davx5,
cost = if (hasPro) null else R.string.cost_money,

@ -25,10 +25,11 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@ -62,141 +63,99 @@ import java.util.concurrent.TimeUnit
@ExperimentalComposeUiApi
object AddReminderDialog {
// Helper functions for converting between Alarm properties and UI state
private fun unitIndexToMillis(unitIndex: Int): Long = when (unitIndex) {
1 -> TimeUnit.HOURS.toMillis(1)
2 -> TimeUnit.DAYS.toMillis(1)
3 -> TimeUnit.DAYS.toMillis(7)
else -> TimeUnit.MINUTES.toMillis(1)
}
private fun timeToAmountAndUnit(time: Long): Pair<Int, Int> {
val absTime = kotlin.math.abs(time)
return when {
absTime == 0L -> 0 to 0 // Default to minutes when time is 0
absTime % TimeUnit.DAYS.toMillis(7) == 0L ->
(absTime / TimeUnit.DAYS.toMillis(7)).toInt() to 3
absTime % TimeUnit.DAYS.toMillis(1) == 0L ->
(absTime / TimeUnit.DAYS.toMillis(1)).toInt() to 2
absTime % TimeUnit.HOURS.toMillis(1) == 0L ->
(absTime / TimeUnit.HOURS.toMillis(1)).toInt() to 1
else ->
(absTime / TimeUnit.MINUTES.toMillis(1)).toInt() to 0
}
}
@Composable
fun AddRandomReminderDialog(
viewState: ViewState,
addAlarm: (Alarm) -> Unit,
alarm: Alarm?,
updateAlarm: (Alarm) -> Unit,
closeDialog: () -> Unit,
) {
val time = rememberSaveable { mutableStateOf(15) }
val units = rememberSaveable { mutableStateOf(0) }
if (viewState.showRandomDialog) {
AlertDialog(
onDismissRequest = closeDialog,
text = { AddRandomReminder(time, units) },
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
time.value.takeIf { it > 0 }?.let { i ->
addAlarm(Alarm(time = i * units.millis, type = TYPE_RANDOM))
closeDialog()
}
})
},
dismissButton = {
Constants.TextButton(
text = R.string.cancel,
onClick = closeDialog
)
},
)
} else {
time.value = 15
units.value = 0
// Create working copy from alarm or use defaults
var workingCopy by rememberSaveable {
mutableStateOf(alarm ?: Alarm(time = 15 * TimeUnit.MINUTES.toMillis(1), type = TYPE_RANDOM))
}
AlertDialog(
onDismissRequest = closeDialog,
text = {
AddRandomReminder(
alarm = workingCopy,
updateAlarm = { workingCopy = it }
)
},
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
val (amount, _) = timeToAmountAndUnit(workingCopy.time)
if (amount > 0) {
updateAlarm(workingCopy)
closeDialog()
}
})
},
dismissButton = {
Constants.TextButton(
text = R.string.cancel,
onClick = closeDialog
)
},
)
}
@Composable
fun AddCustomReminderDialog(
viewState: ViewState,
addAlarm: (Alarm) -> Unit,
alarm: Alarm?,
updateAlarm: (Alarm) -> Unit,
closeDialog: () -> Unit,
) {
val openDialog = viewState.showCustomDialog
val time = rememberSaveable { mutableStateOf(15) }
val units = rememberSaveable { mutableStateOf(0) }
val openRecurringDialog = rememberSaveable { mutableStateOf(false) }
val interval = rememberSaveable { mutableStateOf(0) }
val recurringUnits = rememberSaveable { mutableStateOf(0) }
val repeat = rememberSaveable { mutableStateOf(0) }
if (openDialog) {
if (!openRecurringDialog.value) {
AlertDialog(
onDismissRequest = closeDialog,
text = {
AddCustomReminder(
time,
units,
interval,
recurringUnits,
repeat,
showRecurring = {
openRecurringDialog.value = true
}
)
},
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
time.value.takeIf { it >= 0 }?.let { i ->
addAlarm(
Alarm(
time = -1 * i * units.millis,
type = TYPE_REL_END,
repeat = repeat.value,
interval = interval.value * recurringUnits.millis
)
)
closeDialog()
}
})
},
dismissButton = {
Constants.TextButton(
text = R.string.cancel,
onClick = closeDialog
)
},
// Create working copy from alarm or use defaults
var workingCopy by rememberSaveable {
mutableStateOf(
alarm ?: Alarm(
time = -1 * 15 * TimeUnit.MINUTES.toMillis(1),
type = TYPE_REL_END
)
}
AddRepeatReminderDialog(
openDialog = openRecurringDialog,
initialInterval = interval.value,
initialUnits = recurringUnits.value,
initialRepeat = repeat.value,
selected = { i, u, r ->
interval.value = i
recurringUnits.value = u
repeat.value = r
}
)
} else {
time.value = 15
units.value = 0
interval.value = 0
recurringUnits.value = 0
repeat.value = 0
}
}
var showRecurringDialog by rememberSaveable { mutableStateOf(false) }
@Composable
fun AddRepeatReminderDialog(
openDialog: MutableState<Boolean>,
initialInterval: Int,
initialUnits: Int,
initialRepeat: Int,
selected: (Int, Int, Int) -> Unit,
) {
val interval = rememberSaveable { mutableStateOf(initialInterval) }
val units = rememberSaveable { mutableStateOf(initialUnits) }
val repeat = rememberSaveable { mutableStateOf(initialRepeat) }
val closeDialog = {
openDialog.value = false
}
if (openDialog.value) {
if (!showRecurringDialog) {
AlertDialog(
onDismissRequest = closeDialog,
text = {
AddRecurringReminder(
openDialog.value,
interval,
units,
repeat,
AddCustomReminder(
alarm = workingCopy,
updateAlarm = { workingCopy = it },
showRecurring = { showRecurringDialog = true }
)
},
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
if (interval.value > 0 && repeat.value > 0) {
selected(interval.value, units.value, repeat.value)
openDialog.value = false
val (amount, _) = timeToAmountAndUnit(workingCopy.time)
if (amount >= 0) {
updateAlarm(workingCopy)
closeDialog()
}
})
},
@ -207,19 +166,74 @@ object AddReminderDialog {
)
},
)
} else {
interval.value = initialInterval.takeIf { it > 0 } ?: 15
units.value = initialUnits
repeat.value = initialRepeat.takeIf { it > 0 } ?: 4
}
if (showRecurringDialog) {
AddRepeatReminderDialog(
alarm = workingCopy,
updateAlarm = { workingCopy = it },
closeDialog = { showRecurringDialog = false }
)
}
}
@Composable
fun AddRepeatReminderDialog(
alarm: Alarm,
updateAlarm: (Alarm) -> Unit,
closeDialog: () -> Unit,
) {
// Create working copy with defaults if no recurrence set
var workingCopy by rememberSaveable {
mutableStateOf(
if (alarm.interval == 0L && alarm.repeat == 0) {
// Default to 15 minutes, 4 times
alarm.copy(
interval = 15 * TimeUnit.MINUTES.toMillis(1),
repeat = 4
)
} else {
alarm
}
)
}
AlertDialog(
onDismissRequest = closeDialog,
text = {
AddRecurringReminder(
alarm = workingCopy,
updateAlarm = { workingCopy = it }
)
},
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
val (intervalAmount, _) = timeToAmountAndUnit(workingCopy.interval)
if (intervalAmount > 0 && workingCopy.repeat > 0) {
updateAlarm(workingCopy)
closeDialog()
}
})
},
dismissButton = {
Constants.TextButton(
text = R.string.cancel,
onClick = closeDialog
)
},
)
}
@Composable
fun AddRandomReminder(
time: MutableState<Int>,
units: MutableState<Int>,
alarm: Alarm,
updateAlarm: (Alarm) -> Unit,
) {
val (initialAmount, initialUnit) = timeToAmountAndUnit(alarm.time)
var selectedUnit by rememberSaveable { mutableStateOf(initialUnit) }
val amount = if (alarm.time == 0L) 0 else (alarm.time / unitIndexToMillis(selectedUnit)).toInt()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
@ -228,14 +242,27 @@ object AddReminderDialog {
CenteredH6(text = stringResource(id = R.string.randomly_every, "").trim())
val focusRequester = remember { FocusRequester() }
OutlinedIntInput(
time,
value = amount,
onValueChange = { newAmount ->
val amt = newAmount ?: 0
updateAlarm(alarm.copy(time = amt * unitIndexToMillis(selectedUnit)))
},
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
)
Spacer(modifier = Modifier.height(16.dp))
options.forEachIndexed { index, option ->
RadioRow(index, option, time, units)
RadioRow(
index = index,
option = option,
timeAmount = amount,
unitIndex = selectedUnit,
onUnitSelected = { newUnit ->
selectedUnit = newUnit
updateAlarm(alarm.copy(time = amount * unitIndexToMillis(newUnit)))
}
)
}
ShowKeyboard(true, focusRequester)
}
@ -243,14 +270,19 @@ object AddReminderDialog {
@Composable
fun AddCustomReminder(
time: MutableState<Int>,
units: MutableState<Int>,
interval: MutableState<Int>,
recurringUnits: MutableState<Int>,
repeat: MutableState<Int>,
alarm: Alarm,
updateAlarm: (Alarm) -> Unit,
showRecurring: () -> Unit,
) {
val (initialAmount, initialUnit) = timeToAmountAndUnit(alarm.time)
var selectedUnit by rememberSaveable { mutableStateOf(initialUnit) }
val amount = if (alarm.time == 0L) 0 else kotlin.math.abs(alarm.time / unitIndexToMillis(selectedUnit)).toInt()
val (initialIntervalAmount, initialIntervalUnit) = timeToAmountAndUnit(alarm.interval)
val intervalAmount = if (alarm.interval == 0L) 0 else (alarm.interval / unitIndexToMillis(initialIntervalUnit)).toInt()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
@ -259,7 +291,11 @@ object AddReminderDialog {
CenteredH6(resId = R.string.custom_notification)
val focusRequester = remember { FocusRequester() }
OutlinedIntInput(
time,
value = amount,
onValueChange = { newAmount ->
val amt = newAmount ?: 0
updateAlarm(alarm.copy(time = -1 * amt * unitIndexToMillis(selectedUnit)))
},
minValue = 0,
modifier = Modifier
.fillMaxWidth()
@ -267,7 +303,17 @@ object AddReminderDialog {
)
Spacer(modifier = Modifier.height(16.dp))
options.forEachIndexed { index, option ->
RadioRow(index, option, time, units, R.string.alarm_before_due)
RadioRow(
index = index,
option = option,
timeAmount = amount,
unitIndex = selectedUnit,
onUnitSelected = { newUnit ->
selectedUnit = newUnit
updateAlarm(alarm.copy(time = -1 * amount * unitIndexToMillis(newUnit)))
},
formatString = R.string.alarm_before_due
)
}
Divider(modifier = Modifier.padding(vertical = 4.dp), thickness = 1.dp)
Row(modifier = Modifier
@ -288,11 +334,11 @@ object AddReminderDialog {
),
)
}
val repeating = repeat.value > 0 && interval.value > 0
val repeating = alarm.repeat > 0 && intervalAmount > 0
val text = if (repeating) {
LocalContext.current.resources.getRepeatString(
repeat.value,
interval.value * recurringUnits.millis
alarm.repeat,
alarm.interval
)
} else {
stringResource(id = R.string.repeat_option_does_not_repeat)
@ -305,11 +351,9 @@ object AddReminderDialog {
.align(CenterVertically)
)
if (repeating) {
ClearButton {
repeat.value = 0
interval.value = 0
recurringUnits.value = 0
}
ClearButton(onClick = {
updateAlarm(alarm.copy(repeat = 0, interval = 0))
})
}
}
ShowKeyboard(true, focusRequester)
@ -318,12 +362,14 @@ object AddReminderDialog {
@Composable
fun AddRecurringReminder(
openDialog: Boolean,
interval: MutableState<Int>,
units: MutableState<Int>,
repeat: MutableState<Int>
alarm: Alarm,
updateAlarm: (Alarm) -> Unit,
) {
val (initialIntervalAmount, initialIntervalUnit) = timeToAmountAndUnit(alarm.interval)
var selectedUnit by rememberSaveable { mutableStateOf(initialIntervalUnit) }
val intervalAmount = if (alarm.interval == 0L) 0 else (alarm.interval / unitIndexToMillis(selectedUnit)).toInt()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
@ -332,24 +378,40 @@ object AddReminderDialog {
CenteredH6(text = stringResource(id = R.string.repeats_plural, "").trim())
val focusRequester = remember { FocusRequester() }
OutlinedIntInput(
time = interval,
value = intervalAmount,
onValueChange = { newAmount ->
val amt = newAmount ?: 0
updateAlarm(alarm.copy(interval = amt * unitIndexToMillis(selectedUnit)))
},
modifier = Modifier.focusRequester(focusRequester),
)
Spacer(modifier = Modifier.height(16.dp))
options.forEachIndexed { index, option ->
RadioRow(index, option, interval, units)
RadioRow(
index = index,
option = option,
timeAmount = intervalAmount,
unitIndex = selectedUnit,
onUnitSelected = { newUnit ->
selectedUnit = newUnit
updateAlarm(alarm.copy(interval = intervalAmount * unitIndexToMillis(newUnit)))
}
)
}
Divider(modifier = Modifier.padding(vertical = 4.dp), thickness = 1.dp)
Row(modifier = Modifier.fillMaxWidth()) {
OutlinedIntInput(
time = repeat,
value = alarm.repeat,
onValueChange = { newRepeat ->
updateAlarm(alarm.copy(repeat = newRepeat ?: 0))
},
modifier = Modifier.weight(0.5f),
autoSelect = false,
)
BodyText(
text = LocalContext.current.resources.getQuantityString(
R.plurals.repeat_times,
repeat.value
alarm.repeat
),
modifier = Modifier
.weight(0.5f)
@ -357,7 +419,7 @@ object AddReminderDialog {
)
}
ShowKeyboard(openDialog, focusRequester)
ShowKeyboard(true, focusRequester)
}
}
@ -367,14 +429,6 @@ object AddReminderDialog {
R.plurals.reminder_days,
R.plurals.reminder_week,
)
private val MutableState<Int>.millis: Long
get() = when (value) {
1 -> TimeUnit.HOURS.toMillis(1)
2 -> TimeUnit.DAYS.toMillis(1)
3 -> TimeUnit.DAYS.toMillis(7)
else -> TimeUnit.MINUTES.toMillis(1)
}
}
@ExperimentalComposeUiApi
@ -391,25 +445,48 @@ fun ShowKeyboard(visible: Boolean, focusRequester: FocusRequester) {
@Composable
fun OutlinedIntInput(
time: MutableState<Int>,
value: Int?,
onValueChange: (Int?) -> Unit,
modifier: Modifier = Modifier,
minValue: Int = 1,
autoSelect: Boolean = true,
) {
val value = rememberSaveable(stateSaver = TextFieldValue.Saver) {
val text = time.value.toString()
var textFieldValue by remember {
mutableStateOf(
TextFieldValue(
text = text,
selection = TextRange(0, if (autoSelect) text.length else 0)
text = value?.toString() ?: "",
selection = if (autoSelect) {
TextRange(0, value?.toString()?.length ?: 0)
} else {
TextRange.Zero
}
)
)
}
// Sync when external value changes, but don't interfere with user editing
LaunchedEffect(value) {
val currentParsedValue = textFieldValue.text.toIntOrNull()
// Only sync if the new value is different from what we currently parse to,
// and don't sync if the text field is empty (user is actively deleting)
if (currentParsedValue != value && textFieldValue.text.isNotEmpty()) {
val newText = value?.toString() ?: ""
textFieldValue = TextFieldValue(
text = newText,
selection = if (autoSelect) {
TextRange(0, newText.length)
} else {
textFieldValue.selection
}
)
}
}
OutlinedTextField(
value = value.value,
value = textFieldValue,
onValueChange = {
value.value = it.copy(text = it.text.filter { t -> t.isDigit() })
time.value = value.value.text.toIntOrNull() ?: 0
textFieldValue = it.copy(text = it.text.filter { t -> t.isDigit() })
onValueChange(textFieldValue.text.toIntOrNull())
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = modifier.padding(horizontal = 16.dp),
@ -419,7 +496,7 @@ fun OutlinedIntInput(
focusedBorderColor = MaterialTheme.colorScheme.onSurface,
unfocusedBorderColor = MaterialTheme.colorScheme.onSurface,
),
isError = value.value.text.toIntOrNull()?.let { it < minValue } ?: true,
isError = textFieldValue.text.toIntOrNull()?.let { it < minValue } ?: true,
)
}
@ -445,23 +522,24 @@ fun CenteredH6(text: String) {
fun RadioRow(
index: Int,
option: Int,
time: MutableState<Int>,
units: MutableState<Int>,
timeAmount: Int,
unitIndex: Int,
onUnitSelected: (Int) -> Unit,
formatString: Int? = null,
) {
val optionString = LocalContext.current.resources.getQuantityString(option, time.value)
val optionString = LocalContext.current.resources.getQuantityString(option, timeAmount)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { units.value = index }
.clickable { onUnitSelected(index) }
) {
RadioButton(
selected = index == units.value,
onClick = { units.value = index },
selected = index == unitIndex,
onClick = { onUnitSelected(index) },
modifier = Modifier.align(CenterVertically)
)
BodyText(
text = if (index == units.value) {
text = if (index == unitIndex) {
formatString
?.let { stringResource(id = formatString, optionString) }
?: optionString
@ -506,8 +584,14 @@ fun AddAlarmDialog(
dismiss()
return
}
// TODO: if replacing custom alarm show custom picker
// TODO: prepopulate pickers with existing values
TYPE_REL_END -> {
if (viewState.replace.time < 0) {
// Custom reminder (before due)
addCustom()
dismiss()
return
}
}
}
}
CustomDialog(visible = viewState.showAddAlarm, onDismiss = dismiss) {
@ -555,11 +639,11 @@ fun AddAlarmDialog(
fun AddCustomReminderOne() =
TasksTheme {
AddReminderDialog.AddCustomReminder(
time = remember { mutableStateOf(1) },
units = remember { mutableStateOf(0) },
interval = remember { mutableStateOf(0) },
recurringUnits = remember { mutableStateOf(0) },
repeat = remember { mutableStateOf(0) },
alarm = Alarm(
time = -1 * TimeUnit.MINUTES.toMillis(1),
type = TYPE_REL_END
),
updateAlarm = {},
showRecurring = {},
)
}
@ -571,11 +655,11 @@ fun AddCustomReminderOne() =
fun AddCustomReminder() =
TasksTheme {
AddReminderDialog.AddCustomReminder(
time = remember { mutableStateOf(15) },
units = remember { mutableStateOf(1) },
interval = remember { mutableStateOf(0) },
recurringUnits = remember { mutableStateOf(0) },
repeat = remember { mutableStateOf(0) },
alarm = Alarm(
time = -15 * TimeUnit.HOURS.toMillis(1),
type = TYPE_REL_END
),
updateAlarm = {},
showRecurring = {},
)
}
@ -587,10 +671,13 @@ fun AddCustomReminder() =
fun AddRepeatingReminderOne() =
TasksTheme {
AddReminderDialog.AddRecurringReminder(
openDialog = true,
interval = remember { mutableStateOf(1) },
units = remember { mutableStateOf(0) },
repeat = remember { mutableStateOf(1) },
alarm = Alarm(
time = -1 * TimeUnit.MINUTES.toMillis(1),
type = TYPE_REL_END,
interval = TimeUnit.MINUTES.toMillis(1),
repeat = 1
),
updateAlarm = {},
)
}
@ -601,10 +688,13 @@ fun AddRepeatingReminderOne() =
fun AddRepeatingReminder() =
TasksTheme {
AddReminderDialog.AddRecurringReminder(
openDialog = true,
interval = remember { mutableStateOf(15) },
units = remember { mutableStateOf(1) },
repeat = remember { mutableStateOf(4) },
alarm = Alarm(
time = -15 * TimeUnit.HOURS.toMillis(1),
type = TYPE_REL_END,
interval = 15 * TimeUnit.HOURS.toMillis(1),
repeat = 4
),
updateAlarm = {},
)
}
@ -615,8 +705,11 @@ fun AddRepeatingReminder() =
fun AddRandomReminderOne() =
TasksTheme {
AddReminderDialog.AddRandomReminder(
time = remember { mutableStateOf(1) },
units = remember { mutableStateOf(0) }
alarm = Alarm(
time = TimeUnit.MINUTES.toMillis(1),
type = TYPE_RANDOM
),
updateAlarm = {}
)
}
@ -627,8 +720,11 @@ fun AddRandomReminderOne() =
fun AddRandomReminder() =
TasksTheme {
AddReminderDialog.AddRandomReminder(
time = remember { mutableStateOf(15) },
units = remember { mutableStateOf(1) }
alarm = Alarm(
time = 15 * TimeUnit.HOURS.toMillis(1),
type = TYPE_RANDOM
),
updateAlarm = {}
)
}

@ -31,7 +31,10 @@ fun DeleteButton(
PromptAction(
showDialog = promptDelete,
title = stringResource(id = R.string.delete_tag_confirmation, title),
onAction = { scope.launch { onDelete() } },
onAction = {
scope.launch { onDelete() }
promptDelete = false
},
onCancel = { promptDelete = false },
)
}

@ -0,0 +1,9 @@
package org.tasks.compose
import kotlinx.serialization.Serializable
@Serializable
object HomeDestination
@Serializable
data class AddAccountDestination(val showImport: Boolean)

@ -189,8 +189,10 @@ fun LazyItemScope.DraggableItem(
.zIndex(1f)
.graphicsLayer { translationY = current }
} else {
Modifier.animateItemPlacement(
tween(easing = FastOutLinearInEasing)
Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null,
placementSpec = tween(easing = FastOutLinearInEasing),
)
}
Box(modifier = modifier.then(draggingModifier)) {

@ -23,6 +23,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Abc
@ -48,12 +49,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -229,10 +233,10 @@ object FilterCondition {
)
},
right = {
val context = LocalContext.current
val locale = remember {
val configuration = LocalConfiguration.current
val locale = remember(configuration) {
ConfigurationCompat
.getLocales(context.resources.configuration)
.getLocales(configuration)
.get(0)
?: Locale.getDefault()
}
@ -382,16 +386,31 @@ object FilterCondition {
Row {
for (index in items.indices) {
val highlight = (index == selected.intValue)
val color =
if (highlight) MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f)
else MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f)
OutlinedButton(
onClick = { selected.intValue = index },
border = BorderStroke(1.dp, SolidColor(color.copy(alpha = 0.5f))),
border = BorderStroke(
width = 1.dp,
brush = SolidColor(
if (highlight) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f)
}
)
),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = color.copy(alpha = 0.2f),
contentColor = MaterialTheme.colorScheme.onBackground),
shape = RoundedCornerShape(Constants.HALF_KEYLINE)
containerColor = if (highlight) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f)
},
contentColor = if (highlight) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onBackground
},
),
shape = RoundedCornerShape(Constants.HALF_KEYLINE),
) {
Text(items[index])
}
@ -482,7 +501,12 @@ object FilterCondition {
contentDescription = null
)
},
textStyle = MaterialTheme.typography.bodyMedium,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences
),
textStyle = MaterialTheme.typography.bodyMedium.copy(
textDirection = TextDirection.Content
),
colors = Constants.textFieldColors(),
)
}

@ -16,7 +16,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
@ -34,10 +34,10 @@ fun OutlinedNumberInput(
onFocus: () -> Unit = {},
) {
val interactionSource = remember { MutableInteractionSource() }
val context = LocalContext.current
val locale = remember {
val configuration = LocalConfiguration.current
val locale = remember(configuration) {
ConfigurationCompat
.getLocales(context.resources.configuration)
.getLocales(configuration)
.get(0)
?: Locale.getDefault()
}

@ -21,6 +21,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import org.tasks.R
import org.tasks.compose.Constants.TextButton
@ -108,7 +109,9 @@ object ShareInvite {
contentDescription = label
)
},
textStyle = MaterialTheme.typography.bodyLarge,
textStyle = MaterialTheme.typography.bodyLarge.copy(
textDirection = TextDirection.Content
),
colors = textFieldColors(),
)
}

@ -12,7 +12,6 @@ import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.themes.TasksIcons
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.startOfDay
@Composable
@ -32,7 +31,7 @@ fun StartDateChip(
) {
startDate
.takeIf { Task.hasDueTime(it) }
?.let { getTimeString(currentTimeMillis(), context.is24HourFormat) }
?.let { getTimeString(it, context.is24HourFormat) }
} else {
runBlocking {
getRelativeDateTime(

@ -0,0 +1,348 @@
package org.tasks.compose.accounts
import androidx.activity.compose.BackHandler
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.outlined.Backup
import androidx.compose.material.icons.outlined.CloudOff
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.PreviewFontScale
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp
import org.tasks.R
import org.tasks.sync.AddAccountDialog.Platform
import org.tasks.themes.TasksTheme
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun AddAccountScreen(
gettingStarted: Boolean,
hasTasksAccount: Boolean,
hasPro: Boolean,
onBack: () -> Unit,
signIn: (Platform) -> Unit,
openUrl: (Platform) -> Unit,
onImportBackup: () -> Unit,
) {
BackHandler {
if (!gettingStarted) {
onBack()
}
}
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(),
navigationIcon = {
if (!gettingStarted) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = stringResource(R.string.back),
)
}
}
},
title = {
Text(
text = if (gettingStarted) {
stringResource(R.string.sign_in)
} else {
stringResource(R.string.add_account)
}
)
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.padding(vertical = 16.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalArrangement = Arrangement.spacedBy(16.dp),
maxItemsInEachRow = 5
) {
if (gettingStarted) {
ActionCard(
title = R.string.backup_BAc_import,
icon = Icons.Outlined.Backup,
onClick = onImportBackup,
isOutlined = true
)
ActionCard(
title = R.string.continue_without_sync,
icon = Icons.Outlined.CloudOff,
onClick = { signIn(Platform.LOCAL) },
isOutlined = true
)
}
if (!hasTasksAccount) {
AccountTypeCard(
title = R.string.tasks_org,
cost = R.string.cost_more_money,
icon = R.drawable.ic_round_icon,
onClick = { signIn(Platform.TASKS_ORG) }
)
}
AccountTypeCard(
title = R.string.microsoft,
cost = if (hasPro) null else R.string.cost_free,
icon = R.drawable.ic_microsoft_tasks,
onClick = { signIn(Platform.MICROSOFT) }
)
AccountTypeCard(
title = R.string.gtasks_GPr_header,
cost = if (hasPro) null else R.string.cost_free,
icon = R.drawable.ic_google,
onClick = { signIn(Platform.GOOGLE_TASKS) }
)
AccountTypeCard(
title = R.string.davx5,
cost = if (hasPro) null else R.string.cost_money,
icon = R.drawable.ic_davx5_icon_green_bg,
onClick = { openUrl(Platform.DAVX5) }
)
AccountTypeCard(
title = R.string.caldav,
cost = if (hasPro) null else R.string.cost_money,
icon = R.drawable.ic_webdav_logo,
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = .8f),
onClick = { signIn(Platform.CALDAV) }
)
AccountTypeCard(
title = R.string.etesync,
cost = if (hasPro) null else R.string.cost_money,
icon = R.drawable.ic_etesync,
onClick = { signIn(Platform.ETESYNC) }
)
AccountTypeCard(
title = R.string.decsync,
cost = if (hasPro) null else R.string.cost_money,
icon = R.drawable.ic_decsync,
onClick = { openUrl(Platform.DECSYNC_CC) }
)
if (gettingStarted) {
ActionCard(
title = R.string.help_me_choose,
icon = Icons.AutoMirrored.Outlined.HelpOutline,
onClick = { openUrl(Platform.LOCAL) },
isOutlined = true
)
}
}
}
}
}
@Composable
fun AccountTypeCard(
@StringRes title: Int,
@StringRes cost: Int? = null,
@DrawableRes icon: Int,
tint: Color? = null,
onClick: () -> Unit,
) {
Card(
modifier = Modifier
.width(108.dp),
shape = MaterialTheme.shapes.medium,
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
onClick = onClick
) {
Column(
modifier = Modifier.padding(12.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = painterResource(id = icon),
contentDescription = stringResource(id = title),
tint = tint ?: Color.Unspecified,
modifier = Modifier.size(48.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = buildAnnotatedString {
append(stringResource(id = title))
cost?.let {
append("\n")
withStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontSize = MaterialTheme.typography.labelSmall.fontSize
)
) {
append(stringResource(id = it))
}
}
},
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
minLines = 3,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
fun ActionCard(
@StringRes title: Int,
icon: ImageVector,
onClick: () -> Unit,
isOutlined: Boolean = false
) {
if (isOutlined) {
OutlinedCard(
modifier = Modifier
.width(108.dp),
shape = MaterialTheme.shapes.medium,
onClick = onClick
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = icon,
contentDescription = stringResource(id = title),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = title),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
minLines = 3,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
} else {
Card(
modifier = Modifier
.width(150.dp),
shape = MaterialTheme.shapes.medium,
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
onClick = onClick
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = icon,
contentDescription = stringResource(id = title),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = title),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
@PreviewLightDark
@PreviewScreenSizes
@PreviewFontScale
@Composable
fun GettingStartedPreview() {
TasksTheme {
AddAccountScreen(
gettingStarted = true,
hasTasksAccount = false,
hasPro = false,
onBack = {},
signIn = {},
openUrl = {},
onImportBackup = {},
)
}
}
@PreviewLightDark
@Composable
fun AddAccountPreview() {
TasksTheme {
AddAccountScreen(
gettingStarted = false,
hasTasksAccount = false,
hasPro = false,
onBack = {},
signIn = {},
openUrl = {},
onImportBackup = {},
)
}
}

@ -0,0 +1,32 @@
package org.tasks.compose.accounts
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.data.dao.CaldavDao
import org.tasks.data.newLocalAccount
import org.tasks.extensions.Context.openUri
import org.tasks.sync.AddAccountDialog
import javax.inject.Inject
@HiltViewModel
class AddAccountViewModel @Inject constructor(
private val caldavDao: CaldavDao,
) : ViewModel() {
fun createLocalAccount() = viewModelScope.launch {
caldavDao.newLocalAccount()
}
fun openUrl(context: Context, platform: AddAccountDialog.Platform) {
val url = when (platform) {
AddAccountDialog.Platform.DAVX5 -> R.string.url_davx5
AddAccountDialog.Platform.DECSYNC_CC -> R.string.url_decsync
AddAccountDialog.Platform.LOCAL -> R.string.help_url_sync
else -> return
}
context.openUri(context.getString(url))
}
}

@ -106,23 +106,27 @@ fun AlarmRow(
dismiss = { vm.showAddAlarm(visible = false) },
)
AddReminderDialog.AddCustomReminderDialog(
viewState = viewState,
addAlarm = {
viewState.replace?.let(deleteAlarm)
addAlarm(it)
},
closeDialog = { vm.showCustomDialog(visible = false) }
)
if (viewState.showCustomDialog) {
AddReminderDialog.AddCustomReminderDialog(
alarm = viewState.replace,
updateAlarm = {
viewState.replace?.let(deleteAlarm)
addAlarm(it)
},
closeDialog = { vm.showCustomDialog(visible = false) }
)
}
AddReminderDialog.AddRandomReminderDialog(
viewState = viewState,
addAlarm = {
viewState.replace?.let(deleteAlarm)
addAlarm(it)
},
closeDialog = { vm.showRandomDialog(visible = false) }
)
if (viewState.showRandomDialog) {
AddReminderDialog.AddRandomReminderDialog(
alarm = viewState.replace,
updateAlarm = {
viewState.replace?.let(deleteAlarm)
addAlarm(it)
},
closeDialog = { vm.showRandomDialog(visible = false) }
)
}
},
)
}

@ -24,7 +24,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.widget.addTextChangedListener
import com.todoroo.andlib.utility.AndroidUtilities
import org.tasks.R
import org.tasks.dialogs.Linkify
import org.tasks.markdown.MarkdownProvider
@ -93,10 +92,7 @@ fun EditTextView(
}
setBackgroundColor(context.getColor(android.R.color.transparent))
textAlignment = View.TEXT_ALIGNMENT_VIEW_START
if (AndroidUtilities.atLeastOreo()) {
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
}
importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
freezesText = true
setHorizontallyScrolling(false)
setHint(hint)

@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.tasks.compose.CheckBox
@ -170,6 +171,7 @@ fun NewSubtaskRow(
.padding(top = 12.dp),
textStyle = MaterialTheme.typography.bodyLarge.copy(
textDecoration = if (subtask.isCompleted) TextDecoration.LineThrough else TextDecoration.None,
textDirection = TextDirection.Content,
color = MaterialTheme.colorScheme.onSurface,
),
keyboardOptions = KeyboardOptions.Default.copy(

@ -0,0 +1,316 @@
package org.tasks.compose.home
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsTopHeight
import androidx.compose.material3.DrawerState
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.content.IntentCompat.getParcelableExtra
import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.rememberFragmentState
import androidx.hilt.navigation.compose.hiltViewModel
import com.todoroo.astrid.activity.MainActivity.Companion.OPEN_FILTER
import com.todoroo.astrid.activity.MainActivityViewModel
import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.activity.TaskEditFragment.Companion.EXTRA_TASK
import com.todoroo.astrid.activity.TaskListFragment
import com.todoroo.astrid.activity.TaskListFragment.Companion.EXTRA_FILTER
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.TasksApplication
import org.tasks.activities.TagSettingsActivity
import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity.Companion.EXTRA_CALDAV_ACCOUNT
import org.tasks.compose.drawer.DrawerAction
import org.tasks.compose.drawer.DrawerItem
import org.tasks.compose.drawer.MenuSearchBar
import org.tasks.compose.drawer.TaskListDrawer
import org.tasks.data.listSettingsClass
import org.tasks.extensions.Context.openUri
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_LIST
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_PLACE
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_TAGS
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.kmp.org.tasks.compose.TouchSlopMultiplier
import org.tasks.kmp.org.tasks.compose.rememberImeState
import org.tasks.location.LocationPickerActivity
import org.tasks.preferences.HelpAndFeedback
import org.tasks.preferences.MainPreferences
import timber.log.Timber
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun HomeScreen(
viewModel: MainActivityViewModel = hiltViewModel(LocalActivity.current as ComponentActivity),
state: MainActivityViewModel.State,
drawerState: DrawerState,
showNewFilterDialog: () -> Unit,
navigator: ThreePaneScaffoldNavigator<Any>,
) {
val currentWindowInsets = WindowInsets.systemBars.asPaddingValues()
val windowInsets = remember { mutableStateOf(currentWindowInsets) }
val keyboard = LocalSoftwareKeyboardController.current
val newList =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
result.data
?.let { getParcelableExtra(it, OPEN_FILTER, Filter::class.java) }
?.let { viewModel.setFilter(it) }
}
}
LaunchedEffect(currentWindowInsets) {
Timber.d("insets: $currentWindowInsets")
if (currentWindowInsets.calculateTopPadding() != 0.dp || currentWindowInsets.calculateBottomPadding() != 0.dp) {
windowInsets.value = currentWindowInsets
}
}
val isListVisible =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
val isDetailVisible =
navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
TouchSlopMultiplier {
ModalNavigationDrawer(
drawerState = drawerState,
gesturesEnabled = isListVisible,
drawerContent = {
ModalDrawerSheet(
drawerState = drawerState,
windowInsets = WindowInsets(0, 0, 0, 0),
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
Box(modifier = Modifier.fillMaxSize()) {
TaskListDrawer(
arrangement = if (state.menuQuery.isBlank()) Arrangement.Top else Arrangement.Bottom,
filters = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems,
onClick = {
when (it) {
is DrawerItem.Filter -> {
viewModel.setFilter(it.filter)
scope.launch {
drawerState.close()
keyboard?.hide()
}
}
is DrawerItem.Header -> {
viewModel.toggleCollapsed(it.header)
}
}
},
onAddClick = {
scope.launch {
drawerState.close()
when (it.header.addIntentRc) {
FilterProvider.REQUEST_NEW_FILTER ->
showNewFilterDialog()
REQUEST_NEW_PLACE ->
newList.launch(Intent(context, LocationPickerActivity::class.java))
REQUEST_NEW_TAGS ->
newList.launch(Intent(context, TagSettingsActivity::class.java))
REQUEST_NEW_LIST ->
when (it.header.subheaderType) {
NavigationDrawerSubheader.SubheaderType.CALDAV,
NavigationDrawerSubheader.SubheaderType.TASKS ->
viewModel
.getAccount(it.header.id.toLong())
?.let {
newList.launch(
Intent(context, it.listSettingsClass())
.putExtra(EXTRA_CALDAV_ACCOUNT, it)
)
}
else -> {}
}
else -> Timber.e("Unhandled request code: $it")
}
}
},
onErrorClick = {
context.startActivity(Intent(context, MainPreferences::class.java))
},
searchBar = {
MenuSearchBar(
begForMoney = state.begForMoney,
onDrawerAction = {
scope.launch {
drawerState.close()
when (it) {
DrawerAction.PURCHASE ->
if (TasksApplication.IS_GENERIC)
context.openUri(R.string.url_donate)
else
context.startActivity(
Intent(
context,
PurchaseActivity::class.java
)
)
DrawerAction.HELP_AND_FEEDBACK ->
context.startActivity(
Intent(
context,
HelpAndFeedback::class.java
)
)
}
}
},
query = state.menuQuery,
onQueryChange = { viewModel.queryMenu(it) },
)
},
)
SystemBarScrim(
modifier = Modifier
.windowInsetsTopHeight(WindowInsets.systemBars)
.align(Alignment.TopCenter)
)
SystemBarScrim(
modifier = Modifier
.windowInsetsBottomHeight(WindowInsets.systemBars)
.align(Alignment.BottomCenter),
)
}
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
val scope = rememberCoroutineScope()
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
key (state.filter) {
val fragment = remember { mutableStateOf<TaskListFragment?>(null) }
val keyboardOpen = rememberImeState()
AndroidFragment<TaskListFragment>(
fragmentState = rememberFragmentState(),
arguments = remember(state.filter) {
Bundle()
.apply { putParcelable(EXTRA_FILTER, state.filter) }
},
modifier = Modifier
.fillMaxSize()
.imePadding(),
) { tlf ->
fragment.value = tlf
tlf.applyInsets(windowInsets.value)
tlf.setNavigationClickListener {
scope.launch { drawerState.open() }
}
}
LaunchedEffect(fragment, windowInsets, keyboardOpen.value) {
fragment.value?.applyInsets(
if (keyboardOpen.value) {
PaddingValues(
top = windowInsets.value.calculateTopPadding(),
)
} else {
windowInsets.value
}
)
}
}
},
detailPane = {
val direction = LocalLayoutDirection.current
Box(
modifier = Modifier
.fillMaxSize()
.padding(
top = windowInsets.value.calculateTopPadding(),
start = windowInsets.value.calculateStartPadding(direction),
end = windowInsets.value.calculateEndPadding(direction),
bottom = if (rememberImeState().value)
WindowInsets.ime.asPaddingValues().calculateBottomPadding()
else
windowInsets.value.calculateBottomPadding()
),
contentAlignment = Alignment.Center,
) {
if (state.task == null) {
if (isListVisible && isDetailVisible) {
Icon(
painter = painterResource(org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground),
contentDescription = null,
modifier = Modifier.size(192.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} else {
key(state.task) {
AndroidFragment<TaskEditFragment>(
fragmentState = rememberFragmentState(),
arguments = remember(state.task) {
Bundle()
.apply { putParcelable(EXTRA_TASK, state.task) }
},
modifier = Modifier.fillMaxSize(),
)
}
}
}
},
)
SystemBarScrim(
modifier = Modifier
.windowInsetsTopHeight(WindowInsets.systemBars)
.align(Alignment.TopCenter),
)
}
}
}
}

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

Loading…
Cancel
Save