Compare commits

..

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

@ -1,4 +1,5 @@
github: abaker
liberapay: tasks
open_collective: tasks
patreon: tasks
custom: tasks.org/donate

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

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

@ -1,47 +0,0 @@
name: Update Dependency Diff
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches:
- main
paths:
- 'gradle/libs.versions.toml'
pull_request:
paths:
- 'gradle/libs.versions.toml'
workflow_dispatch:
jobs:
update-deps:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Update dependency diffs
run: ./update_dependency_diff
- name: Commit changes
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add deps_*.txt
git diff --staged --quiet || git commit -m "Update dependency diffs"
git push

@ -1,32 +0,0 @@
name: Deploy
on:
workflow_dispatch:
permissions:
contents: read
env:
FASTLANE: ${{ secrets.FASTLANE }}
jobs:
bundle:
uses: ./.github/workflows/bundle.yml
secrets: inherit
deploy:
runs-on: ubuntu-latest
needs: [ bundle ]
steps:
- uses: actions/checkout@v6
- name: Fastlane key
run: |
echo "$FASTLANE" > ./fastlane.json
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- uses: actions/download-artifact@v7
with:
name: release
path: .
- name: Deploy
run: bundle exec fastlane deploy

1
.gitignore vendored

@ -11,4 +11,3 @@ Thumbs.db
/captures/
/fastlane/report.xml
/compose-metrics/
.DS_Store

@ -1 +1 @@
3.4.8
3.3.4

@ -1,70 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="wear" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false" singleton="true">
<module name="tasks.Tasks.wear.main" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="ALL_USERS" value="false" />
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="RESTORE_ENABLED" value="false" />
<option name="RESTORE_FILE" value="" />
<option name="CLEAR_LOGCAT" value="true" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Hybrid>
<Java>
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Java>
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
<option name="ATTACH_ON_WAIT_FOR_DEBUGGER" value="false" />
<option name="DEBUG_SANDBOX_SDK" value="false" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Java/Kotlin Method Sample (legacy)" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY_CLASS" value="" />
<option name="SEARCH_ACTIVITY_IN_GLOBAL_SCOPE" value="false" />
<option name="SKIP_ACTIVITY_VALIDATION" value="false" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

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

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

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

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

@ -51,8 +51,8 @@ android {
defaultConfig {
testApplicationId = "org.tasks.test"
applicationId = "org.tasks"
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()
versionCode = 131107
versionName = "13.11.2"
targetSdk = libs.versions.android.targetSdk.get().toInt()
minSdk = libs.versions.android.minSdk.get().toInt()
testInstrumentationRunner = "org.tasks.TestRunner"
@ -154,8 +154,6 @@ dependencies {
implementation(projects.data)
implementation(projects.kmp)
implementation(projects.icons)
implementation(libs.androidx.navigation)
implementation(libs.androidx.adaptive.navigation.android)
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.bitfire.dav4jvm) {
exclude(group = "junit")
@ -178,17 +176,14 @@ dependencies {
implementation(libs.dagger.hilt)
ksp(libs.dagger.hilt.compiler)
ksp(libs.androidx.hilt.compiler)
implementation(libs.androidx.hilt.navigation)
implementation(libs.androidx.hilt.work)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.datastore)
implementation(libs.androidx.fragment.compose)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.runtime)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.room)
implementation(libs.androidx.sqlite)
implementation(libs.androidx.appcompat)
implementation(libs.iconics)
implementation(libs.markwon)
@ -198,6 +193,9 @@ dependencies {
implementation(libs.markwon.tables)
implementation(libs.markwon.tasklist)
debugImplementation(libs.facebook.flipper)
debugImplementation(libs.facebook.flipper.network)
debugImplementation(libs.facebook.soloader)
debugImplementation(libs.leakcanary)
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation(libs.kotlin.reflect)
@ -209,7 +207,6 @@ dependencies {
implementation(libs.persistent.cookiejar)
implementation(libs.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.androidx.preference)
@ -230,6 +227,8 @@ dependencies {
implementation(libs.colorpicker)
implementation(libs.appauth)
implementation(libs.osmdroid)
implementation(libs.retrofit)
implementation(libs.retrofit.moshi)
implementation(libs.androidx.recyclerview)
implementation(platform(libs.androidx.compose))
@ -238,40 +237,30 @@ dependencies {
implementation("androidx.compose.material:material")
implementation("androidx.compose.runtime:runtime-livedata")
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.material.icons.extended)
implementation("androidx.compose.material:material-icons-extended")
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation("androidx.compose.ui:ui-viewbinding")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation(libs.coil.compose)
implementation(libs.coil.video)
implementation(libs.coil.svg)
implementation(libs.coil.gif)
implementation(libs.ktor)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.content.negotiation)
implementation(libs.ktor.serialization)
implementation(libs.accompanist.flowlayout)
implementation(libs.accompanist.permissions)
implementation(libs.accompanist.systemuicontroller)
googleplayImplementation(platform(libs.firebase))
googleplayImplementation(libs.firebase.crashlytics)
googleplayImplementation(libs.firebase.analytics) {
googleplayImplementation("com.google.firebase:firebase-crashlytics")
googleplayImplementation("com.google.firebase:firebase-analytics") {
exclude("com.google.android.gms", "play-services-ads-identifier")
}
googleplayImplementation(libs.firebase.config.ktx)
googleplayImplementation("com.google.firebase:firebase-config-ktx")
googleplayImplementation(libs.play.services.location)
googleplayImplementation(libs.play.services.maps)
googleplayImplementation(libs.play.billing.ktx)
googleplayImplementation(libs.play.review)
googleplayImplementation(libs.play.services.oss.licenses)
googleplayImplementation(libs.horologist.datalayer.phone)
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)
kspAndroidTest(libs.dagger.hilt.compiler)

@ -1,20 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "org.tasks.ak",
"variantName": "genericRelease",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 130804,
"versionName": "14.0.6",
"outputFile": "app-generic-release.apk"
}
],
"elementType": "File"
}

19
app/proguard.pro vendored

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

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

@ -4,11 +4,11 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.todoroo.astrid.activity.MainActivity.Companion.isFromHistory
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.tasks.extensions.isFromHistory
@RunWith(AndroidJUnit4::class)
class MainActivityTest {

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

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

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

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

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

@ -1,6 +1,7 @@
package com.todoroo.astrid.alarms
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
@ -11,11 +12,13 @@ import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Notification
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class AlarmJobServiceTest : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao

@ -8,13 +8,16 @@ package com.todoroo.astrid.dao
import org.tasks.data.entity.Task
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TaskDaoTests : InjectingTestCase() {

@ -1,142 +0,0 @@
package com.todoroo.astrid.gcal
import android.Manifest
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.provider.CalendarContract
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import androidx.core.net.toUri
import androidx.test.core.app.ApplicationProvider
import androidx.test.rule.GrantPermissionRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.tasks.TestUtilities.withTZ
import org.tasks.data.entity.Task
import org.tasks.injection.InjectingTestCase
import org.tasks.time.DateTime
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidTest
class GCalHelperTest : InjectingTestCase() {
@get:Rule
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.READ_CALENDAR,
Manifest.permission.WRITE_CALENDAR
)
@Inject lateinit var gcalHelper: GCalHelper
private var testCalendarId: Long = -1
@Before
override fun setUp() {
super.setUp()
testCalendarId = createTestCalendar()
}
@After
fun tearDown() {
if (testCalendarId > 0) {
try {
val context = ApplicationProvider.getApplicationContext<Context>()
context.contentResolver.delete(
ContentUris.withAppendedId(Calendars.CONTENT_URI, testCalendarId),
null,
null
)
} catch (e: Exception) {
Timber.e(e)
}
}
}
@Test fun allDayEventInNewYork() = assertAllDayEvent("America/New_York") // UTC-5
@Test fun allDayEventInBerlin() = assertAllDayEvent("Europe/Berlin") // UTC+1
@Test fun allDayEventInAuckland() = assertAllDayEvent("Pacific/Auckland") // UTC+13
@Test fun allDayEventInTokyo() = assertAllDayEvent("Asia/Tokyo") // UTC+9
@Test fun allDayEventInHonolulu() = assertAllDayEvent("Pacific/Honolulu") // UTC-10
@Test fun allDayEventInChatham() = assertAllDayEvent("Pacific/Chatham") // UTC+13:45
private fun assertAllDayEvent(timezone: String) = withTZ(timezone) {
val task = Task(dueDate = DateTime(2024, 12, 20).millis)
val eventUri = gcalHelper.createTaskEvent(task, testCalendarId.toString())
?: throw RuntimeException("Event not created")
val event = queryEvent(eventUri.toString()) ?: throw RuntimeException("Event not found")
assertEquals(
"DTSTART should be Dec 20 00:00 UTC",
DateTime(2024, 12, 20, timeZone = DateTime.UTC).millis,
event.dtStart
)
assertEquals(
"DTEND should be Dec 21 00:00 UTC",
DateTime(2024, 12, 21, timeZone = DateTime.UTC).millis,
event.dtEnd
)
}
private fun createTestCalendar(): Long {
val context = ApplicationProvider.getApplicationContext<Context>()
val values = ContentValues().apply {
put(Calendars.ACCOUNT_NAME, "test@test.com")
put(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
put(Calendars.NAME, "Test Calendar")
put(Calendars.CALENDAR_DISPLAY_NAME, "Test Calendar")
put(Calendars.CALENDAR_COLOR, 0xFF0000)
put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
put(Calendars.OWNER_ACCOUNT, "test@test.com")
put(Calendars.VISIBLE, 1)
put(Calendars.SYNC_EVENTS, 1)
}
val uri = Calendars.CONTENT_URI.buildUpon()
.appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(Calendars.ACCOUNT_NAME, "test@test.com")
.appendQueryParameter(Calendars.ACCOUNT_TYPE, CalendarContract.ACCOUNT_TYPE_LOCAL)
.build()
val calendarUri = context.contentResolver.insert(uri, values)
return ContentUris.parseId(calendarUri!!)
}
private fun queryEvent(eventUri: String): CalendarEvent? {
val context = ApplicationProvider.getApplicationContext<Context>()
val cursor = context.contentResolver.query(
eventUri.toUri(),
arrayOf(
Events.DTSTART,
Events.DTEND,
Events.ALL_DAY,
Events.EVENT_TIMEZONE
),
null,
null,
null
)
return cursor?.use {
if (it.moveToFirst()) {
CalendarEvent(
dtStart = it.getLong(0),
dtEnd = it.getLong(1),
allDay = it.getInt(2) == 1,
timezone = it.getString(3)
)
} else null
}
}
private data class CalendarEvent(
val dtStart: Long,
val dtEnd: Long,
val allDay: Boolean,
val timezone: String?
)
}

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

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

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

@ -8,14 +8,17 @@ package com.todoroo.astrid.service
import org.tasks.data.entity.Task
import com.todoroo.astrid.utility.TitleParser
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.tasks.data.dao.TagDataDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import java.util.*
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class QuickAddMarkupTest : InjectingTestCase() {
private val tags = ArrayList<String>()

@ -8,6 +8,7 @@ import org.tasks.data.entity.Task.Companion.DUE_DATE
import org.tasks.data.entity.Task.Companion.HIDE_UNTIL
import org.tasks.data.entity.Task.Companion.URGENCY_SPECIFIC_DAY
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
@ -15,10 +16,12 @@ import org.tasks.R
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.data.createDueDate
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TaskCreatorTest : InjectingTestCase() {
@Inject lateinit var preferences: Preferences

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

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

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

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

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

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

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

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

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

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

@ -2,6 +2,7 @@ package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@ -10,8 +11,10 @@ import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class SharingMailboxDotOrgTest : CaldavTest() {

@ -2,6 +2,7 @@ package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Test
@ -10,8 +11,10 @@ import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class SharingOwncloudTest : CaldavTest() {

@ -2,6 +2,7 @@ package org.tasks.caldav
import org.tasks.data.UUIDHelper
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@ -12,8 +13,10 @@ import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_WRITE
import org.tasks.data.entity.CaldavCalendar.Companion.INVITE_ACCEPTED
import org.tasks.data.dao.PrincipalDao
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class SharingSabredavTest : CaldavTest() {

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

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.natpryce.makeiteasy.PropertyValue
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@ -12,12 +13,14 @@ import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskContainerMaker
import org.tasks.makers.TaskContainerMaker.CREATED
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class CaldavDaoShiftTests : InjectingTestCase() {
@Inject lateinit var taskDao: TaskDao

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

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

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@ -10,11 +11,13 @@ import org.junit.Before
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavTask
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
@ -23,8 +26,10 @@ import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.newTask
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class GoogleTaskDaoTests : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var caldavDao: CaldavDao
@ -180,21 +185,6 @@ class GoogleTaskDaoTests : InjectingTestCase() {
assertEquals("abcd", googleTaskDao.getByTaskId(1)!!.remoteParent)
}
@Test
fun ignoreSelfParent() = runBlocking {
insert(
newCaldavTask(
with(TASK, 1),
with(REMOTE_ID, "123"),
with(REMOTE_PARENT, "123")
)
)
caldavDao.updateParents()
assertEquals(0, taskDao.fetch(1)!!.parent)
}
@Test
fun updateParents() = runBlocking {
insert(newCaldavTask(with(TASK, 1), with(REMOTE_ID, "123")))

@ -1,16 +1,21 @@
package org.tasks.data
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class GoogleTaskListDaoTest : InjectingTestCase() {
@Inject lateinit var googleTaskListDao: GoogleTaskListDao
@Inject lateinit var caldavDao: CaldavDao
@Test
@ -21,6 +26,6 @@ class GoogleTaskListDaoTest : InjectingTestCase() {
)
caldavDao.insert(account)
assertTrue(caldavDao.getCaldavFilters(account.username!!).isEmpty())
assertTrue(googleTaskListDao.getGoogleTaskFilters(account.username!!).isEmpty())
}
}

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@ -20,6 +21,7 @@ import org.tasks.data.entity.Place
import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.DELETION_TIME
import org.tasks.makers.TaskMaker.DUE_TIME
@ -29,6 +31,7 @@ import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class LocationDaoTest : InjectingTestCase() {
@Inject lateinit var locationDao: LocationDao

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

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

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

@ -3,6 +3,7 @@ package org.tasks.data
import com.natpryce.makeiteasy.MakeItEasy
import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@ -15,9 +16,11 @@ import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Tag
import org.tasks.data.entity.TagData
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class UpgraderDaoTests : InjectingTestCase() {

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

@ -10,6 +10,7 @@ import androidx.test.InstrumentationRegistry
import com.todoroo.astrid.dao.TaskDao
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertEquals
@ -21,11 +22,13 @@ import org.tasks.backup.BackupConstants.BACKUP_CLEANUP_MATCHER
import org.tasks.backup.TasksJsonExporter
import org.tasks.backup.TasksJsonExporter.ExportType
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences
import java.io.File
import java.io.IOException
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class BackupServiceTests : InjectingTestCase() {
@Inject lateinit var jsonExporter: TasksJsonExporter

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

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

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

@ -2,6 +2,7 @@ package org.tasks.opentasks
import com.natpryce.makeiteasy.MakeItEasy.with
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
@ -10,6 +11,7 @@ import org.junit.Test
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.entity.CaldavCalendar
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.TASK
@ -17,6 +19,7 @@ import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.RECUR
import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class OpenTasksSynchronizerTest : OpenTasksTest() {

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

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

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

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

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

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

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

@ -1,22 +1,25 @@
package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.entity.Task.Priority.Companion.HIGH
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.entity.Task.Priority.Companion.HIGH
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class TitleTests : BaseTaskEditViewModelTest() {
@Test
fun changeTitleCausesChange() {
setup(newTask())
viewModel.setTitle("Test")
viewModel.title = "Test"
assertTrue(viewModel.hasChanges())
}
@ -26,7 +29,7 @@ class TitleTests : BaseTaskEditViewModelTest() {
val task = newTask()
setup(task)
viewModel.setPriority(HIGH)
viewModel.priority.value = HIGH
save()

@ -1,18 +1,19 @@
package org.tasks.billing
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.runBlocking
import dagger.hilt.android.testing.UninstallModules
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.LocalBroadcastManager
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.preferences.Preferences
import javax.inject.Inject
@UninstallModules(ProductionModule::class)
@HiltAndroidTest
class InventoryTest : InjectingTestCase() {
@ -23,24 +24,6 @@ class InventoryTest : InjectingTestCase() {
lateinit var inventory: Inventory
@Test
fun hasTasksAccount() = runBlocking {
caldavDao.insert(CaldavAccount(accountType = CaldavAccount.TYPE_TASKS, url = "https://caldav.tasks.org/calendars/"))
initInventory()
inventory.updateTasksAccount()
assertTrue(inventory.hasTasksAccount)
}
@Test
fun hasTasksAccountWithCaldav() = runBlocking {
caldavDao.insert(CaldavAccount(accountType = CaldavAccount.TYPE_CALDAV, url = "https://caldav.tasks.org/calendars/"))
initInventory()
inventory.updateTasksAccount()
assertTrue(inventory.hasTasksAccount)
}
@Test
fun monthlyIsPro() {
withPurchases(monthly01)
@ -89,17 +72,13 @@ class InventoryTest : InjectingTestCase() {
}
.map(::Purchase)
preferences.setPurchases(asPurchases)
initInventory()
}
private fun initInventory() {
runOnMainSync {
inventory = Inventory(
context,
preferences,
signatureVerifier,
localBroadcastManager,
caldavDao
context,
preferences,
signatureVerifier,
localBroadcastManager,
caldavDao
)
}
}

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

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

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

@ -3,24 +3,40 @@ package org.tasks
import android.app.Application
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.android.utils.FlipperUtils
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
import com.facebook.soloader.SoLoader
import com.todoroo.andlib.utility.AndroidUtilities.atLeastOreo
import com.todoroo.andlib.utility.AndroidUtilities.atLeastQ
import leakcanary.AppWatcher
import org.tasks.logging.FileLogger
import org.tasks.preferences.Preferences
import timber.log.Timber
import timber.log.Timber.DebugTree
import javax.inject.Inject
class BuildSetup @Inject constructor(
private val context: Application,
private val preferences: Preferences,
private val fileLogger: FileLogger,
) {
private val context: Application,
private val preferences: Preferences) {
fun setup() {
Timber.plant(Timber.DebugTree())
Timber.plant(fileLogger)
Timber.plant(DebugTree())
SoLoader.init(context, false)
if (preferences.getBoolean(R.string.p_leakcanary, false)) {
AppWatcher.manualInstall(context)
}
if (preferences.getBoolean(R.string.p_flipper, false) && FlipperUtils.shouldEnableFlipper(context)) {
val client = AndroidFlipperClient.getInstance(context)
client.addPlugin(InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()))
client.addPlugin(DatabasesFlipperPlugin(context))
client.addPlugin(NetworkFlipperPlugin())
client.addPlugin(SharedPreferencesFlipperPlugin(context))
client.start()
}
if (preferences.getBoolean(R.string.p_strict_mode_thread, false)) {
val builder = StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog()
if (preferences.getBoolean(R.string.p_crash_main_queries, false)) {
@ -36,7 +52,9 @@ class BuildSetup @Inject constructor(
.detectLeakedClosableObjects()
.detectFileUriExposure()
.penaltyLog()
.detectContentUriWithoutPermission()
if (atLeastOreo()) {
builder.detectContentUriWithoutPermission()
}
if (atLeastQ()) {
builder
.detectCredentialProtectedWhileLocked()
@ -45,4 +63,4 @@ class BuildSetup @Inject constructor(
StrictMode.setVmPolicy(builder.build())
}
}
}
}

@ -0,0 +1,39 @@
package org.tasks
import android.content.Context
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.google.api.client.http.HttpRequest
import com.google.api.client.http.HttpResponse
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.OkHttpClient
import java.io.IOException
import javax.inject.Inject
class DebugNetworkInterceptor @Inject constructor(@param:ApplicationContext private val context: Context) {
fun apply(builder: OkHttpClient.Builder?) {
builder?.addNetworkInterceptor(FlipperOkhttpInterceptor(getNetworkPlugin(context)))
}
@Throws(IOException::class)
fun <T> execute(request: HttpRequest, responseClass: Class<T>): T? {
val interceptor = FlipperHttpInterceptor(getNetworkPlugin(context), responseClass)
request
.setInterceptor(interceptor)
.setResponseInterceptor(interceptor)
.execute()
return interceptor.response
}
@Throws(IOException::class)
fun <T> report(httpResponse: HttpResponse, responseClass: Class<T>, start: Long, finish: Long): T? {
val interceptor = FlipperHttpInterceptor(getNetworkPlugin(context), responseClass)
interceptor.report(httpResponse, start, finish)
return interceptor.response
}
private fun getNetworkPlugin(context: Context): NetworkFlipperPlugin {
return AndroidFlipperClient.getInstance(context).getPlugin(NetworkFlipperPlugin.ID)!!
}
}

@ -0,0 +1,82 @@
package org.tasks
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin
import com.facebook.flipper.plugins.network.NetworkReporter
import com.facebook.flipper.plugins.network.NetworkReporter.ResponseInfo
import com.google.api.client.http.*
import com.google.api.client.json.GenericJson
import org.tasks.data.UUIDHelper
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.IOException
internal class FlipperHttpInterceptor<T>(private val plugin: NetworkFlipperPlugin, private val responseClass: Class<T>) : HttpExecuteInterceptor, HttpResponseInterceptor {
private val requestId = UUIDHelper.newUUID()
var response: T? = null
private set
override fun intercept(request: HttpRequest) {
plugin.reportRequest(toRequestInfo(request, currentTimeMillis()))
}
@Throws(IOException::class)
override fun interceptResponse(response: HttpResponse) {
plugin.reportResponse(toResponseInfo(response, currentTimeMillis()))
}
@Throws(IOException::class)
fun report(response: HttpResponse, start: Long, end: Long) {
plugin.reportRequest(toRequestInfo(response.request, start))
plugin.reportResponse(toResponseInfo(response, end))
}
private fun toRequestInfo(request: HttpRequest, timestamp: Long): NetworkReporter.RequestInfo {
val requestInfo = NetworkReporter.RequestInfo()
requestInfo.method = request.requestMethod
requestInfo.body = bodyToByteArray(request.content)
requestInfo.headers = getHeaders(request.headers)
requestInfo.requestId = requestId
requestInfo.timeStamp = timestamp
requestInfo.uri = request.url.toString()
return requestInfo
}
@Throws(IOException::class)
private fun toResponseInfo(response: HttpResponse, timestamp: Long): ResponseInfo {
val responseInfo = ResponseInfo()
responseInfo.timeStamp = timestamp
responseInfo.headers = getHeaders(response.headers)
responseInfo.requestId = requestId
responseInfo.statusCode = response.statusCode
responseInfo.statusReason = response.statusMessage
this.response = response.parseAs(responseClass)
if (this.response is GenericJson) {
try {
responseInfo.body = (this.response as GenericJson).toPrettyString().toByteArray()
} catch (e: IOException) {
Timber.e(e)
}
}
return responseInfo
}
private fun getHeaders(headers: HttpHeaders): List<NetworkReporter.Header> {
return headers.map { (name, value) -> NetworkReporter.Header(name, value.toString()) }
}
private fun bodyToByteArray(content: HttpContent?): ByteArray? {
if (content == null) {
return null
}
val output = ByteArrayOutputStream()
try {
content.writeTo(output)
} catch (e: IOException) {
Timber.e(e)
return null
}
return output.toByteArray()
}
}

@ -1,19 +1,15 @@
package org.tasks.preferences.fragments
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import at.bitfire.cert4android.CustomCertManager.Companion.resetCertificates
import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.billing.BillingClient
import org.tasks.billing.Inventory
import org.tasks.data.createDueDate
import org.tasks.data.entity.Task
import org.tasks.extensions.Context.toast
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.preferences.Preferences
@ -28,14 +24,13 @@ class Debug : InjectingPreferenceFragment() {
@Inject lateinit var inventory: Inventory
@Inject lateinit var billingClient: BillingClient
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var taskDao: com.todoroo.astrid.dao.TaskDao
override fun getPreferenceXml() = R.xml.preferences_debug
override suspend fun setupPreferences(savedInstanceState: Bundle?) {
for (pref in listOf(
R.string.p_leakcanary,
R.string.p_flipper,
R.string.p_strict_mode_vm,
R.string.p_strict_mode_thread,
R.string.p_crash_main_queries
@ -59,6 +54,7 @@ class Debug : InjectingPreferenceFragment() {
}
setupIap(R.string.debug_themes, Inventory.SKU_THEMES)
setupIap(R.string.debug_tasker, Inventory.SKU_TASKER)
findPreference(R.string.debug_crash_app).setOnPreferenceClickListener {
throw RuntimeException("Crashed app from debug preferences")
@ -70,25 +66,7 @@ class Debug : InjectingPreferenceFragment() {
preferences.lastSubscribeRequest = 0L
preferences.lastReviewRequest = 0L
preferences.shownBeastModeHint = false
preferences.warnMicrosoft = true
preferences.warnGoogleTasks = true
preferences.warnQuietHoursDisabled = true
preferences.setBoolean(R.string.p_just_updated, true)
true
}
findPreference(R.string.debug_create_tasks).setOnPreferenceClickListener {
lifecycleScope.launch {
val count = 5000
for (i in 1..count) {
val task = taskCreator.createWithValues("")
taskDao.createNew(task)
task.title = "Task ${task.id}"
task.dueDate = createDueDate(Task.URGENCY_SPECIFIC_DAY, currentTimeMillis())
taskDao.save(task)
}
Toast.makeText(context, "Created $count tasks", Toast.LENGTH_SHORT).show()
}
false
}
}

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

@ -4,16 +4,16 @@
<string name="debug_strict_mode_thread">Strict mode - Thread</string>
<string name="debug_strict_mode_vm">Strict mode - VM</string>
<string name="debug_leakcanary">LeakCanary</string>
<string name="debug_flipper">Flipper</string>
<string name="debug_pro">Unlock pro</string>
<string name="debug_purchase">Purchase %s</string>
<string name="debug_consume">Consume %s</string>
<string name="debug_themes">debug_themes</string>
<string name="debug_tasker">debug_tasker</string>
<string name="debug_reset_ssl">Reset SSL certificates</string>
<string name="debug_crash_app">Crash app now</string>
<string name="debug_create_tasks">Create tasks</string>
<string name="debug_main_queries">Crash on violation</string>
<string name="debug_force_restart">Restart app</string>
<string name="debug_clear_hints">Clear hints</string>
<string name="google_oauth_scheme">com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf</string>
<string name="microsoft_oauth_path">/8wnYBRqh5nnQgFzbIXfxXSs41xE=</string>
</resources>

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

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

@ -1,40 +1,19 @@
package org.tasks.analytics
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@Suppress("UNUSED_PARAMETER")
class Firebase @Inject constructor(
@param:ApplicationContext val context: Context,
private val preferences: Preferences
) {
class Firebase @Inject constructor() {
fun reportException(t: Throwable) = Timber.e(t)
fun updateRemoteConfig() {}
fun logEvent(event: Int, vararg params: Pair<Int, Any>) {
Timber.d("${context.getString(event)} -> $params")
}
fun logEvent(event: Int, vararg params: Pair<Int, Any>) {}
fun addTask(source: String) =
logEvent(R.string.event_add_task, R.string.param_type to source)
fun addTask(source: String) {}
fun completeTask(source: String) =
logEvent(R.string.event_complete_task, R.string.param_type to source)
val subscribeCooldown: Boolean
get() = installCooldown
|| preferences.lastSubscribeRequest + days(28L) > currentTimeMillis()
private val installCooldown: Boolean
get() = preferences.installDate + days(7L) > currentTimeMillis()
private fun days(default: Long): Long =
TimeUnit.DAYS.toMillis(default)
}
val subscribeCooldown = false
val moreOptionsBadge = false
val moreOptionsSolid = false
}

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

@ -21,7 +21,6 @@ class BillingClientImpl(
) {}
override suspend fun acknowledge(purchase: Purchase) {}
override suspend fun getSkus(skus: List<String>): List<Sku> = emptyList()
override suspend fun consume(sku: String) {}

@ -4,13 +4,7 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.tasks.location.Geocoder
import org.tasks.location.GeocoderNominatim
import org.tasks.location.LocationService
import org.tasks.location.LocationServiceAndroid
import org.tasks.location.MapFragment
import org.tasks.location.OsmMapFragment
import org.tasks.wear.WearRefresher
import org.tasks.location.*
@Module
@InstallIn(SingletonComponent::class)
@ -23,9 +17,4 @@ class FlavorModule {
@Provides
fun getGeocoder(nominatim: GeocoderNominatim): Geocoder = nominatim
@Provides
fun getWearRefresher(): WearRefresher = object : WearRefresher {
override suspend fun refresh() = Unit
}
}

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

@ -40,33 +40,6 @@
android:name=".location.GoogleGeofenceTransitionIntentService"
android:permission="android.permission.BIND_JOB_SERVICE"/>
<service
android:name=".wear.WearDataService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="com.google.android.gms.wearable.REQUEST_RECEIVED" />
<data
android:host="*"
android:pathPrefix="/grpc/"
android:scheme="wear" />
</intent-filter>
</service>
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${applicationId}"
android:path="@string/microsoft_oauth_path"
android:scheme="msauth" />
</intent-filter>
</activity>
</application>
</manifest>

@ -19,42 +19,13 @@ import javax.inject.Singleton
@Singleton
class Firebase @Inject constructor(
@ApplicationContext private val context: Context,
@param:ApplicationContext val context: Context,
private val preferences: Preferences
) {
private val crashlytics by lazy {
if (preferences.isTrackingEnabled) {
FirebaseCrashlytics.getInstance().apply {
setCrashlyticsCollectionEnabled(true)
}
} else {
null
}
}
private val analytics by lazy {
if (preferences.isTrackingEnabled) {
FirebaseAnalytics.getInstance(context).apply {
setAnalyticsCollectionEnabled(true)
}
} else {
null
}
}
private val remoteConfig by lazy {
if (preferences.isTrackingEnabled) {
FirebaseRemoteConfig.getInstance().apply {
setConfigSettingsAsync(remoteConfigSettings {
minimumFetchIntervalInSeconds =
TimeUnit.HOURS.toSeconds(WorkManager.REMOTE_CONFIG_INTERVAL_HOURS)
})
setDefaultsAsync(R.xml.remote_config_defaults)
}
} else {
null
}
}
private var crashlytics: FirebaseCrashlytics? = null
private var analytics: FirebaseAnalytics? = null
private var remoteConfig: FirebaseRemoteConfig? = null
fun reportException(t: Throwable) {
Timber.e(t)
@ -79,13 +50,8 @@ class Firebase @Inject constructor(
fun addTask(source: String) =
logEvent(R.string.event_add_task, R.string.param_type to source)
fun completeTask(source: String) =
logEvent(R.string.event_complete_task, R.string.param_type to source)
fun logEvent(@StringRes event: Int, vararg p: Pair<Int, Any>) {
val eventName = context.getString(event)
Timber.d("$eventName -> $p")
analytics?.logEvent(eventName, Bundle().apply {
analytics?.logEvent(context.getString(event), Bundle().apply {
p.forEach {
val key = context.getString(it.first)
when (it.second::class) {
@ -107,6 +73,30 @@ class Firebase @Inject constructor(
get() = installCooldown
|| preferences.lastSubscribeRequest + days("subscribe_cooldown", 30L) > currentTimeMillis()
val moreOptionsBadge: Boolean
get() = remoteConfig?.getBoolean("more_options_badge") ?: false
val moreOptionsSolid: Boolean
get() = remoteConfig?.getBoolean("more_options_solid") ?: false
private fun days(key: String, default: Long): Long =
TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default)
init {
if (preferences.isTrackingEnabled) {
analytics = FirebaseAnalytics.getInstance(context).apply {
setAnalyticsCollectionEnabled(true)
}
crashlytics = FirebaseCrashlytics.getInstance().apply {
setCrashlyticsCollectionEnabled(true)
}
remoteConfig = FirebaseRemoteConfig.getInstance().apply {
setConfigSettingsAsync(remoteConfigSettings {
minimumFetchIntervalInSeconds =
TimeUnit.HOURS.toSeconds(WorkManager.REMOTE_CONFIG_INTERVAL_HOURS)
})
setDefaultsAsync(R.xml.remote_config_defaults)
}
}
}
}

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

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

@ -1,17 +1,10 @@
package org.tasks.injection
import android.content.Context
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.WearDataLayerRegistry
import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import org.tasks.extensions.wearDataLayerRegistry
import org.tasks.location.Geocoder
import org.tasks.location.GeocoderMapbox
import org.tasks.location.GoogleMapFragment
@ -21,9 +14,6 @@ import org.tasks.location.LocationServiceGooglePlay
import org.tasks.location.MapFragment
import org.tasks.location.OsmMapFragment
import org.tasks.play.PlayServices
import org.tasks.wear.WearRefresher
import org.tasks.wear.WearRefresherImpl
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@ -44,30 +34,4 @@ class FlavorModule {
@Provides
fun getGeocoder(mapbox: GeocoderMapbox): Geocoder = mapbox
@OptIn(ExperimentalHorologistApi::class)
@Provides
fun wearDataLayerRegistry(
@ApplicationContext applicationContext: Context,
@ApplicationScope coroutineScope: CoroutineScope,
) = applicationContext.wearDataLayerRegistry(coroutineScope)
@OptIn(ExperimentalHorologistApi::class)
@Provides
fun phoneDataLayerAppHelper(
@ApplicationContext applicationContext: Context,
wearDataLayerRegistry: WearDataLayerRegistry,
) = PhoneDataLayerAppHelper(
context = applicationContext,
registry = wearDataLayerRegistry,
)
@OptIn(ExperimentalHorologistApi::class)
@Provides
@Singleton
fun getWearRefresher(
phoneDataLayerAppHelper: PhoneDataLayerAppHelper,
wearDataLayerRegistry: WearDataLayerRegistry,
@ApplicationScope scope: CoroutineScope,
): WearRefresher = WearRefresherImpl(phoneDataLayerAppHelper, wearDataLayerRegistry, scope)
}

@ -2,7 +2,6 @@ package org.tasks.location
import android.annotation.SuppressLint
import android.content.Context
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
@ -24,18 +23,14 @@ class GoogleMapFragment @Inject constructor(
private var map: GoogleMap? = null
private var circle: Circle? = null
override fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean, parent: ViewGroup?) {
override fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean) {
this.callback = callback
this.dark = dark
val fragmentManager = activity.supportFragmentManager
var mapFragment = fragmentManager.findFragmentByTag(FRAG_TAG_MAP) as SupportMapFragment?
if (mapFragment == null) {
mapFragment = SupportMapFragment()
if (parent == null) {
fragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit()
} else {
fragmentManager.beginTransaction().add(parent, mapFragment, null).commit()
}
fragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit()
}
mapFragment.getMapAsync(this)
}

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

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

@ -1,65 +0,0 @@
package org.tasks.wear
import android.text.format.DateFormat
import androidx.datastore.core.DataStore
import androidx.lifecycle.lifecycleScope
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.ProtoDataStoreHelper.protoDataStore
import com.google.android.horologist.data.WearDataLayerRegistry
import com.google.android.horologist.datalayer.grpc.server.BaseGrpcDataService
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.GrpcProto.Settings
import org.tasks.WearServiceGrpcKt
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.extensions.wearDataLayerRegistry
import org.tasks.filters.FilterProvider
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import org.tasks.themes.ColorProvider
import javax.inject.Inject
@OptIn(ExperimentalHorologistApi::class)
@AndroidEntryPoint
class WearDataService : BaseGrpcDataService<WearServiceGrpcKt.WearServiceCoroutineImplBase>() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var preferences: Preferences
@Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var headerFormatter: HeaderFormatter
@Inject lateinit var firebase: Firebase
@Inject lateinit var filterProvider: FilterProvider
@Inject lateinit var inventory: Inventory
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var defaultFilterProvider: DefaultFilterProvider
@Inject lateinit var taskCreator: TaskCreator
override val registry: WearDataLayerRegistry by lazy {
applicationContext.wearDataLayerRegistry(lifecycleScope)
}
private val settings: DataStore<Settings> by lazy {
registry.protoDataStore(lifecycleScope)
}
override fun buildService(): WearServiceGrpcKt.WearServiceCoroutineImplBase {
return WearService(
taskDao = taskDao,
appPreferences = preferences,
taskCompleter = taskCompleter,
headerFormatter = headerFormatter,
settings = settings,
firebase = firebase,
filterProvider = filterProvider,
inventory = inventory,
colorProvider = colorProvider,
defaultFilterProvider = defaultFilterProvider,
taskCreator = taskCreator,
is24HourTime = DateFormat.is24HourFormat(applicationContext),
)
}
}

@ -1,16 +0,0 @@
package org.tasks.wear
import org.tasks.GrpcProto.Settings
import org.tasks.preferences.Preferences
import org.tasks.preferences.QueryPreferences
class WearPreferences(
preferences: Preferences,
private val settings: Settings,
): QueryPreferences by preferences {
override val showHidden: Boolean
get() = settings.showHidden
override val showCompleted: Boolean
get() = settings.showCompleted
}

@ -1,50 +0,0 @@
package org.tasks.wear
import androidx.datastore.core.DataStore
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.data.ProtoDataStoreHelper.protoDataStore
import com.google.android.horologist.data.WearDataLayerRegistry
import com.google.android.horologist.datalayer.phone.PhoneDataLayerAppHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.tasks.GrpcProto.LastUpdate
import org.tasks.copy
import timber.log.Timber
@OptIn(ExperimentalHorologistApi::class)
class WearRefresherImpl(
phoneDataLayerAppHelper: PhoneDataLayerAppHelper,
private val registry: WearDataLayerRegistry,
private val scope: CoroutineScope,
) : WearRefresher {
private var watchConnected = false
init {
phoneDataLayerAppHelper
.connectedAndInstalledNodes
.catch { Timber.e("${it.message}") }
.onEach { nodes ->
Timber.d("Connected nodes: ${nodes.joinToString()}")
watchConnected = nodes.isNotEmpty()
lastUpdate.update()
}
.launchIn(scope)
}
private val lastUpdate: DataStore<LastUpdate> by lazy {
registry.protoDataStore<LastUpdate>(scope)
}
override suspend fun refresh() {
if (watchConnected) {
lastUpdate.update()
}
}
}
private suspend fun DataStore<LastUpdate>.update() {
updateData { it.copy { now = System.currentTimeMillis() } }
}

@ -1,264 +0,0 @@
package org.tasks.wear
import androidx.datastore.core.DataStore
import com.todoroo.astrid.core.SortHelper.SORT_DUE
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator
import kotlinx.coroutines.flow.firstOrNull
import org.tasks.GrpcProto
import org.tasks.GrpcProto.CompleteTaskRequest
import org.tasks.GrpcProto.CompleteTaskResponse
import org.tasks.GrpcProto.GetListsResponse
import org.tasks.GrpcProto.GetTaskResponse
import org.tasks.GrpcProto.GetTasksRequest
import org.tasks.GrpcProto.ListItem
import org.tasks.GrpcProto.ListItemType
import org.tasks.GrpcProto.SaveTaskResponse
import org.tasks.GrpcProto.Tasks
import org.tasks.GrpcProto.ToggleGroupRequest
import org.tasks.GrpcProto.ToggleGroupResponse
import org.tasks.WearServiceGrpcKt
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.copy
import org.tasks.data.NO_COUNT
import org.tasks.data.isHidden
import org.tasks.filters.AstridOrderingFilter
import org.tasks.filters.Filter
import org.tasks.filters.FilterProvider
import org.tasks.filters.MyTasksFilter
import org.tasks.filters.NavigationDrawerSubheader
import org.tasks.filters.getIcon
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.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import org.tasks.tasklist.SectionedDataSource
import org.tasks.tasklist.UiItem
import org.tasks.themes.ColorProvider
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.startOfDay
import timber.log.Timber
class WearService(
private val taskDao: TaskDao,
private val appPreferences: Preferences,
private val taskCompleter: TaskCompleter,
private val headerFormatter: HeaderFormatter,
private val settings: DataStore<GrpcProto.Settings>,
private val firebase: Firebase,
private val filterProvider: FilterProvider,
private val inventory: Inventory,
private val colorProvider: ColorProvider,
private val defaultFilterProvider: DefaultFilterProvider,
private val taskCreator: TaskCreator,
private val is24HourTime: Boolean,
) : WearServiceGrpcKt.WearServiceCoroutineImplBase() {
override suspend fun getTasks(request: GetTasksRequest): Tasks {
val position = request.position
val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE
val settingsData = settings.data.firstOrNull() ?: GrpcProto.Settings.getDefaultInstance()
val filter =
defaultFilterProvider.getFilterFromPreference(settingsData.filter.takeIf { it.isNotBlank() })
val preferences = WearPreferences(appPreferences, settingsData)
val collapsed = settingsData?.collapsedList?.toSet() ?: emptySet()
val payload = SectionedDataSource(
tasks = taskDao.fetchTasks(preferences, filter),
disableHeaders = filter.disableHeaders()
|| (filter.supportsManualSort() && preferences.isManualSort)
|| (filter is AstridOrderingFilter && preferences.isAstridSort),
groupMode = preferences.groupMode,
subtaskMode = preferences.subtaskMode,
completedAtBottom = preferences.completedTasksAtBottom,
collapsed = collapsed,
)
return Tasks.newBuilder()
.setTotalItems(payload.size)
.addAllItems(
payload
.subList(position, position + limit)
.map { item ->
when (item) {
is UiItem.Header ->
GrpcProto.UiItem.newBuilder()
.setId(item.value)
.setType(ListItemType.Header)
.setTitle(headerFormatter.headerString(item.value, style = DateStyle.MEDIUM))
.setCollapsed(item.collapsed)
.build()
is UiItem.Task -> {
val timestamp = if (preferences.groupMode == SORT_DUE &&
(item.task.sortGroup
?: 0) >= currentTimeMillis().startOfDay()
) {
item.task.takeIf { it.hasDueTime() }?.let {
getTimeString(item.task.dueDate, is24HourTime)
}
} else if (item.task.hasDueDate()) {
getRelativeDateTime(
item.task.dueDate,
is24HourTime,
)
} else {
null
}
GrpcProto.UiItem.newBuilder()
.setType(ListItemType.Item)
.setId(item.task.id)
.setPriority(item.task.priority)
.setCompleted(item.task.isCompleted)
.setHidden(item.task.task.isHidden)
.setIndent(item.task.indent)
.setCollapsed(item.task.isCollapsed)
.setNumSubtasks(item.task.children)
.apply {
if (item.task.title != null) {
setTitle(item.task.title)
}
if (timestamp != null) {
setTimestamp(timestamp)
}
}
.setRepeating(item.task.task.isRecurring)
.build()
}
}
}
)
.build()
}
override suspend fun completeTask(request: CompleteTaskRequest): CompleteTaskResponse {
taskCompleter.setComplete(request.id, request.completed)
firebase.completeTask("wearable")
return CompleteTaskResponse.newBuilder().setSuccess(true).build()
}
override suspend fun toggleGroup(request: ToggleGroupRequest): ToggleGroupResponse {
settings.updateData {
it.copy {
if (request.collapsed) {
if (!collapsed.contains(request.value)) {
collapsed.add(request.value)
}
} else {
if (collapsed.contains(request.value)) {
collapsed.clear()
collapsed.addAll(
it.collapsedList.toMutableList().apply { remove(request.value) })
}
}
}
}
return ToggleGroupResponse.getDefaultInstance()
}
override suspend fun updateSettings(request: GrpcProto.UpdateSettingsRequest): GrpcProto.Settings {
return settings.updateData { request.settings }
}
override suspend fun toggleSubtasks(request: ToggleGroupRequest): ToggleGroupResponse {
taskDao.setCollapsed(request.value, request.collapsed)
return ToggleGroupResponse.newBuilder().build()
}
override suspend fun getLists(request: GrpcProto.GetListsRequest): GetListsResponse {
val position = request.position
val limit = request.limit.takeIf { it > 0 } ?: Int.MAX_VALUE
val selected = settings.data.firstOrNull()?.filter?.takeIf { it.isNotBlank() }
?: defaultFilterProvider.getFilterPreferenceValue(MyTasksFilter.create())
val filters = filterProvider.wearableFilters()
return GetListsResponse.newBuilder()
.setTotalItems(filters.size)
.addAllItems(
filters
.subList(position, (position + limit).coerceAtMost(filters.size))
.map { item ->
when (item) {
is Filter -> {
ListItem.newBuilder()
.setId(defaultFilterProvider.getFilterPreferenceValue(item))
.setType(ListItemType.Item)
.setTitle(item.title ?: "")
.setIcon(item.getIcon(inventory))
.setColor(getColor(item))
.setTaskCount(item.count.takeIf { it != NO_COUNT } ?: try {
taskDao.count(item)
} catch (e: Exception) {
Timber.e(e)
0
})
.build()
}
is NavigationDrawerSubheader ->
ListItem.newBuilder()
.setType(ListItemType.Header)
.setTitle(item.title ?: "")
.setId("${item.subheaderType}_${item.id}")
.build()
else -> throw IllegalArgumentException()
}
}
)
.build()
}
override suspend fun getTask(request: GrpcProto.GetTaskRequest): GetTaskResponse {
Timber.d("getTask($request)")
val task = taskDao.fetch(request.taskId)
?: throw IllegalArgumentException()
return GetTaskResponse.newBuilder()
.setTitle(task.title ?: "")
.setCompleted(task.isCompleted)
.setPriority(task.priority)
.setRepeating(task.isRecurring)
.build()
}
override suspend fun saveTask(request: GrpcProto.SaveTaskRequest): SaveTaskResponse {
Timber.d("saveTask($request)")
if (request.taskId == 0L) {
val filter = defaultFilterProvider.getFilterFromPreference(
settings.data.firstOrNull()?.filter?.takeIf { it.isNotBlank() }
)
val task = taskCreator.basicQuickAddTask(
title = request.title,
filter = filter,
)
firebase.addTask("wearable")
return SaveTaskResponse.newBuilder().setTaskId(task.id).build()
} else {
taskDao.fetch(request.taskId)?.let { task ->
taskDao.save(
task.copy(
title = request.title,
completionDate = when {
!request.completed -> 0
task.isCompleted -> task.completionDate
else -> System.currentTimeMillis()
},
)
)
}
return SaveTaskResponse.newBuilder().setTaskId(request.taskId).build()
}
}
private fun getColor(filter: Filter): Int {
if (filter.tint != 0) {
val color = colorProvider.getThemeColor(filter.tint, true)
if (color.isFree || inventory.purchasedThemes()) {
return color.primaryColor
}
}
return 0
}
}

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

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

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

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

Loading…
Cancel
Save